Merge branch 'dev' into moulinette-logging

This commit is contained in:
Alexandre Aubin 2021-02-28 17:03:15 +01:00 committed by GitHub
commit 38bed2aab2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
96 changed files with 7943 additions and 4559 deletions

View file

@ -14,7 +14,7 @@ generate-helpers-doc:
- cd doc
- python generate_helper_doc.py
- hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo
- cp helpers.html doc_repo/packaging_apps_helpers.md
- cp helpers.md doc_repo/pages/02.contribute/04.packaging_apps/11.helpers/packaging_apps_helpers.md
- cd doc_repo
# replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ?
- hub checkout -b "${CI_COMMIT_REF_NAME}"
@ -22,6 +22,6 @@ generate-helpers-doc:
- hub pull-request -m "[CI] Helper for ${CI_COMMIT_REF_NAME}" -p # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd
artifacts:
paths:
- doc/helpers.html
- doc/helpers.md
only:
- tags

View file

@ -21,8 +21,8 @@ invalidcode37:
format-check:
stage: lint
image: "before-install"
needs: []
allow_failure: true
needs: []
script:
- tox -e py37-black-check

View file

@ -451,6 +451,14 @@ domain:
help: Domain to delete
extra:
pattern: *pattern_domain
-r:
full: --remove-apps
help: Remove apps installed on the domain
action: store_true
-f:
full: --force
help: Do not ask confirmation to remove apps
action: store_true
### domain_dns_conf()
dns-conf:
@ -1355,13 +1363,11 @@ dyndns:
### dyndns_installcron()
installcron:
action_help: Install IP update cron
api: POST /dyndns/cron
deprecated: true
### dyndns_removecron()
removecron:
action_help: Remove IP update cron
api: DELETE /dyndns/cron
deprecated: true
#############################

View file

@ -12,28 +12,33 @@ import os
import yaml
THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ACTIONSMAP_FILE = THIS_SCRIPT_DIR + '/yunohost.yml'
BASH_COMPLETION_FILE = THIS_SCRIPT_DIR + '/../bash-completion.d/yunohost'
ACTIONSMAP_FILE = THIS_SCRIPT_DIR + "/yunohost.yml"
BASH_COMPLETION_FILE = THIS_SCRIPT_DIR + "/../bash-completion.d/yunohost"
def get_dict_actions(OPTION_SUBTREE, category):
ACTIONS = [action for action in OPTION_SUBTREE[category]["actions"].keys()
if not action.startswith('_')]
ACTIONS_STR = '{}'.format(' '.join(ACTIONS))
ACTIONS = [
action
for action in OPTION_SUBTREE[category]["actions"].keys()
if not action.startswith("_")
]
ACTIONS_STR = "{}".format(" ".join(ACTIONS))
DICT = {"actions_str": ACTIONS_STR}
return DICT
with open(ACTIONSMAP_FILE, 'r') as stream:
with open(ACTIONSMAP_FILE, "r") as stream:
# Getting the dictionary containning what actions are possible per category
OPTION_TREE = yaml.load(stream)
CATEGORY = [category for category in OPTION_TREE.keys() if not category.startswith('_')]
CATEGORY = [
category for category in OPTION_TREE.keys() if not category.startswith("_")
]
CATEGORY_STR = '{}'.format(' '.join(CATEGORY))
CATEGORY_STR = "{}".format(" ".join(CATEGORY))
ACTIONS_DICT = {}
for category in CATEGORY:
ACTIONS_DICT[category] = get_dict_actions(OPTION_TREE, category)
@ -42,86 +47,112 @@ with open(ACTIONSMAP_FILE, 'r') as stream:
ACTIONS_DICT[category]["subcategories_str"] = ""
if "subcategories" in OPTION_TREE[category].keys():
SUBCATEGORIES = [subcategory for subcategory in OPTION_TREE[category]["subcategories"].keys()]
SUBCATEGORIES = [
subcategory
for subcategory in OPTION_TREE[category]["subcategories"].keys()
]
SUBCATEGORIES_STR = '{}'.format(' '.join(SUBCATEGORIES))
SUBCATEGORIES_STR = "{}".format(" ".join(SUBCATEGORIES))
ACTIONS_DICT[category]["subcategories_str"] = SUBCATEGORIES_STR
for subcategory in SUBCATEGORIES:
ACTIONS_DICT[category]["subcategories"][subcategory] = get_dict_actions(OPTION_TREE[category]["subcategories"], subcategory)
ACTIONS_DICT[category]["subcategories"][subcategory] = get_dict_actions(
OPTION_TREE[category]["subcategories"], subcategory
)
with open(BASH_COMPLETION_FILE, 'w') as generated_file:
with open(BASH_COMPLETION_FILE, "w") as generated_file:
# header of the file
generated_file.write('#\n')
generated_file.write('# completion for yunohost\n')
generated_file.write('# automatically generated from the actionsmap\n')
generated_file.write('#\n\n')
generated_file.write("#\n")
generated_file.write("# completion for yunohost\n")
generated_file.write("# automatically generated from the actionsmap\n")
generated_file.write("#\n\n")
# Start of the completion function
generated_file.write('_yunohost()\n')
generated_file.write('{\n')
generated_file.write("_yunohost()\n")
generated_file.write("{\n")
# Defining local variable for previously and currently typed words
generated_file.write('\tlocal cur prev opts narg\n')
generated_file.write('\tCOMPREPLY=()\n\n')
generated_file.write('\t# the number of words already typed\n')
generated_file.write('\tnarg=${#COMP_WORDS[@]}\n\n')
generated_file.write('\t# the current word being typed\n')
generated_file.write("\tlocal cur prev opts narg\n")
generated_file.write("\tCOMPREPLY=()\n\n")
generated_file.write("\t# the number of words already typed\n")
generated_file.write("\tnarg=${#COMP_WORDS[@]}\n\n")
generated_file.write("\t# the current word being typed\n")
generated_file.write('\tcur="${COMP_WORDS[COMP_CWORD]}"\n\n')
# If one is currently typing a category then match with the category list
generated_file.write('\t# If one is currently typing a category,\n')
generated_file.write('\t# match with categorys\n')
generated_file.write('\tif [[ $narg == 2 ]]; then\n')
generated_file.write("\t# If one is currently typing a category,\n")
generated_file.write("\t# match with categorys\n")
generated_file.write("\tif [[ $narg == 2 ]]; then\n")
generated_file.write('\t\topts="{}"\n'.format(CATEGORY_STR))
generated_file.write('\tfi\n\n')
generated_file.write("\tfi\n\n")
# If one is currently typing an action then match with the action list
# of the previously typed category
generated_file.write('\t# If one already typed a category,\n')
generated_file.write('\t# match the actions or the subcategories of that category\n')
generated_file.write('\tif [[ $narg == 3 ]]; then\n')
generated_file.write('\t\t# the category typed\n')
generated_file.write("\t# If one already typed a category,\n")
generated_file.write(
"\t# match the actions or the subcategories of that category\n"
)
generated_file.write("\tif [[ $narg == 3 ]]; then\n")
generated_file.write("\t\t# the category typed\n")
generated_file.write('\t\tcategory="${COMP_WORDS[1]}"\n\n')
for category in CATEGORY:
generated_file.write('\t\tif [[ $category == "{}" ]]; then\n'.format(category))
generated_file.write('\t\t\topts="{} {}"\n'.format(ACTIONS_DICT[category]["actions_str"], ACTIONS_DICT[category]["subcategories_str"]))
generated_file.write('\t\tfi\n')
generated_file.write('\tfi\n\n')
generated_file.write(
'\t\tif [[ $category == "{}" ]]; then\n'.format(category)
)
generated_file.write(
'\t\t\topts="{} {}"\n'.format(
ACTIONS_DICT[category]["actions_str"],
ACTIONS_DICT[category]["subcategories_str"],
)
)
generated_file.write("\t\tfi\n")
generated_file.write("\tfi\n\n")
generated_file.write('\t# If one already typed an action or a subcategory,\n')
generated_file.write('\t# match the actions of that subcategory\n')
generated_file.write('\tif [[ $narg == 4 ]]; then\n')
generated_file.write('\t\t# the category typed\n')
generated_file.write("\t# If one already typed an action or a subcategory,\n")
generated_file.write("\t# match the actions of that subcategory\n")
generated_file.write("\tif [[ $narg == 4 ]]; then\n")
generated_file.write("\t\t# the category typed\n")
generated_file.write('\t\tcategory="${COMP_WORDS[1]}"\n\n')
generated_file.write('\t\t# the action or the subcategory typed\n')
generated_file.write("\t\t# the action or the subcategory typed\n")
generated_file.write('\t\taction_or_subcategory="${COMP_WORDS[2]}"\n\n')
for category in CATEGORY:
if len(ACTIONS_DICT[category]["subcategories"]):
generated_file.write('\t\tif [[ $category == "{}" ]]; then\n'.format(category))
generated_file.write(
'\t\tif [[ $category == "{}" ]]; then\n'.format(category)
)
for subcategory in ACTIONS_DICT[category]["subcategories"]:
generated_file.write('\t\t\tif [[ $action_or_subcategory == "{}" ]]; then\n'.format(subcategory))
generated_file.write('\t\t\t\topts="{}"\n'.format(ACTIONS_DICT[category]["subcategories"][subcategory]["actions_str"]))
generated_file.write('\t\t\tfi\n')
generated_file.write('\t\tfi\n')
generated_file.write('\tfi\n\n')
generated_file.write(
'\t\t\tif [[ $action_or_subcategory == "{}" ]]; then\n'.format(
subcategory
)
)
generated_file.write(
'\t\t\t\topts="{}"\n'.format(
ACTIONS_DICT[category]["subcategories"][subcategory][
"actions_str"
]
)
)
generated_file.write("\t\t\tfi\n")
generated_file.write("\t\tfi\n")
generated_file.write("\tfi\n\n")
# If both category and action have been typed or the category
# was not recognized propose --help (only once)
generated_file.write('\t# If no options were found propose --help\n')
generated_file.write("\t# If no options were found propose --help\n")
generated_file.write('\tif [ -z "$opts" ]; then\n')
generated_file.write('\t\tprev="${COMP_WORDS[COMP_CWORD-1]}"\n\n')
generated_file.write('\t\tif [[ $prev != "--help" ]]; then\n')
generated_file.write('\t\t\topts=( --help )\n')
generated_file.write('\t\tfi\n')
generated_file.write('\tfi\n')
generated_file.write("\t\t\topts=( --help )\n")
generated_file.write("\t\tfi\n")
generated_file.write("\tfi\n")
# generate the completion list from the possible options
generated_file.write('\tCOMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )\n')
generated_file.write('\treturn 0\n')
generated_file.write('}\n\n')
generated_file.write("\treturn 0\n")
generated_file.write("}\n\n")
# Add the function to bash completion
generated_file.write('complete -F _yunohost yunohost')
generated_file.write("complete -F _yunohost yunohost")

View file

@ -459,11 +459,11 @@ ynh_remove_extra_repo () {
ynh_handle_getopts_args "$@"
name="${name:-$app}"
ynh_secure_remove "/etc/apt/sources.list.d/$name.list"
ynh_secure_remove --file="/etc/apt/sources.list.d/$name.list"
# Sury pinning is managed by the regenconf in the core...
[[ "$name" == "extra_php_version" ]] || ynh_secure_remove "/etc/apt/preferences.d/$name"
ynh_secure_remove "/etc/apt/trusted.gpg.d/$name.gpg" > /dev/null
ynh_secure_remove "/etc/apt/trusted.gpg.d/$name.asc" > /dev/null
ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.gpg" > /dev/null
ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.asc" > /dev/null
# Update the list of package to exclude the old repo
ynh_package_update

View file

@ -399,6 +399,15 @@ ynh_delete_file_checksum () {
ynh_app_setting_delete --app=$app --key=$checksum_setting_name
}
# Checks a backup archive exists
#
# [internal]
#
ynh_backup_archive_exists () {
yunohost backup list --output-as json --quiet \
| jq -e --arg archive "$1" '.archives | index($archive)' >/dev/null
}
# Make a backup in case of failed upgrade
#
# usage:
@ -423,7 +432,7 @@ ynh_backup_before_upgrade () {
if [ "$NO_BACKUP_UPGRADE" -eq 0 ]
then
# Check if a backup already exists with the prefix 1
if yunohost backup list | grep --quiet $app_bck-pre-upgrade1
if ynh_backup_archive_exists "$app_bck-pre-upgrade1"
then
# Prefix becomes 2 to preserve the previous backup
backup_number=2
@ -435,7 +444,7 @@ ynh_backup_before_upgrade () {
if [ "$?" -eq 0 ]
then
# If the backup succeeded, remove the previous backup
if yunohost backup list | grep --quiet $app_bck-pre-upgrade$old_backup_number
if ynh_backup_archive_exists "$app_bck-pre-upgrade$old_backup_number"
then
# Remove the previous backup only if it exists
yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null
@ -467,7 +476,7 @@ ynh_restore_upgradebackup () {
if [ "$NO_BACKUP_UPGRADE" -eq 0 ]
then
# Check if an existing backup can be found before removing and restoring the application.
if yunohost backup list | grep --quiet $app_bck-pre-upgrade$backup_number
if ynh_backup_archive_exists "$app_bck-pre-upgrade$backup_number"
then
# Remove the application then restore it
yunohost app remove $app

View file

@ -16,11 +16,8 @@
# | for example : 'var_1 var_2 ...'
#
# This will use a template in ../conf/f2b_jail.conf and ../conf/f2b_filter.conf
# __APP__ by $app
#
# You can dynamically replace others variables by example :
# __VAR_1__ by $var_1
# __VAR_2__ by $var_2
# See the documentation of ynh_add_config for a description of the template
# format and how placeholders are replaced with actual variables.
#
# Generally your template will look like that by example (for synapse):
#
@ -64,73 +61,45 @@
# Requires YunoHost version 3.5.0 or higher.
ynh_add_fail2ban_config () {
# Declare an array to define the options of this helper.
local legacy_args=lrmptv
local -A args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template [v]=others_var=)
local legacy_args=lrmpt
local -A args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template)
local logpath
local failregex
local max_retry
local ports
local others_var
local use_template
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
max_retry=${max_retry:-3}
ports=${ports:-http,https}
others_var=${others_var:-}
use_template="${use_template:-0}"
finalfail2banjailconf="/etc/fail2ban/jail.d/$app.conf"
finalfail2banfilterconf="/etc/fail2ban/filter.d/$app.conf"
ynh_backup_if_checksum_is_different "$finalfail2banjailconf"
ynh_backup_if_checksum_is_different "$finalfail2banfilterconf"
if [ $use_template -eq 1 ]
if [ $use_template -ne 1 ]
then
# Usage 2, templates
cp ../conf/f2b_jail.conf $finalfail2banjailconf
cp ../conf/f2b_filter.conf $finalfail2banfilterconf
if [ -n "${app:-}" ]
then
ynh_replace_string "__APP__" "$app" "$finalfail2banjailconf"
ynh_replace_string "__APP__" "$app" "$finalfail2banfilterconf"
fi
# Replace all other variable given as arguments
for var_to_replace in $others_var
do
# ${var_to_replace^^} make the content of the variable on upper-cases
# ${!var_to_replace} get the content of the variable named $var_to_replace
ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalfail2banjailconf"
ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalfail2banfilterconf"
done
else
# Usage 1, no template. Build a config file from scratch.
test -n "$logpath" || ynh_die "ynh_add_fail2ban_config expects a logfile path as first argument and received nothing."
test -n "$failregex" || ynh_die "ynh_add_fail2ban_config expects a failure regex as second argument and received nothing."
test -n "$logpath" || ynh_die --message="ynh_add_fail2ban_config expects a logfile path as first argument and received nothing."
test -n "$failregex" || ynh_die --message="ynh_add_fail2ban_config expects a failure regex as second argument and received nothing."
tee $finalfail2banjailconf <<EOF
[$app]
echo "
[__APP__]
enabled = true
port = $ports
filter = $app
logpath = $logpath
maxretry = $max_retry
EOF
port = __PORTS__
filter = __APP__
logpath = __LOGPATH__
maxretry = __MAX_RETRY__
" > $YNH_APP_BASEDIR/conf/f2b_jail.conf
tee $finalfail2banfilterconf <<EOF
echo "
[INCLUDES]
before = common.conf
[Definition]
failregex = $failregex
failregex = __FAILREGEX__
ignoreregex =
EOF
" > $YNH_APP_BASEDIR/conf/f2b_filter.conf
fi
# Common to usage 1 and 2.
ynh_store_file_checksum "$finalfail2banjailconf"
ynh_store_file_checksum "$finalfail2banfilterconf"
ynh_add_config --template="$YNH_APP_BASEDIR/conf/f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf"
ynh_add_config --template="$YNH_APP_BASEDIR/conf/f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf"
ynh_systemd_action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) Fail2Ban Service" --log_path=systemd
@ -148,7 +117,7 @@ EOF
#
# Requires YunoHost version 3.5.0 or higher.
ynh_remove_fail2ban_config () {
ynh_secure_remove "/etc/fail2ban/jail.d/$app.conf"
ynh_secure_remove "/etc/fail2ban/filter.d/$app.conf"
ynh_secure_remove --file="/etc/fail2ban/jail.d/$app.conf"
ynh_secure_remove --file="/etc/fail2ban/filter.d/$app.conf"
ynh_systemd_action --service_name=fail2ban --action=reload
}

93
data/helpers.d/multimedia Normal file
View file

@ -0,0 +1,93 @@
#!/bin/bash
readonly MEDIA_GROUP=multimedia
readonly MEDIA_DIRECTORY=/home/yunohost.multimedia
# Initialize the multimedia directory system
#
# usage: ynh_multimedia_build_main_dir
ynh_multimedia_build_main_dir() {
## Création du groupe multimedia
groupadd -f $MEDIA_GROUP
## Création des dossiers génériques
mkdir -p "$MEDIA_DIRECTORY"
mkdir -p "$MEDIA_DIRECTORY/share"
mkdir -p "$MEDIA_DIRECTORY/share/Music"
mkdir -p "$MEDIA_DIRECTORY/share/Picture"
mkdir -p "$MEDIA_DIRECTORY/share/Video"
mkdir -p "$MEDIA_DIRECTORY/share/eBook"
## Création des dossiers utilisateurs
for user in $(yunohost user list --output-as json | jq -r '.users | keys[]')
do
mkdir -p "$MEDIA_DIRECTORY/$user"
mkdir -p "$MEDIA_DIRECTORY/$user/Music"
mkdir -p "$MEDIA_DIRECTORY/$user/Picture"
mkdir -p "$MEDIA_DIRECTORY/$user/Video"
mkdir -p "$MEDIA_DIRECTORY/$user/eBook"
ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share"
# Création du lien symbolique dans le home de l'utilisateur.
ln -sfn "$MEDIA_DIRECTORY/$user" "/home/$user/Multimedia"
# Propriétaires des dossiers utilisateurs.
chown -R $user "$MEDIA_DIRECTORY/$user"
done
# Default yunohost hooks for post_user_create,delete will take care
# of creating/deleting corresponding multimedia folders when users
# are created/deleted in the future...
## Application des droits étendus sur le dossier multimedia.
# Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other:
setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY"
# Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers.
setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY"
# Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl.
setfacl -RL -m m::rwx "$MEDIA_DIRECTORY"
}
# Add a directory in yunohost.multimedia
# This "directory" will be a symbolic link to a existing directory.
#
# usage: ynh_multimedia_addfolder "Source directory" "Destination directory"
#
# | arg: -s, --source_dir= - Source directory - The real directory which contains your medias.
# | arg: -d, --dest_dir= - Destination directory - The name and the place of the symbolic link, relative to "/home/yunohost.multimedia"
ynh_multimedia_addfolder() {
# Declare an array to define the options of this helper.
local legacy_args=sd
local -A args_array=( [s]=source_dir= [d]=dest_dir= )
local source_dir
local dest_dir
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
# Ajout d'un lien symbolique vers le dossier à partager
ln -sfn "$source_dir" "$MEDIA_DIRECTORY/$dest_dir"
## Application des droits étendus sur le dossier ajouté
# Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other:
setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir"
# Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers.
setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir"
# Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl.
setfacl -RL -m m::rwx "$source_dir"
}
# Allow an user to have an write authorisation in multimedia directories
#
# usage: ynh_multimedia_addaccess user_name
#
# | arg: -u, --user_name= - The name of the user which gain this access.
ynh_multimedia_addaccess () {
# Declare an array to define the options of this helper.
local legacy_args=u
declare -Ar args_array=( [u]=user_name=)
local user_name
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
groupadd -f multimedia
usermod -a -G multimedia $user_name
}

View file

@ -27,7 +27,7 @@ ynh_find_port () {
# Test if a port is available
#
# example: ynh_port_available --port=1234 || ynh_die "Port 1234 is needs to be available for this app"
# example: ynh_port_available --port=1234 || ynh_die --message="Port 1234 is needs to be available for this app"
#
# usage: ynh_find_port --port=XYZ
# | arg: -p, --port= - port to check

View file

@ -2,69 +2,33 @@
# Create a dedicated nginx config
#
# usage: ynh_add_nginx_config "list of others variables to replace"
#
# | arg: list - (Optional) list of others variables to replace separated by spaces. For example : 'path_2 port_2 ...'
# usage: ynh_add_nginx_config
#
# This will use a template in ../conf/nginx.conf
# __PATH__ by $path_url
# __DOMAIN__ by $domain
# __PORT__ by $port
# __NAME__ by $app
# __FINALPATH__ by $final_path
# __PHPVERSION__ by $YNH_PHP_VERSION ($YNH_PHP_VERSION is either the default php version or the version defined for the app)
# See the documentation of ynh_add_config for a description of the template
# format and how placeholders are replaced with actual variables.
#
# And dynamic variables (from the last example) :
# __PATH_2__ by $path_2
# __PORT_2__ by $port_2
# Additionally, ynh_add_nginx_config will replace:
# - #sub_path_only by empty string if path_url is not '/'
# - #root_path_only by empty string if path_url *is* '/'
#
# This allows to enable/disable specific behaviors dependenging on the install
# location
#
# Requires YunoHost version 2.7.2 or higher.
# Requires YunoHost version 2.7.13 or higher for dynamic variables
ynh_add_nginx_config () {
finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf"
local others_var=${1:-}
ynh_backup_if_checksum_is_different --file="$finalnginxconf"
cp ../conf/nginx.conf "$finalnginxconf"
# To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable.
# Substitute in a nginx config file only if the variable is not empty
if test -n "${path_url:-}"
then
# path_url_slash_less is path_url, or a blank value if path_url is only '/'
local path_url_slash_less=${path_url%/}
ynh_replace_string --match_string="__PATH__/" --replace_string="$path_url_slash_less/" --target_file="$finalnginxconf"
ynh_replace_string --match_string="__PATH__" --replace_string="$path_url" --target_file="$finalnginxconf"
fi
if test -n "${domain:-}"; then
ynh_replace_string --match_string="__DOMAIN__" --replace_string="$domain" --target_file="$finalnginxconf"
fi
if test -n "${port:-}"; then
ynh_replace_string --match_string="__PORT__" --replace_string="$port" --target_file="$finalnginxconf"
fi
if test -n "${app:-}"; then
ynh_replace_string --match_string="__NAME__" --replace_string="$app" --target_file="$finalnginxconf"
fi
if test -n "${final_path:-}"; then
ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalnginxconf"
fi
ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$YNH_PHP_VERSION" --target_file="$finalnginxconf"
# Replace all other variable given as arguments
for var_to_replace in $others_var
do
# ${var_to_replace^^} make the content of the variable on upper-cases
# ${!var_to_replace} get the content of the variable named $var_to_replace
ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalnginxconf"
done
local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf"
if [ "${path_url:-}" != "/" ]
then
ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$finalnginxconf"
ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$YNH_APP_BASEDIR/conf/nginx.conf"
else
ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$finalnginxconf"
ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$YNH_APP_BASEDIR/conf/nginx.conf"
fi
ynh_store_file_checksum --file="$finalnginxconf"
ynh_add_config --template="$YNH_APP_BASEDIR/conf/nginx.conf" --destination="$finalnginxconf"
ynh_systemd_action --service_name=nginx --action=reload
}

View file

@ -25,19 +25,14 @@
# usage: ynh_permission_create --permission="permission" [--url="url"] [--additional_urls="second-url" [ "third-url" ]] [--auth_header=true|false]
# [--allowed=group1 [ group2 ]] [--label="label"] [--show_tile=true|false]
# [--protected=true|false]
# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist)
# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden.
# | Not that if 'show_tile' is enabled, this URL will be the URL of the tile.
# | arg: -A, additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden
# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true
# | arg: -a, allowed= - (optional) A list of group/user to allow for the permission
# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin.
# | Default is "APP_LABEL (permission name)".
# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter.
# | Default is false (for the permission different than 'main').
# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator
# | won't be able to add or remove the visitors group of this permission.
# | By default it's 'false'
# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist)
# | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden. Note that if 'show_tile' is enabled, this URL will be the URL of the tile.
# | arg: -A, --additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden
# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true
# | arg: -a, --allowed= - (optional) A list of group/user to allow for the permission
# | arg: -l, --label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. Default is "APP_LABEL (permission name)".
# | arg: -t, --show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter. Defaults to false for the permission different than 'main'.
# | arg: -P, --protected= - (optional) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. Defaults to 'false'.
#
# If provided, 'url' or 'additional_urls' is assumed to be relative to the app domain/path if they
# start with '/'. For example:
@ -185,20 +180,20 @@ ynh_permission_exists() {
local permission
ynh_handle_getopts_args "$@"
yunohost user permission list --short | grep --word-regexp --quiet "$app.$permission"
yunohost user permission list --output-as json --quiet \
| jq -e --arg perm "$app.$permission" '.permissions[$perm]' >/dev/null
}
# Redefine the url associated to a permission
#
# usage: ynh_permission_url --permission "permission" [--url="url"] [--add_url="new-url" [ "other-new-url" ]] [--remove_url="old-url" [ "other-old-url" ]]
# [--auth_header=true|false] [--clear_urls]
# | arg: -p, permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed)
# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden.
# | Note that if you want to remove url you can pass an empty sting as arguments ("").
# | arg: -a, add_url= - (optional) List of additional url to add for which access will be allowed/forbidden.
# | arg: -r, remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden
# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application
# | arg: -c, clear_urls - (optional) Clean all urls (url and additional_urls)
# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed)
# | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden. Note that if you want to remove url you can pass an empty sting as arguments ("").
# | arg: -a, --add_url= - (optional) List of additional url to add for which access will be allowed/forbidden.
# | arg: -r, --remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden
# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application
# | arg: -c, --clear_urls - (optional) Clean all urls (url and additional_urls)
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_url() {
@ -268,13 +263,12 @@ ynh_permission_url() {
#
# usage: ynh_permission_update --permission "permission" [--add="group" ["group" ...]] [--remove="group" ["group" ...]]
# [--label="label"] [--show_tile=true|false] [--protected=true|false]
# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist)
# | arg: -a, add= - the list of group or users to enable add to the permission
# | arg: -r, remove= - the list of group or users to remove from the permission
# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin.
# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO
# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator
# | won't be able to add or remove the visitors group of this permission.
# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist)
# | arg: -a, --add= - the list of group or users to enable add to the permission
# | arg: -r, --remove= - the list of group or users to remove from the permission
# | arg: -l, --label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin.
# | arg: -t, --show_tile= - (optional) Define if a tile will be shown in the SSO
# | arg: -P, --protected= - (optional) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission.
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_update() {
@ -366,7 +360,8 @@ ynh_permission_has_user() {
return 1
fi
yunohost user permission info "$app.$permission" | grep --word-regexp --quiet "$user"
yunohost user permission info "$app.$permission" --output-as json --quiet \
| jq -e --arg user $user '.corresponding_users | index($user)' >/dev/null
}
# Check if a legacy permissions exist

View file

@ -132,7 +132,6 @@ ynh_add_fpm_config () {
ynh_app_setting_set --app=$app --key=fpm_service --value="$fpm_service"
ynh_app_setting_set --app=$app --key=fpm_dedicated_service --value="$dedicated_service"
ynh_app_setting_set --app=$app --key=phpversion --value=$phpversion
finalphpconf="$fpm_config_dir/pool.d/$app.conf"
# Migrate from mutual PHP service to dedicated one.
if [ $dedicated_service -eq 1 ]
@ -151,23 +150,12 @@ ynh_add_fpm_config () {
fi
fi
ynh_backup_if_checksum_is_different --file="$finalphpconf"
if [ $use_template -eq 1 ]
then
# Usage 1, use the template in conf/php-fpm.conf
local phpfpm_path="../conf/php-fpm.conf"
if [ ! -e "$phpfpm_path" ]; then
phpfpm_path="../settings/conf/php-fpm.conf" # Into the restore script, the PHP-FPM template is not at the same place
fi
local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf"
# Make sure now that the template indeed exists
[ -e "$phpfpm_path" ] || ynh_die --message="Unable to find template to configure PHP-FPM."
cp "$phpfpm_path" "$finalphpconf"
ynh_replace_string --match_string="__NAMETOCHANGE__" --replace_string="$app" --target_file="$finalphpconf"
ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalphpconf"
ynh_replace_string --match_string="__USER__" --replace_string="$app" --target_file="$finalphpconf"
ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$phpversion" --target_file="$finalphpconf"
else
# Usage 2, generate a PHP-FPM config file with ynh_get_scalable_phpfpm
@ -178,87 +166,83 @@ ynh_add_fpm_config () {
# Define the values to use for the configuration of PHP.
ynh_get_scalable_phpfpm --usage=$usage --footprint=$footprint
# Copy the default file
cp "/etc/php/$phpversion/fpm/pool.d/www.conf" "$finalphpconf"
local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf"
echo "
[__APP__]
# Replace standard variables into the default file
ynh_replace_string --match_string="^\[www\]" --replace_string="[$app]" --target_file="$finalphpconf"
ynh_replace_string --match_string=".*listen = .*" --replace_string="listen = /var/run/php/php$phpversion-fpm-$app.sock" --target_file="$finalphpconf"
ynh_replace_string --match_string="^user = .*" --replace_string="user = $app" --target_file="$finalphpconf"
ynh_replace_string --match_string="^group = .*" --replace_string="group = $app" --target_file="$finalphpconf"
ynh_replace_string --match_string=".*chdir = .*" --replace_string="chdir = $final_path" --target_file="$finalphpconf"
user = __APP__
group = __APP__
chdir = __FINALPATH__
listen = /var/run/php/php__PHPVERSION__-fpm-__APP__.sock
listen.owner = www-data
listen.group = www-data
pm = __PHP_PM__
pm.max_children = __PHP_MAX_CHILDREN__
pm.max_requests = 500
request_terminate_timeout = 1d
" > $phpfpm_path
# Configure FPM children
ynh_replace_string --match_string=".*pm = .*" --replace_string="pm = $php_pm" --target_file="$finalphpconf"
ynh_replace_string --match_string=".*pm.max_children = .*" --replace_string="pm.max_children = $php_max_children" --target_file="$finalphpconf"
ynh_replace_string --match_string=".*pm.max_requests = .*" --replace_string="pm.max_requests = 500" --target_file="$finalphpconf"
ynh_replace_string --match_string=".*request_terminate_timeout = .*" --replace_string="request_terminate_timeout = 1d" --target_file="$finalphpconf"
if [ "$php_pm" = "dynamic" ]
then
ynh_replace_string --match_string=".*pm.start_servers = .*" --replace_string="pm.start_servers = $php_start_servers" --target_file="$finalphpconf"
ynh_replace_string --match_string=".*pm.min_spare_servers = .*" --replace_string="pm.min_spare_servers = $php_min_spare_servers" --target_file="$finalphpconf"
ynh_replace_string --match_string=".*pm.max_spare_servers = .*" --replace_string="pm.max_spare_servers = $php_max_spare_servers" --target_file="$finalphpconf"
echo "
pm.start_servers = __PHP_START_SERVERS__
pm.min_spare_servers = __PHP_MIN_SPARE_SERVERS__
pm.max_spare_servers = __PHP_MAX_SPARE_SERVERS__
" >> $phpfpm_path
elif [ "$php_pm" = "ondemand" ]
then
ynh_replace_string --match_string=".*pm.process_idle_timeout = .*" --replace_string="pm.process_idle_timeout = 10s" --target_file="$finalphpconf"
fi
# Comment unused parameters
if [ "$php_pm" != "dynamic" ]
then
ynh_replace_string --match_string=".*\(pm.start_servers = .*\)" --replace_string=";\1" --target_file="$finalphpconf"
ynh_replace_string --match_string=".*\(pm.min_spare_servers = .*\)" --replace_string=";\1" --target_file="$finalphpconf"
ynh_replace_string --match_string=".*\(pm.max_spare_servers = .*\)" --replace_string=";\1" --target_file="$finalphpconf"
fi
if [ "$php_pm" != "ondemand" ]
then
ynh_replace_string --match_string=".*\(pm.process_idle_timeout = .*\)" --replace_string=";\1" --target_file="$finalphpconf"
echo "
pm.process_idle_timeout = 10s
" >> $phpfpm_path
fi
# Concatene the extra config.
if [ -e ../conf/extra_php-fpm.conf ]; then
cat ../conf/extra_php-fpm.conf >> "$finalphpconf"
if [ -e $YNH_APP_BASEDIR/conf/extra_php-fpm.conf ]; then
cat $YNH_APP_BASEDIR/conf/extra_php-fpm.conf >> "$phpfpm_path"
fi
fi
chown root: "$finalphpconf"
ynh_store_file_checksum --file="$finalphpconf"
local finalphpconf="$fpm_config_dir/pool.d/$app.conf"
ynh_add_config --template="$phpfpm_path" --destination="$finalphpconf"
if [ -e "../conf/php-fpm.ini" ]
if [ -e "$YNH_APP_BASEDIR/conf/php-fpm.ini" ]
then
ynh_print_warn --message="Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead."
finalphpini="$fpm_config_dir/conf.d/20-$app.ini"
ynh_backup_if_checksum_is_different "$finalphpini"
cp ../conf/php-fpm.ini "$finalphpini"
chown root: "$finalphpini"
ynh_store_file_checksum "$finalphpini"
ynh_add_config --template="$YNH_APP_BASEDIR/conf/php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini"
fi
if [ $dedicated_service -eq 1 ]
then
# Create a dedicated php-fpm.conf for the service
local globalphpconf=$fpm_config_dir/php-fpm-$app.conf
cp /etc/php/${phpversion}/fpm/php-fpm.conf $globalphpconf
ynh_replace_string --match_string="^[; ]*pid *=.*" --replace_string="pid = /run/php/php${phpversion}-fpm-$app.pid" --target_file="$globalphpconf"
ynh_replace_string --match_string="^[; ]*error_log *=.*" --replace_string="error_log = /var/log/php/fpm-php.$app.log" --target_file="$globalphpconf"
ynh_replace_string --match_string="^[; ]*syslog.ident *=.*" --replace_string="syslog.ident = php-fpm-$app" --target_file="$globalphpconf"
ynh_replace_string --match_string="^[; ]*include *=.*" --replace_string="include = $finalphpconf" --target_file="$globalphpconf"
echo "[global]
pid = /run/php/php__PHPVERSION__-fpm-__APP__.pid
error_log = /var/log/php/fpm-php.__APP__.log
syslog.ident = php-fpm-__APP__
include = __FINALPHPCONF__
" > $YNH_APP_BASEDIR/conf/php-fpm-$app.conf
ynh_add_config --template="../config/php-fpm-$app.conf" --destination="$globalphpconf"
# Create a config for a dedicated PHP-FPM service for the app
echo "[Unit]
Description=PHP $phpversion FastCGI Process Manager for $app
Description=PHP __PHPVERSION__ FastCGI Process Manager for __APP__
After=network.target
[Service]
[Service]
Type=notify
PIDFile=/run/php/php${phpversion}-fpm-$app.pid
ExecStart=/usr/sbin/php-fpm$phpversion --nodaemonize --fpm-config $globalphpconf
PIDFile=/run/php/php__PHPVERSION__-fpm-__APP__.pid
ExecStart=/usr/sbin/php-fpm__PHPVERSION__ --nodaemonize --fpm-config __GLOBALPHPCONF__
ExecReload=/bin/kill -USR2 \$MAINPID
[Install]
WantedBy=multi-user.target
" > ../conf/$fpm_service
" > $YNH_APP_BASEDIR/conf/$fpm_service
# Create this dedicated PHP-FPM service
ynh_add_systemd_config --service=$fpm_service --template=$fpm_service
@ -353,7 +337,7 @@ ynh_install_php () {
if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ]
then
ynh_die "Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION"
ynh_die --message="Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION"
fi
# Create the file if doesn't exist already
@ -627,9 +611,9 @@ ynh_install_composer () {
curl -sS https://getcomposer.org/installer \
| COMPOSER_HOME="$workdir/.composer" \
php${phpversion} -- --quiet --install-dir="$workdir" --version=$composerversion \
|| ynh_die "Unable to install Composer."
|| ynh_die --message="Unable to install Composer."
# install dependencies
ynh_composer_exec --phpversion="${phpversion}" --workdir="$workdir" --commands="install --no-dev $install_args" \
|| ynh_die "Unable to install core dependencies with Composer."
|| ynh_die --message="Unable to install core dependencies with Composer."
}

View file

@ -295,10 +295,10 @@ ynh_psql_remove_db() {
ynh_psql_test_if_first_run() {
# Make sure postgresql is indeed installed
dpkg --list | grep -q "ii postgresql-$PSQL_VERSION" || ynh_die "postgresql-$PSQL_VERSION is not installed !?"
dpkg --list | grep -q "ii postgresql-$PSQL_VERSION" || ynh_die --message="postgresql-$PSQL_VERSION is not installed !?"
# Check for some weird issue where postgresql could be installed but etc folder would not exist ...
[ -e "/etc/postgresql/$PSQL_VERSION" ] || ynh_die "It looks like postgresql was not properly configured ? /etc/postgresql/$PSQL_VERSION is missing ... Could be due to a locale issue, c.f.https://serverfault.com/questions/426989/postgresql-etc-postgresql-doesnt-exist"
[ -e "/etc/postgresql/$PSQL_VERSION" ] || ynh_die --message="It looks like postgresql was not properly configured ? /etc/postgresql/$PSQL_VERSION is missing ... Could be due to a locale issue, c.f.https://serverfault.com/questions/426989/postgresql-etc-postgresql-doesnt-exist"
# Make sure postgresql is started and enabled
# (N.B. : to check the active state, we check the cluster state because

View file

@ -3,61 +3,27 @@
# Create a dedicated systemd config
#
# usage: ynh_add_systemd_config [--service=service] [--template=template]
# usage: ynh_add_systemd_config [--service=service] [--template=template] [--others_var="list of others variables to replace"]
# | arg: -s, --service= - Service name (optionnal, $app by default)
# | arg: -t, --template= - Name of template file (optionnal, this is 'systemd' by default, meaning ./conf/systemd.service will be used as template)
# | arg: -v, --others_var= - List of others variables to replace separated by a space. For example: 'var_1 var_2 ...'
#
# This will use the template ../conf/<templatename>.service
# to generate a systemd config, by replacing the following keywords
# with global variables that should be defined before calling
# this helper :
#
# __APP__ by $app
# __FINALPATH__ by $final_path
#
# And dynamic variables (from the last example) :
# __VAR_1__ by $var_1
# __VAR_2__ by $var_2
# See the documentation of ynh_add_config for a description of the template
# format and how placeholders are replaced with actual variables.
#
# Requires YunoHost version 2.7.11 or higher.
ynh_add_systemd_config () {
# Declare an array to define the options of this helper.
local legacy_args=stv
local -A args_array=( [s]=service= [t]=template= [v]=others_var= )
local legacy_args=st
local -A args_array=( [s]=service= [t]=template=)
local service
local template
local others_var
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
local service="${service:-$app}"
local template="${template:-systemd.service}"
others_var="${others_var:-}"
finalsystemdconf="/etc/systemd/system/$service.service"
ynh_backup_if_checksum_is_different --file="$finalsystemdconf"
cp ../conf/$template "$finalsystemdconf"
ynh_add_config --template="$YNH_APP_BASEDIR/conf/$template" --destination="/etc/systemd/system/$service.service"
# To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable.
# Substitute in a nginx config file only if the variable is not empty
if [ -n "${final_path:-}" ]; then
ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalsystemdconf"
fi
if [ -n "${app:-}" ]; then
ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$finalsystemdconf"
fi
# Replace all other variables given as arguments
for var_to_replace in $others_var
do
# ${var_to_replace^^} make the content of the variable on upper-cases
# ${!var_to_replace} get the content of the variable named $var_to_replace
ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalsystemdconf"
done
ynh_store_file_checksum --file="$finalsystemdconf"
chown root: "$finalsystemdconf"
systemctl enable $service --quiet
systemctl daemon-reload
}
@ -92,7 +58,7 @@ ynh_remove_systemd_config () {
# usage: ynh_systemd_action [--service_name=service_name] [--action=action] [ [--line_match="line to match"] [--log_path=log_path] [--timeout=300] [--length=20] ]
# | arg: -n, --service_name= - Name of the service to start. Default : $app
# | arg: -a, --action= - Action to perform with systemctl. Default: start
# | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. WARNING: When using --line_match, you should always add `ynh_clean_check_starting` into your `ynh_clean_setup` at the beginning of the script. Otherwise, tail will not stop in case of failure of the script. The script will then hang forever.
# | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started.
# | arg: -p, --log_path= - Log file - Path to the log file. Default : /var/log/$app/$app.log
# | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds.
# | arg: -e, --length= - Length of the error log : Default : 20
@ -151,6 +117,7 @@ ynh_systemd_action() {
then
ynh_exec_err tail --lines=$length "$log_path"
fi
ynh_clean_check_starting
return 1
fi
@ -195,9 +162,8 @@ ynh_systemd_action() {
}
# Clean temporary process and file used by ynh_check_starting
# (usually used in ynh_clean_setup scripts)
#
# usage: ynh_clean_check_starting
# [internal]
#
# Requires YunoHost version 3.5.0 or higher.
ynh_clean_check_starting () {
@ -208,7 +174,7 @@ ynh_clean_check_starting () {
fi
if [ -n "$templog" ]
then
ynh_secure_remove "$templog" 2>&1
ynh_secure_remove --file="$templog" 2>&1
fi
}

View file

@ -17,7 +17,7 @@ ynh_user_exists() {
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
yunohost user list --output-as json | grep --quiet "\"username\": \"${username}\""
yunohost user list --output-as json --quiet | jq -e ".users.${username}" >/dev/null
}
# Retrieve a YunoHost user information
@ -39,7 +39,7 @@ ynh_user_get_info() {
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
yunohost user info "$username" --output-as plain | ynh_get_plain_key "$key"
yunohost user info "$username" --output-as json --quiet | jq -r ".$key"
}
# Get the list of YunoHost users
@ -51,8 +51,7 @@ ynh_user_get_info() {
#
# Requires YunoHost version 2.4.0 or higher.
ynh_user_list() {
yunohost user list --output-as plain --quiet \
| awk '/^##username$/{getline; print}'
yunohost user list --output-as json --quiet | jq -r ".users | keys[]"
}
# Check if a user exists on the system
@ -166,24 +165,16 @@ ynh_system_user_delete () {
# Execute a command as another user
#
# usage: ynh_exec_as --user=USER --command=COMMAND [ARG ...]
# | arg: -u, --user= - the user that will execute the command
# | arg: -n, --command= - the command to be executed
# usage: ynh_exec_as $USER COMMAND [ARG ...]
#
# Requires YunoHost version 4.1.7 or higher.
ynh_exec_as()
{
# Declare an array to define the options of this helper.
local legacy_args=uc
local -A args_array=( [u]=user= [c]=command= )
local user
local command
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
ynh_exec_as() {
local user=$1
shift 1
if [[ $user = $(whoami) ]]; then
eval "$command"
eval "$@"
else
sudo -u "$user" "$command"
sudo -u "$user" "$@"
fi
}

View file

@ -1,5 +1,7 @@
#!/bin/bash
YNH_APP_BASEDIR=$([[ "$(basename $0)" =~ ^backup|restore$ ]] && echo '../settings' || echo '..')
# Handle script crashes / failures
#
# [internal]
@ -112,12 +114,7 @@ ynh_setup_source () {
ynh_handle_getopts_args "$@"
source_id="${source_id:-app}" # If the argument is not given, source_id equals "app"
local src_file_path="$YNH_CWD/../conf/${source_id}.src"
# In case of restore script the src file is in an other path.
# So try to use the restore path if the general path point to no file.
if [ ! -e "$src_file_path" ]; then
src_file_path="$YNH_CWD/../settings/conf/${source_id}.src"
fi
local src_file_path="$YNH_APP_BASEDIR/conf/${source_id}.src"
# Load value from configuration file (see above for a small doc about this file
# format)
@ -309,10 +306,8 @@ ynh_add_config () {
ynh_handle_getopts_args "$@"
local template_path
if [ -f "../conf/$template" ]; then
template_path="../conf/$template"
elif [ -f "../settings/conf/$template" ]; then
template_path="../settings/conf/$template"
if [ -f "$YNH_APP_BASEDIR/conf/$template" ]; then
template_path="$YNH_APP_BASEDIR/conf/$template"
elif [ -f "$template" ]; then
template_path=$template
else
@ -321,7 +316,8 @@ ynh_add_config () {
ynh_backup_if_checksum_is_different --file="$destination"
cp "$template_path" "$destination"
cp -f "$template_path" "$destination"
chown root: "$destination"
ynh_replace_vars --file="$destination"
@ -386,20 +382,24 @@ ynh_replace_vars () {
# Replace others variables
# List other unique (__ __) variables in $file
local uniques_vars=( $(grep -o '__[A-Z0-9_]*__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" ))
local uniques_vars=( $(grep -oP '__[A-Z0-9_]+?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" ))
# Do the replacement
local delimit=@
for one_var in "${uniques_vars[@]}"
do
# Validate that one_var is indeed defined
# Explanation for the weird '+x' syntax: https://stackoverflow.com/a/13864829
test -n "${one_var+x}" || ynh_die --message="Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file"
# -v checks if the variable is defined, for example:
# -v FOO tests if $FOO is defined
# -v $FOO tests if ${!FOO} is defined
# More info: https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash/17538964#comment96392525_17538964
[[ -v "${one_var:-}" ]] || ynh_die --message="Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file"
# Escape delimiter in match/replace string
match_string="__${one_var^^}__"
match_string=${match_string//${delimit}/"\\${delimit}"}
replace_string="${!one_var}"
replace_string=${replace_string//\\/\\\\}
replace_string=${replace_string//${delimit}/"\\${delimit}"}
# Actually replace (sed is used instead of ynh_replace_string to avoid triggering an epic amount of debug logs)
@ -503,12 +503,7 @@ ynh_secure_remove () {
#
# [internal]
#
# example: yunohost user info tata --output-as plain | ynh_get_plain_key mail
#
# usage: ynh_get_plain_key key [subkey [subsubkey ...]]
# | ret: string - the key's value
#
# Requires YunoHost version 2.2.4 or higher.
# (Deprecated, use --output-as json and jq instead)
ynh_get_plain_key() {
local prefix="#"
local founded=0

View file

@ -8,4 +8,3 @@ cd "$YNH_CWD"
# Backup the configuration
ynh_exec_warn_less ynh_backup --src_path="/etc/yunohost/dyndns" --not_mandatory
ynh_exec_warn_less ynh_backup --src_path="/etc/cron.d/yunohost-dyndns" --not_mandatory

View file

@ -27,6 +27,29 @@ do_init_regen() {
# allow users to access /media directory
[[ -d /etc/skel/media ]] \
|| (mkdir -p /media && ln -s /media /etc/skel/media)
# Cert folders
mkdir -p /etc/yunohost/certs
chown -R root:ssl-cert /etc/yunohost/certs
chmod 750 /etc/yunohost/certs
# App folders
mkdir -p /etc/yunohost/apps
chmod 700 /etc/yunohost/apps
mkdir -p /home/yunohost.app
chmod 755 /home/yunohost.app
# Backup folders
mkdir -p /home/yunohost.backup/archives
chmod 750 /home/yunohost.backup/archives
chown root:root /home/yunohost.backup/archives # This is later changed to admin:root once admin user exists
# Empty ssowat json persistent conf
echo "{}" > '/etc/ssowat/conf.json.persistent'
chmod 644 /etc/ssowat/conf.json.persistent
chown root:root /etc/ssowat/conf.json.persistent
mkdir -p /var/cache/yunohost/repo
}
do_pre_regen() {
@ -63,11 +86,22 @@ SHELL=/bin/bash
0 7,19 * * * root : YunoHost Automatic Diagnosis; sleep \$((RANDOM\\%1200)); yunohost diagnosis run --email > /dev/null 2>/dev/null || echo "Running the automatic diagnosis failed miserably"
EOF
# If we subscribed to a dyndns domain, add the corresponding cron
# - delay between 0 and 60 secs to spread the check over a 1 min window
# - do not run the command if some process already has the lock, to avoid queuing hundreds of commands...
if ls -l /etc/yunohost/dyndns/K*.private 2>/dev/null
then
cat > $pending_dir/etc/cron.d/yunohost-dyndns << EOF
SHELL=/bin/bash
*/10 * * * * root : YunoHost DynDNS update; sleep \$((RANDOM\\%60)); test -e /var/run/moulinette_yunohost.lock || yunohost dyndns update >> /dev/null
EOF
fi
# legacy stuff to avoid yunohost reporting etckeeper as manually modified
# (this make sure that the hash is null / file is flagged as to-delete)
mkdir -p $pending_dir/etc/etckeeper
touch $pending_dir/etc/etckeeper/etckeeper.conf
# Skip ntp if inside a container (inspired from the conf of systemd-timesyncd)
mkdir -p ${pending_dir}/etc/systemd/system/ntp.service.d/
echo "
@ -75,7 +109,7 @@ EOF
ConditionCapability=CAP_SYS_TIME
ConditionVirtualization=!container
" > ${pending_dir}/etc/systemd/system/ntp.service.d/ynh-override.conf
# Make nftable conflict with yunohost-firewall
mkdir -p ${pending_dir}/etc/systemd/system/nftables.service.d/
cat > ${pending_dir}/etc/systemd/system/nftables.service.d/ynh-override.conf << EOF
@ -94,6 +128,8 @@ do_post_regen() {
# Enfore permissions #
######################
chown admin:root /home/yunohost.backup/archives
# Certs
# We do this with find because there could be a lot of them...
chown -R root:ssl-cert /etc/yunohost/certs

View file

@ -3,71 +3,85 @@
set -e
ssl_dir="/usr/share/yunohost/yunohost-config/ssl/yunoCA"
ynh_ca="/etc/yunohost/certs/yunohost.org/ca.pem"
ynh_crt="/etc/yunohost/certs/yunohost.org/crt.pem"
ynh_key="/etc/yunohost/certs/yunohost.org/key.pem"
openssl_conf="/usr/share/yunohost/templates/ssl/openssl.cnf"
regen_local_ca() {
domain="$1"
echo -e "\n# Creating local certification authority with domain=$domain\n"
# create certs and SSL directories
mkdir -p "/etc/yunohost/certs/yunohost.org"
mkdir -p "${ssl_dir}/"{ca,certs,crl,newcerts}
pushd ${ssl_dir}
# (Update the serial so that it's specific to this very instance)
# N.B. : the weird RANDFILE thing comes from:
# https://stackoverflow.com/questions/94445/using-openssl-what-does-unable-to-write-random-state-mean
RANDFILE=.rnd openssl rand -hex 19 > serial
rm -f index.txt
touch index.txt
cp /usr/share/yunohost/templates/ssl/openssl.cnf openssl.ca.cnf
sed -i "s/yunohost.org/${domain}/g" openssl.ca.cnf
openssl req -x509 \
-new \
-config openssl.ca.cnf \
-days 3650 \
-out ca/cacert.pem \
-keyout ca/cakey.pem \
-nodes \
-batch \
-subj /CN=${domain}/O=${domain%.*} 2>&1
chmod 640 ca/cacert.pem
chmod 640 ca/cakey.pem
cp ca/cacert.pem $ynh_ca
ln -sf "$ynh_ca" /etc/ssl/certs/ca-yunohost_crt.pem
update-ca-certificates
popd
}
do_init_regen() {
if [[ $EUID -ne 0 ]]; then
echo "You must be root to run this script" 1>&2
exit 1
fi
LOGFILE="/tmp/yunohost-ssl-init"
LOGFILE=/tmp/yunohost-ssl-init
echo "" > $LOGFILE
chown root:root $LOGFILE
chmod 640 $LOGFILE
echo "Initializing a local SSL certification authority ..."
echo "(logs available in $LOGFILE)"
rm -f $LOGFILE
touch $LOGFILE
# create certs and SSL directories
mkdir -p "/etc/yunohost/certs/yunohost.org"
mkdir -p "${ssl_dir}/"{ca,certs,crl,newcerts}
# initialize some files
# N.B. : the weird RANDFILE thing comes from:
# https://stackoverflow.com/questions/94445/using-openssl-what-does-unable-to-write-random-state-mean
[[ -f "${ssl_dir}/serial" ]] \
|| RANDFILE=.rnd openssl rand -hex 19 > "${ssl_dir}/serial"
[[ -f "${ssl_dir}/index.txt" ]] \
|| touch "${ssl_dir}/index.txt"
openssl_conf="/usr/share/yunohost/templates/ssl/openssl.cnf"
ynh_ca="/etc/yunohost/certs/yunohost.org/ca.pem"
ynh_crt="/etc/yunohost/certs/yunohost.org/crt.pem"
ynh_key="/etc/yunohost/certs/yunohost.org/key.pem"
# Make sure this conf exists
mkdir -p ${ssl_dir}
cp /usr/share/yunohost/templates/ssl/openssl.cnf ${ssl_dir}/openssl.ca.cnf
# create default certificates
if [[ ! -f "$ynh_ca" ]]; then
echo -e "\n# Creating the CA key (?)\n" >>$LOGFILE
openssl req -x509 \
-new \
-config "$openssl_conf" \
-days 3650 \
-out "${ssl_dir}/ca/cacert.pem" \
-keyout "${ssl_dir}/ca/cakey.pem" \
-nodes -batch >>$LOGFILE 2>&1
cp "${ssl_dir}/ca/cacert.pem" "$ynh_ca"
ln -sf "$ynh_ca" /etc/ssl/certs/ca-yunohost_crt.pem
update-ca-certificates
regen_local_ca yunohost.org >>$LOGFILE
fi
if [[ ! -f "$ynh_crt" ]]; then
echo -e "\n# Creating initial key and certificate (?)\n" >>$LOGFILE
echo -e "\n# Creating initial key and certificate \n" >>$LOGFILE
openssl req -new \
-config "$openssl_conf" \
-days 730 \
-out "${ssl_dir}/certs/yunohost_csr.pem" \
-keyout "${ssl_dir}/certs/yunohost_key.pem" \
-nodes -batch >>$LOGFILE 2>&1
-nodes -batch &>>$LOGFILE
openssl ca \
-config "$openssl_conf" \
-days 730 \
-in "${ssl_dir}/certs/yunohost_csr.pem" \
-out "${ssl_dir}/certs/yunohost_crt.pem" \
-batch >>$LOGFILE 2>&1
-batch &>>$LOGFILE
chmod 640 "${ssl_dir}/certs/yunohost_key.pem"
chmod 640 "${ssl_dir}/certs/yunohost_crt.pem"
@ -80,6 +94,8 @@ do_init_regen() {
chown -R root:ssl-cert /etc/yunohost/certs/yunohost.org/
chmod o-rwx /etc/yunohost/certs/yunohost.org/
install -D -m 644 $openssl_conf "${ssl_dir}/openssl.cnf"
}
do_pre_regen() {
@ -93,22 +109,16 @@ do_pre_regen() {
do_post_regen() {
regen_conf_files=$1
# Ensure that index.txt exists
index_txt=/usr/share/yunohost/yunohost-config/ssl/yunoCA/index.txt
[[ -f "${index_txt}" ]] || {
if [[ -f "${index_txt}.saved" ]]; then
# use saved database from 2.2
cp "${index_txt}.saved" "${index_txt}"
elif [[ -f "${index_txt}.old" ]]; then
# ... or use the state-1 database
cp "${index_txt}.old" "${index_txt}"
else
# ... or create an empty one
touch "${index_txt}"
fi
}
current_local_ca_domain=$(openssl x509 -in $ynh_ca -text | tr ',' '\n' | grep Issuer | awk '{print $4}')
main_domain=$(cat /etc/yunohost/current_host)
# TODO: regenerate certificates if conf changed?
if [[ "$current_local_ca_domain" != "$main_domain" ]]
then
regen_local_ca $main_domain
# Idk how useful this is, but this was in the previous python code (domain.main_domain())
ln -sf /etc/yunohost/certs/$domain/crt.pem /etc/ssl/certs/yunohost_crt.pem
ln -sf /etc/yunohost/certs/$domain/key.pem /etc/ssl/private/yunohost_key.pem
fi
}
FORCE=${2:-0}

View file

@ -13,7 +13,31 @@ do_init_regen() {
do_pre_regen ""
systemctl daemon-reload
systemctl restart slapd
# Drop current existing slapd data
rm -rf /var/backups/*.ldapdb
rm -rf /var/backups/slapd-*
debconf-set-selections << EOF
slapd slapd/password1 password yunohost
slapd slapd/password2 password yunohost
slapd slapd/domain string yunohost.org
slapd shared/organization string yunohost.org
slapd slapd/allow_ldap_v2 boolean false
slapd slapd/invalid_config boolean true
slapd slapd/backend select MDB
slapd slapd/move_old_database boolean true
slapd slapd/no_configuration boolean false
slapd slapd/purge_database boolean false
EOF
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure slapd -u
# Regen conf
_regenerate_slapd_conf
# Enforce permissions
@ -21,7 +45,11 @@ do_init_regen() {
chown -R openldap:openldap /etc/ldap/schema/
usermod -aG ssl-cert openldap
service slapd restart
systemctl restart slapd
# (Re-)init data according to ldap_scheme.yaml
yunohost tools shell -c "from yunohost.tools import tools_ldapinit; tools_ldapinit()"
}
_regenerate_slapd_conf() {
@ -31,7 +59,8 @@ _regenerate_slapd_conf() {
# so we use a temporary directory slapd_new.d
rm -Rf /etc/ldap/slapd_new.d
mkdir /etc/ldap/slapd_new.d
slapadd -n0 -l /etc/ldap/slapd.ldif -F /etc/ldap/slapd_new.d/ 2>&1
slapadd -n0 -l /etc/ldap/slapd.ldif -F /etc/ldap/slapd_new.d/ 2>&1 \
| grep -v "none elapsed\|Closing DB" || true
# Actual validation (-Q is for quiet, -u is for dry-run)
slaptest -Q -u -F /etc/ldap/slapd_new.d

View file

@ -2,6 +2,11 @@
set -e
do_init_regen() {
do_pre_regen ""
systemctl restart nslcd
}
do_pre_regen() {
pending_dir=$1
@ -14,7 +19,7 @@ do_post_regen() {
regen_conf_files=$1
[[ -z "$regen_conf_files" ]] \
|| service nslcd restart
|| systemctl restart nslcd
}
FORCE=${2:-0}
@ -27,6 +32,9 @@ case "$1" in
post)
do_post_regen $4
;;
init)
do_init_regen
;;
*)
echo "hook called with unknown argument \`$1'" >&2
exit 1

View file

@ -15,6 +15,31 @@ do_pre_regen() {
do_post_regen() {
regen_conf_files=$1
if [[ ! -d /var/lib/mysql/mysql ]]
then
# dpkg-reconfigure will initialize mysql (if it ain't already)
# It enabled auth_socket for root, so no need to define any root password...
# c.f. : cat /var/lib/dpkg/info/mariadb-server-10.3.postinst | grep install_db -C3
dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1
systemctl -q is-active mariadb.service \
|| systemctl start mariadb
sleep 5
echo "" | mysql && echo "Can't connect to mysql using unix_socket auth ... something went wrong during initial configuration of mysql !?"
fi
if [ ! -e /etc/yunohost/mysql ]
then
# Dummy password that's not actually used nor meaningful ...
# (because mysql is supposed to be configured to use unix_socket on new setups)
# but keeping it for legacy
# until we merge https://github.com/YunoHost/yunohost/pull/912 ...
ynh_string_random 10 > /etc/yunohost/mysql
chmod 400 /etc/yunohost/mysql
fi
# mysql is supposed to be an alias to mariadb... but in some weird case is not
# c.f. https://forum.yunohost.org/t/mysql-ne-fonctionne-pas/11661
# Playing with enable/disable allows to recreate the proper symlinks.
@ -27,44 +52,6 @@ do_post_regen() {
systemctl is-active mariadb -q || systemctl start mariadb
fi
if [ ! -f /etc/yunohost/mysql ]; then
# ensure that mysql is running
systemctl -q is-active mysql.service \
|| service mysql start
# generate and set new root password
mysql_password=$(ynh_string_random 10)
mysqladmin -s -u root -pyunohost password "$mysql_password" || {
if [ $FORCE -eq 1 ]; then
echo "It seems that you have already configured MySQL." \
"YunoHost needs to have a root access to MySQL to runs its" \
"applications, and is going to reset the MySQL root password." \
"You can find this new password in /etc/yunohost/mysql." >&2
# set new password with debconf
debconf-set-selections << EOF
$MYSQL_PKG mysql-server/root_password password $mysql_password
$MYSQL_PKG mysql-server/root_password_again password $mysql_password
EOF
# reconfigure Debian package
dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1
else
echo "It seems that you have already configured MySQL." \
"YunoHost needs to have a root access to MySQL to runs its" \
"applications, but the MySQL root password is unknown." \
"You must either pass --force to reset the password or" \
"put the current one into the file /etc/yunohost/mysql." >&2
exit 1
fi
}
# store new root password
echo "$mysql_password" | tee /etc/yunohost/mysql
chmod 400 /etc/yunohost/mysql
fi
[[ -z "$regen_conf_files" ]] \
|| service mysql restart
}

View file

@ -2,6 +2,11 @@
set -e
do_init_regen() {
do_pre_regen ""
systemctl restart unscd
}
do_pre_regen() {
pending_dir=$1
@ -14,7 +19,7 @@ do_post_regen() {
regen_conf_files=$1
[[ -z "$regen_conf_files" ]] \
|| service unscd restart
|| systemctl restart unscd
}
FORCE=${2:-0}
@ -27,6 +32,9 @@ case "$1" in
post)
do_post_regen $4
;;
init)
do_init_regen
;;
*)
echo "hook called with unknown argument \`$1'" >&2
exit 1

View file

@ -27,38 +27,47 @@ class BaseSystemDiagnoser(Diagnoser):
# Detect arch
arch = check_output("dpkg --print-architecture")
hardware = dict(meta={"test": "hardware"},
status="INFO",
data={"virt": virt, "arch": arch},
summary="diagnosis_basesystem_hardware")
hardware = dict(
meta={"test": "hardware"},
status="INFO",
data={"virt": virt, "arch": arch},
summary="diagnosis_basesystem_hardware",
)
# Also possibly the board / hardware name
if os.path.exists("/proc/device-tree/model"):
model = read_file('/proc/device-tree/model').strip().replace('\x00', '')
model = read_file("/proc/device-tree/model").strip().replace("\x00", "")
hardware["data"]["model"] = model
hardware["details"] = ["diagnosis_basesystem_hardware_model"]
elif os.path.exists("/sys/devices/virtual/dmi/id/sys_vendor"):
model = read_file("/sys/devices/virtual/dmi/id/sys_vendor").strip()
if os.path.exists("/sys/devices/virtual/dmi/id/product_name"):
model = "%s %s" % (model, read_file("/sys/devices/virtual/dmi/id/product_name").strip())
model = "%s %s" % (
model,
read_file("/sys/devices/virtual/dmi/id/product_name").strip(),
)
hardware["data"]["model"] = model
hardware["details"] = ["diagnosis_basesystem_hardware_model"]
yield hardware
# Kernel version
kernel_version = read_file('/proc/sys/kernel/osrelease').strip()
yield dict(meta={"test": "kernel"},
data={"kernel_version": kernel_version},
status="INFO",
summary="diagnosis_basesystem_kernel")
kernel_version = read_file("/proc/sys/kernel/osrelease").strip()
yield dict(
meta={"test": "kernel"},
data={"kernel_version": kernel_version},
status="INFO",
summary="diagnosis_basesystem_kernel",
)
# Debian release
debian_version = read_file("/etc/debian_version").strip()
yield dict(meta={"test": "host"},
data={"debian_version": debian_version},
status="INFO",
summary="diagnosis_basesystem_host")
yield dict(
meta={"test": "host"},
data={"debian_version": debian_version},
status="INFO",
summary="diagnosis_basesystem_host",
)
# Yunohost packages versions
# We check if versions are consistent (e.g. all 3.6 and not 3 packages with 3.6 and the other with 3.5)
@ -67,41 +76,62 @@ class BaseSystemDiagnoser(Diagnoser):
# Here, ynh_core_version is for example "3.5.4.12", so [:3] is "3.5" and we check it's the same for all packages
ynh_packages = ynh_packages_version()
ynh_core_version = ynh_packages["yunohost"]["version"]
consistent_versions = all(infos["version"][:3] == ynh_core_version[:3] for infos in ynh_packages.values())
ynh_version_details = [("diagnosis_basesystem_ynh_single_version",
{"package": package,
"version": infos["version"],
"repo": infos["repo"]}
)
for package, infos in ynh_packages.items()]
consistent_versions = all(
infos["version"][:3] == ynh_core_version[:3]
for infos in ynh_packages.values()
)
ynh_version_details = [
(
"diagnosis_basesystem_ynh_single_version",
{
"package": package,
"version": infos["version"],
"repo": infos["repo"],
},
)
for package, infos in ynh_packages.items()
]
yield dict(meta={"test": "ynh_versions"},
data={"main_version": ynh_core_version, "repo": ynh_packages["yunohost"]["repo"]},
status="INFO" if consistent_versions else "ERROR",
summary="diagnosis_basesystem_ynh_main_version" if consistent_versions else "diagnosis_basesystem_ynh_inconsistent_versions",
details=ynh_version_details)
yield dict(
meta={"test": "ynh_versions"},
data={
"main_version": ynh_core_version,
"repo": ynh_packages["yunohost"]["repo"],
},
status="INFO" if consistent_versions else "ERROR",
summary="diagnosis_basesystem_ynh_main_version"
if consistent_versions
else "diagnosis_basesystem_ynh_inconsistent_versions",
details=ynh_version_details,
)
if self.is_vulnerable_to_meltdown():
yield dict(meta={"test": "meltdown"},
status="ERROR",
summary="diagnosis_security_vulnerable_to_meltdown",
details=["diagnosis_security_vulnerable_to_meltdown_details"]
)
yield dict(
meta={"test": "meltdown"},
status="ERROR",
summary="diagnosis_security_vulnerable_to_meltdown",
details=["diagnosis_security_vulnerable_to_meltdown_details"],
)
bad_sury_packages = list(self.bad_sury_packages())
if bad_sury_packages:
cmd_to_fix = "apt install --allow-downgrades " \
+ " ".join(["%s=%s" % (package, version) for package, version in bad_sury_packages])
yield dict(meta={"test": "packages_from_sury"},
data={"cmd_to_fix": cmd_to_fix},
status="WARNING",
summary="diagnosis_package_installed_from_sury",
details=["diagnosis_package_installed_from_sury_details"])
cmd_to_fix = "apt install --allow-downgrades " + " ".join(
["%s=%s" % (package, version) for package, version in bad_sury_packages]
)
yield dict(
meta={"test": "packages_from_sury"},
data={"cmd_to_fix": cmd_to_fix},
status="WARNING",
summary="diagnosis_package_installed_from_sury",
details=["diagnosis_package_installed_from_sury_details"],
)
if self.backports_in_sources_list():
yield dict(meta={"test": "backports_in_sources_list"},
status="WARNING",
summary="diagnosis_backports_in_sources_list")
yield dict(
meta={"test": "backports_in_sources_list"},
status="WARNING",
summary="diagnosis_backports_in_sources_list",
)
def bad_sury_packages(self):
@ -112,7 +142,10 @@ class BaseSystemDiagnoser(Diagnoser):
if os.system(cmd) != 0:
continue
cmd = "LC_ALL=C apt policy %s 2>&1 | grep http -B1 | tr -d '*' | grep '+deb' | grep -v 'gbp' | head -n 1 | awk '{print $1}'" % package
cmd = (
"LC_ALL=C apt policy %s 2>&1 | grep http -B1 | tr -d '*' | grep '+deb' | grep -v 'gbp' | head -n 1 | awk '{print $1}'"
% package
)
version_to_downgrade_to = check_output(cmd)
yield (package, version_to_downgrade_to)
@ -136,8 +169,12 @@ class BaseSystemDiagnoser(Diagnoser):
cache_file = "/tmp/yunohost-meltdown-diagnosis"
dpkg_log = "/var/log/dpkg.log"
if os.path.exists(cache_file):
if not os.path.exists(dpkg_log) or os.path.getmtime(cache_file) > os.path.getmtime(dpkg_log):
self.logger_debug("Using cached results for meltdown checker, from %s" % cache_file)
if not os.path.exists(dpkg_log) or os.path.getmtime(
cache_file
) > os.path.getmtime(dpkg_log):
self.logger_debug(
"Using cached results for meltdown checker, from %s" % cache_file
)
return read_json(cache_file)[0]["VULNERABLE"]
# script taken from https://github.com/speed47/spectre-meltdown-checker
@ -149,10 +186,12 @@ class BaseSystemDiagnoser(Diagnoser):
# [{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}]
try:
self.logger_debug("Running meltdown vulnerability checker")
call = subprocess.Popen("bash %s --batch json --variant 3" %
SCRIPT_PATH, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
call = subprocess.Popen(
"bash %s --batch json --variant 3" % SCRIPT_PATH,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# TODO / FIXME : here we are ignoring error messages ...
# in particular on RPi2 and other hardware, the script complains about
@ -176,11 +215,17 @@ class BaseSystemDiagnoser(Diagnoser):
assert CVEs[0]["NAME"] == "MELTDOWN"
except Exception as e:
import traceback
traceback.print_exc()
self.logger_warning("Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s" % e)
self.logger_warning(
"Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s"
% e
)
raise Exception("Command output for failed meltdown check: '%s'" % output)
self.logger_debug("Writing results from meltdown checker to cache file, %s" % cache_file)
self.logger_debug(
"Writing results from meltdown checker to cache file, %s" % cache_file
)
write_to_json(cache_file, CVEs)
return CVEs[0]["VULNERABLE"]

View file

@ -28,9 +28,11 @@ class IPDiagnoser(Diagnoser):
can_ping_ipv6 = self.can_ping_outside(6)
if not can_ping_ipv4 and not can_ping_ipv6:
yield dict(meta={"test": "ping"},
status="ERROR",
summary="diagnosis_ip_not_connected_at_all")
yield dict(
meta={"test": "ping"},
status="ERROR",
summary="diagnosis_ip_not_connected_at_all",
)
# Not much else we can do if there's no internet at all
return
@ -49,21 +51,29 @@ class IPDiagnoser(Diagnoser):
# If it turns out that at the same time, resolvconf is bad, that's probably
# the cause of this, so we use a different message in that case
if not can_resolve_dns:
yield dict(meta={"test": "dnsresolv"},
status="ERROR",
summary="diagnosis_ip_broken_dnsresolution" if good_resolvconf else "diagnosis_ip_broken_resolvconf")
yield dict(
meta={"test": "dnsresolv"},
status="ERROR",
summary="diagnosis_ip_broken_dnsresolution"
if good_resolvconf
else "diagnosis_ip_broken_resolvconf",
)
return
# Otherwise, if the resolv conf is bad but we were able to resolve domain name,
# still warn that we're using a weird resolv conf ...
elif not good_resolvconf:
yield dict(meta={"test": "dnsresolv"},
status="WARNING",
summary="diagnosis_ip_weird_resolvconf",
details=["diagnosis_ip_weird_resolvconf_details"])
yield dict(
meta={"test": "dnsresolv"},
status="WARNING",
summary="diagnosis_ip_weird_resolvconf",
details=["diagnosis_ip_weird_resolvconf_details"],
)
else:
yield dict(meta={"test": "dnsresolv"},
status="SUCCESS",
summary="diagnosis_ip_dnsresolution_working")
yield dict(
meta={"test": "dnsresolv"},
status="SUCCESS",
summary="diagnosis_ip_dnsresolution_working",
)
# ##################################################### #
# IP DIAGNOSIS : Check that we're actually able to talk #
@ -76,8 +86,11 @@ class IPDiagnoser(Diagnoser):
network_interfaces = get_network_interfaces()
def get_local_ip(version):
local_ip = {iface: addr[version].split("/")[0]
for iface, addr in network_interfaces.items() if version in addr}
local_ip = {
iface: addr[version].split("/")[0]
for iface, addr in network_interfaces.items()
if version in addr
}
if not local_ip:
return None
elif len(local_ip):
@ -85,23 +98,34 @@ class IPDiagnoser(Diagnoser):
else:
return local_ip
yield dict(meta={"test": "ipv4"},
data={"global": ipv4, "local": get_local_ip("ipv4")},
status="SUCCESS" if ipv4 else "ERROR",
summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4",
details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None)
yield dict(
meta={"test": "ipv4"},
data={"global": ipv4, "local": get_local_ip("ipv4")},
status="SUCCESS" if ipv4 else "ERROR",
summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4",
details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None,
)
yield dict(meta={"test": "ipv6"},
data={"global": ipv6, "local": get_local_ip("ipv6")},
status="SUCCESS" if ipv6 else "WARNING",
summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6",
details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv6 else ["diagnosis_ip_no_ipv6_tip"])
yield dict(
meta={"test": "ipv6"},
data={"global": ipv6, "local": get_local_ip("ipv6")},
status="SUCCESS" if ipv6 else "WARNING",
summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6",
details=["diagnosis_ip_global", "diagnosis_ip_local"]
if ipv6
else ["diagnosis_ip_no_ipv6_tip"],
)
# TODO / FIXME : add some attempt to detect ISP (using whois ?) ?
def can_ping_outside(self, protocol=4):
assert protocol in [4, 6], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr(protocol)
assert protocol in [
4,
6,
], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr(
protocol
)
# We can know that ipv6 is not available directly if this file does not exists
if protocol == 6 and not os.path.exists("/proc/net/if_inet6"):
@ -115,26 +139,49 @@ class IPDiagnoser(Diagnoser):
# But of course IPv6 is more complex ... e.g. on internet cube there's
# no default route but a /3 which acts as a default-like route...
# e.g. 2000:/3 dev tun0 ...
return r.startswith("default") or (":" in r and re.match(r".*/[0-3]$", r.split()[0]))
return r.startswith("default") or (
":" in r and re.match(r".*/[0-3]$", r.split()[0])
)
if not any(is_default_route(r) for r in routes):
self.logger_debug("No default route for IPv%s, so assuming there's no IP address for that version" % protocol)
self.logger_debug(
"No default route for IPv%s, so assuming there's no IP address for that version"
% protocol
)
return None
# We use the resolver file as a list of well-known, trustable (ie not google ;)) IPs that we can ping
resolver_file = "/usr/share/yunohost/templates/dnsmasq/plain/resolv.dnsmasq.conf"
resolvers = [r.split(" ")[1] for r in read_file(resolver_file).split("\n") if r.startswith("nameserver")]
resolver_file = (
"/usr/share/yunohost/templates/dnsmasq/plain/resolv.dnsmasq.conf"
)
resolvers = [
r.split(" ")[1]
for r in read_file(resolver_file).split("\n")
if r.startswith("nameserver")
]
if protocol == 4:
resolvers = [r for r in resolvers if ":" not in r]
if protocol == 6:
resolvers = [r for r in resolvers if ":" in r]
assert resolvers != [], "Uhoh, need at least one IPv%s DNS resolver in %s ..." % (protocol, resolver_file)
assert (
resolvers != []
), "Uhoh, need at least one IPv%s DNS resolver in %s ..." % (
protocol,
resolver_file,
)
# So let's try to ping the first 4~5 resolvers (shuffled)
# If we succesfully ping any of them, we conclude that we are indeed connected
def ping(protocol, target):
return os.system("ping%s -c1 -W 3 %s >/dev/null 2>/dev/null" % ("" if protocol == 4 else "6", target)) == 0
return (
os.system(
"ping%s -c1 -W 3 %s >/dev/null 2>/dev/null"
% ("" if protocol == 4 else "6", target)
)
== 0
)
random.shuffle(resolvers)
return any(ping(protocol, resolver) for resolver in resolvers[:5])
@ -145,7 +192,13 @@ class IPDiagnoser(Diagnoser):
def good_resolvconf(self):
content = read_file("/etc/resolv.conf").strip().split("\n")
# Ignore comments and empty lines
content = [l.strip() for l in content if l.strip() and not l.strip().startswith("#") and not l.strip().startswith("search")]
content = [
line.strip()
for line in content
if line.strip()
and not line.strip().startswith("#")
and not line.strip().startswith("search")
]
# We should only find a "nameserver 127.0.0.1"
return len(content) == 1 and content[0].split() == ["nameserver", "127.0.0.1"]
@ -155,14 +208,21 @@ class IPDiagnoser(Diagnoser):
# but if we want to be able to diagnose DNS resolution issues independently from
# internet connectivity, we gotta rely on fixed IPs first....
assert protocol in [4, 6], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr(protocol)
assert protocol in [
4,
6,
], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr(
protocol
)
url = 'https://ip%s.yunohost.org' % ('6' if protocol == 6 else '')
url = "https://ip%s.yunohost.org" % ("6" if protocol == 6 else "")
try:
return download_text(url, timeout=30).strip()
except Exception as e:
self.logger_debug("Could not get public IPv%s : %s" % (str(protocol), str(e)))
self.logger_debug(
"Could not get public IPv%s : %s" % (str(protocol), str(e))
)
return None

View file

@ -12,7 +12,7 @@ from yunohost.utils.network import dig
from yunohost.diagnosis import Diagnoser
from yunohost.domain import domain_list, _build_dns_conf, _get_maindomain
YNH_DYNDNS_DOMAINS = ['nohost.me', 'noho.st', 'ynh.fr']
YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"]
class DNSRecordsDiagnoser(Diagnoser):
@ -29,20 +29,30 @@ class DNSRecordsDiagnoser(Diagnoser):
for domain in all_domains:
self.logger_debug("Diagnosing DNS conf for %s" % domain)
is_subdomain = domain.split(".", 1)[1] in all_domains
for report in self.check_domain(domain, domain == main_domain, is_subdomain=is_subdomain):
for report in self.check_domain(
domain, domain == main_domain, is_subdomain=is_subdomain
):
yield report
# Check if a domain buy by the user will expire soon
psl = PublicSuffixList()
domains_from_registrar = [psl.get_public_suffix(domain) for domain in all_domains]
domains_from_registrar = [domain for domain in domains_from_registrar if "." in domain]
domains_from_registrar = set(domains_from_registrar) - set(YNH_DYNDNS_DOMAINS + ["netlib.re"])
domains_from_registrar = [
psl.get_public_suffix(domain) for domain in all_domains
]
domains_from_registrar = [
domain for domain in domains_from_registrar if "." in domain
]
domains_from_registrar = set(domains_from_registrar) - set(
YNH_DYNDNS_DOMAINS + ["netlib.re"]
)
for report in self.check_expiration_date(domains_from_registrar):
yield report
def check_domain(self, domain, is_main_domain, is_subdomain):
expected_configuration = _build_dns_conf(domain, include_empty_AAAA_if_no_ipv6=True)
expected_configuration = _build_dns_conf(
domain, include_empty_AAAA_if_no_ipv6=True
)
categories = ["basic", "mail", "xmpp", "extra"]
# For subdomains, we only diagnosis A and AAAA records
@ -92,14 +102,19 @@ class DNSRecordsDiagnoser(Diagnoser):
status = "SUCCESS"
summary = "diagnosis_dns_good_conf"
output = dict(meta={"domain": domain, "category": category},
data=results,
status=status,
summary=summary)
output = dict(
meta={"domain": domain, "category": category},
data=results,
status=status,
summary=summary,
)
if discrepancies:
# For ynh-managed domains (nohost.me etc...), tell people to try to "yunohost dyndns update --force"
if any(domain.endswith(ynh_dyndns_domain) for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS):
if any(
domain.endswith(ynh_dyndns_domain)
for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS
):
output["details"] = ["diagnosis_dns_try_dyndns_update_force"]
# Otherwise point to the documentation
else:
@ -134,11 +149,17 @@ class DNSRecordsDiagnoser(Diagnoser):
# some DNS registrar sometime split it into several pieces like this:
# "p=foo" "bar" (with a space and quotes in the middle)...
expected = set(r["value"].strip(';" ').replace(";", " ").split())
current = set(r["current"].replace('" "', '').strip(';" ').replace(";", " ").split())
current = set(
r["current"].replace('" "', "").strip(';" ').replace(";", " ").split()
)
# For SPF, ignore parts starting by ip4: or ip6:
if r["name"] == "@":
current = {part for part in current if not part.startswith("ip4:") and not part.startswith("ip6:")}
current = {
part
for part in current
if not part.startswith("ip4:") and not part.startswith("ip6:")
}
return expected == current
elif r["type"] == "MX":
# For MX, we want to ignore the priority
@ -153,12 +174,7 @@ class DNSRecordsDiagnoser(Diagnoser):
Alert if expiration date of a domain is soon
"""
details = {
"not_found": [],
"error": [],
"warning": [],
"success": []
}
details = {"not_found": [], "error": [], "warning": [], "success": []}
for domain in domains:
expire_date = self.get_domain_expiration(domain)
@ -167,9 +183,12 @@ class DNSRecordsDiagnoser(Diagnoser):
status_ns, _ = dig(domain, "NS", resolvers="force_external")
status_a, _ = dig(domain, "A", resolvers="force_external")
if "ok" not in [status_ns, status_a]:
details["not_found"].append((
"diagnosis_domain_%s_details" % (expire_date),
{"domain": domain}))
details["not_found"].append(
(
"diagnosis_domain_%s_details" % (expire_date),
{"domain": domain},
)
)
else:
self.logger_debug("Dyndns domain: %s" % (domain))
continue
@ -185,7 +204,7 @@ class DNSRecordsDiagnoser(Diagnoser):
args = {
"domain": domain,
"days": expire_in.days - 1,
"expire_date": str(expire_date)
"expire_date": str(expire_date),
}
details[alert_type].append(("diagnosis_domain_expires_in", args))
@ -198,11 +217,15 @@ class DNSRecordsDiagnoser(Diagnoser):
# Allow to ignore specifically a single domain
if len(details[alert_type]) == 1:
meta["domain"] = details[alert_type][0][1]["domain"]
yield dict(meta=meta,
data={},
status=alert_type.upper() if alert_type != "not_found" else "WARNING",
summary="diagnosis_domain_expiration_" + alert_type,
details=details[alert_type])
yield dict(
meta=meta,
data={},
status=alert_type.upper()
if alert_type != "not_found"
else "WARNING",
summary="diagnosis_domain_expiration_" + alert_type,
details=details[alert_type],
)
def get_domain_expiration(self, domain):
"""
@ -212,25 +235,28 @@ class DNSRecordsDiagnoser(Diagnoser):
out = check_output(command).split("\n")
# Reduce output to determine if whois answer is equivalent to NOT FOUND
filtered_out = [line for line in out
if re.search(r'^[a-zA-Z0-9 ]{4,25}:', line, re.IGNORECASE) and
not re.match(r'>>> Last update of whois', line, re.IGNORECASE) and
not re.match(r'^NOTICE:', line, re.IGNORECASE) and
not re.match(r'^%%', line, re.IGNORECASE) and
not re.match(r'"https?:"', line, re.IGNORECASE)]
filtered_out = [
line
for line in out
if re.search(r"^[a-zA-Z0-9 ]{4,25}:", line, re.IGNORECASE)
and not re.match(r">>> Last update of whois", line, re.IGNORECASE)
and not re.match(r"^NOTICE:", line, re.IGNORECASE)
and not re.match(r"^%%", line, re.IGNORECASE)
and not re.match(r'"https?:"', line, re.IGNORECASE)
]
# If there is less than 7 lines, it's NOT FOUND response
if len(filtered_out) <= 6:
return "not_found"
for line in out:
match = re.search(r'Expir.+(\d{4}-\d{2}-\d{2})', line, re.IGNORECASE)
match = re.search(r"Expir.+(\d{4}-\d{2}-\d{2})", line, re.IGNORECASE)
if match is not None:
return datetime.strptime(match.group(1), '%Y-%m-%d')
return datetime.strptime(match.group(1), "%Y-%m-%d")
match = re.search(r'Expir.+(\d{2}-\w{3}-\d{4})', line, re.IGNORECASE)
match = re.search(r"Expir.+(\d{2}-\w{3}-\d{4})", line, re.IGNORECASE)
if match is not None:
return datetime.strptime(match.group(1), '%d-%b-%Y')
return datetime.strptime(match.group(1), "%d-%b-%Y")
return "expiration_not_found"

View file

@ -42,16 +42,18 @@ class PortsDiagnoser(Diagnoser):
results = {}
for ipversion in ipversions:
try:
r = Diagnoser.remote_diagnosis('check-ports',
data={'ports': ports.keys()},
ipversion=ipversion)
r = Diagnoser.remote_diagnosis(
"check-ports", data={"ports": ports.keys()}, ipversion=ipversion
)
results[ipversion] = r["ports"]
except Exception as e:
yield dict(meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion},
data={"error": str(e)},
status="WARNING",
summary="diagnosis_ports_could_not_diagnose",
details=["diagnosis_ports_could_not_diagnose_details"])
yield dict(
meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion},
data={"error": str(e)},
status="WARNING",
summary="diagnosis_ports_could_not_diagnose",
details=["diagnosis_ports_could_not_diagnose_details"],
)
continue
ipversions = results.keys()
@ -64,18 +66,27 @@ class PortsDiagnoser(Diagnoser):
# If both IPv4 and IPv6 (if applicable) are good
if all(results[ipversion].get(port) is True for ipversion in ipversions):
yield dict(meta={"port": port},
data={"service": service, "category": category},
status="SUCCESS",
summary="diagnosis_ports_ok",
details=["diagnosis_ports_needed_by"])
yield dict(
meta={"port": port},
data={"service": service, "category": category},
status="SUCCESS",
summary="diagnosis_ports_ok",
details=["diagnosis_ports_needed_by"],
)
# If both IPv4 and IPv6 (if applicable) are failed
elif all(results[ipversion].get(port) is not True for ipversion in ipversions):
yield dict(meta={"port": port},
data={"service": service, "category": category},
status="ERROR",
summary="diagnosis_ports_unreachable",
details=["diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip"])
elif all(
results[ipversion].get(port) is not True for ipversion in ipversions
):
yield dict(
meta={"port": port},
data={"service": service, "category": category},
status="ERROR",
summary="diagnosis_ports_unreachable",
details=[
"diagnosis_ports_needed_by",
"diagnosis_ports_forwarding_tip",
],
)
# If only IPv4 is failed or only IPv6 is failed (if applicable)
else:
passed, failed = (4, 6) if results[4].get(port) is True else (6, 4)
@ -87,29 +98,54 @@ class PortsDiagnoser(Diagnoser):
# If any AAAA record is set, IPv6 is important...
def ipv6_is_important():
dnsrecords = Diagnoser.get_cached_report("dnsrecords") or {}
return any(record["data"].get("AAAA:@") in ["OK", "WRONG"] for record in dnsrecords.get("items", []))
return any(
record["data"].get("AAAA:@") in ["OK", "WRONG"]
for record in dnsrecords.get("items", [])
)
if failed == 4 or ipv6_is_important():
yield dict(meta={"port": port},
data={"service": service, "category": category, "passed": passed, "failed": failed},
status="ERROR",
summary="diagnosis_ports_partially_unreachable",
details=["diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip"])
yield dict(
meta={"port": port},
data={
"service": service,
"category": category,
"passed": passed,
"failed": failed,
},
status="ERROR",
summary="diagnosis_ports_partially_unreachable",
details=[
"diagnosis_ports_needed_by",
"diagnosis_ports_forwarding_tip",
],
)
# So otherwise we report a success
# And in addition we report an info about the failure in IPv6
# *with a different meta* (important to avoid conflicts when
# fetching the other info...)
else:
yield dict(meta={"port": port},
data={"service": service, "category": category},
status="SUCCESS",
summary="diagnosis_ports_ok",
details=["diagnosis_ports_needed_by"])
yield dict(meta={"test": "ipv6", "port": port},
data={"service": service, "category": category, "passed": passed, "failed": failed},
status="INFO",
summary="diagnosis_ports_partially_unreachable",
details=["diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip"])
yield dict(
meta={"port": port},
data={"service": service, "category": category},
status="SUCCESS",
summary="diagnosis_ports_ok",
details=["diagnosis_ports_needed_by"],
)
yield dict(
meta={"test": "ipv6", "port": port},
data={
"service": service,
"category": category,
"passed": passed,
"failed": failed,
},
status="INFO",
summary="diagnosis_ports_partially_unreachable",
details=[
"diagnosis_ports_needed_by",
"diagnosis_ports_forwarding_tip",
],
)
def main(args, env, loggers):

View file

@ -28,14 +28,16 @@ class WebDiagnoser(Diagnoser):
# probably because nginx conf manually modified...
nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain
if ".well-known/ynh-diagnosis/" not in read_file(nginx_conf):
yield dict(meta={"domain": domain},
status="WARNING",
summary="diagnosis_http_nginx_conf_not_up_to_date",
details=["diagnosis_http_nginx_conf_not_up_to_date_details"])
yield dict(
meta={"domain": domain},
status="WARNING",
summary="diagnosis_http_nginx_conf_not_up_to_date",
details=["diagnosis_http_nginx_conf_not_up_to_date_details"],
)
else:
domains_to_check.append(domain)
self.nonce = ''.join(random.choice("0123456789abcedf") for i in range(16))
self.nonce = "".join(random.choice("0123456789abcedf") for i in range(16))
os.system("rm -rf /tmp/.well-known/ynh-diagnosis/")
os.system("mkdir -p /tmp/.well-known/ynh-diagnosis/")
os.system("touch /tmp/.well-known/ynh-diagnosis/%s" % self.nonce)
@ -74,11 +76,13 @@ class WebDiagnoser(Diagnoser):
try:
requests.head("http://" + global_ipv4, timeout=5)
except requests.exceptions.Timeout:
yield dict(meta={"test": "hairpinning"},
status="WARNING",
summary="diagnosis_http_hairpinning_issue",
details=["diagnosis_http_hairpinning_issue_details"])
except:
yield dict(
meta={"test": "hairpinning"},
status="WARNING",
summary="diagnosis_http_hairpinning_issue",
details=["diagnosis_http_hairpinning_issue_details"],
)
except Exception:
# Well I dunno what to do if that's another exception
# type... That'll most probably *not* be an hairpinning
# issue but something else super weird ...
@ -89,17 +93,20 @@ class WebDiagnoser(Diagnoser):
results = {}
for ipversion in ipversions:
try:
r = Diagnoser.remote_diagnosis('check-http',
data={'domains': domains,
"nonce": self.nonce},
ipversion=ipversion)
r = Diagnoser.remote_diagnosis(
"check-http",
data={"domains": domains, "nonce": self.nonce},
ipversion=ipversion,
)
results[ipversion] = r["http"]
except Exception as e:
yield dict(meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion},
data={"error": str(e)},
status="WARNING",
summary="diagnosis_http_could_not_diagnose",
details=["diagnosis_http_could_not_diagnose_details"])
yield dict(
meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion},
data={"error": str(e)},
status="WARNING",
summary="diagnosis_http_could_not_diagnose",
details=["diagnosis_http_could_not_diagnose_details"],
)
continue
ipversions = results.keys()
@ -109,22 +116,32 @@ class WebDiagnoser(Diagnoser):
for domain in domains:
# If both IPv4 and IPv6 (if applicable) are good
if all(results[ipversion][domain]["status"] == "ok" for ipversion in ipversions):
if all(
results[ipversion][domain]["status"] == "ok" for ipversion in ipversions
):
if 4 in ipversions:
self.do_hairpinning_test = True
yield dict(meta={"domain": domain},
status="SUCCESS",
summary="diagnosis_http_ok")
yield dict(
meta={"domain": domain},
status="SUCCESS",
summary="diagnosis_http_ok",
)
# If both IPv4 and IPv6 (if applicable) are failed
elif all(results[ipversion][domain]["status"] != "ok" for ipversion in ipversions):
elif all(
results[ipversion][domain]["status"] != "ok" for ipversion in ipversions
):
detail = results[4 if 4 in ipversions else 6][domain]["status"]
yield dict(meta={"domain": domain},
status="ERROR",
summary="diagnosis_http_unreachable",
details=[detail.replace("error_http_check", "diagnosis_http")])
yield dict(
meta={"domain": domain},
status="ERROR",
summary="diagnosis_http_unreachable",
details=[detail.replace("error_http_check", "diagnosis_http")],
)
# If only IPv4 is failed or only IPv6 is failed (if applicable)
else:
passed, failed = (4, 6) if results[4][domain]["status"] == "ok" else (6, 4)
passed, failed = (
(4, 6) if results[4][domain]["status"] == "ok" else (6, 4)
)
detail = results[failed][domain]["status"]
# Failing in ipv4 is critical.
@ -132,17 +149,24 @@ class WebDiagnoser(Diagnoser):
# It's an acceptable situation and we shall not report an
# error
def ipv6_is_important_for_this_domain():
dnsrecords = Diagnoser.get_cached_report("dnsrecords", item={"domain": domain, "category": "basic"}) or {}
dnsrecords = (
Diagnoser.get_cached_report(
"dnsrecords", item={"domain": domain, "category": "basic"}
)
or {}
)
AAAA_status = dnsrecords.get("data", {}).get("AAAA:@")
return AAAA_status in ["OK", "WRONG"]
if failed == 4 or ipv6_is_important_for_this_domain():
yield dict(meta={"domain": domain},
data={"passed": passed, "failed": failed},
status="ERROR",
summary="diagnosis_http_partially_unreachable",
details=[detail.replace("error_http_check", "diagnosis_http")])
yield dict(
meta={"domain": domain},
data={"passed": passed, "failed": failed},
status="ERROR",
summary="diagnosis_http_partially_unreachable",
details=[detail.replace("error_http_check", "diagnosis_http")],
)
# So otherwise we report a success (note that this info is
# later used to know that ACME challenge is doable)
#
@ -151,14 +175,18 @@ class WebDiagnoser(Diagnoser):
# fetching the other info...)
else:
self.do_hairpinning_test = True
yield dict(meta={"domain": domain},
status="SUCCESS",
summary="diagnosis_http_ok")
yield dict(meta={"test": "ipv6", "domain": domain},
data={"passed": passed, "failed": failed},
status="INFO",
summary="diagnosis_http_partially_unreachable",
details=[detail.replace("error_http_check", "diagnosis_http")])
yield dict(
meta={"domain": domain},
status="SUCCESS",
summary="diagnosis_http_ok",
)
yield dict(
meta={"test": "ipv6", "domain": domain},
data={"passed": passed, "failed": failed},
status="INFO",
summary="diagnosis_http_partially_unreachable",
details=[detail.replace("error_http_check", "diagnosis_http")],
)
def main(args, env, loggers):

View file

@ -34,8 +34,13 @@ class MailDiagnoser(Diagnoser):
# TODO Validate DKIM and dmarc ?
# TODO check that the recent mail logs are not filled with thousand of email sending (unusual number of mail sent)
# TODO check for unusual failed sending attempt being refused in the logs ?
checks = ["check_outgoing_port_25", "check_ehlo", "check_fcrdns",
"check_blacklist", "check_queue"]
checks = [
"check_outgoing_port_25",
"check_ehlo",
"check_fcrdns",
"check_blacklist",
"check_queue",
]
for check in checks:
self.logger_debug("Running " + check)
reports = list(getattr(self, check)())
@ -43,9 +48,11 @@ class MailDiagnoser(Diagnoser):
yield report
if not reports:
name = check[6:]
yield dict(meta={"test": "mail_" + name},
status="SUCCESS",
summary="diagnosis_mail_" + name + "_ok")
yield dict(
meta={"test": "mail_" + name},
status="SUCCESS",
summary="diagnosis_mail_" + name + "_ok",
)
def check_outgoing_port_25(self):
"""
@ -54,14 +61,20 @@ class MailDiagnoser(Diagnoser):
"""
for ipversion in self.ipversions:
cmd = '/bin/nc -{ipversion} -z -w2 yunohost.org 25'.format(ipversion=ipversion)
cmd = "/bin/nc -{ipversion} -z -w2 yunohost.org 25".format(
ipversion=ipversion
)
if os.system(cmd) != 0:
yield dict(meta={"test": "outgoing_port_25", "ipversion": ipversion},
data={},
status="ERROR",
summary="diagnosis_mail_outgoing_port_25_blocked",
details=["diagnosis_mail_outgoing_port_25_blocked_details",
"diagnosis_mail_outgoing_port_25_blocked_relay_vpn"])
yield dict(
meta={"test": "outgoing_port_25", "ipversion": ipversion},
data={},
status="ERROR",
summary="diagnosis_mail_outgoing_port_25_blocked",
details=[
"diagnosis_mail_outgoing_port_25_blocked_details",
"diagnosis_mail_outgoing_port_25_blocked_relay_vpn",
],
)
def check_ehlo(self):
"""
@ -71,31 +84,40 @@ class MailDiagnoser(Diagnoser):
for ipversion in self.ipversions:
try:
r = Diagnoser.remote_diagnosis('check-smtp',
data={},
ipversion=ipversion)
r = Diagnoser.remote_diagnosis(
"check-smtp", data={}, ipversion=ipversion
)
except Exception as e:
yield dict(meta={"test": "mail_ehlo", "reason": "remote_server_failed",
"ipversion": ipversion},
data={"error": str(e)},
status="WARNING",
summary="diagnosis_mail_ehlo_could_not_diagnose",
details=["diagnosis_mail_ehlo_could_not_diagnose_details"])
yield dict(
meta={
"test": "mail_ehlo",
"reason": "remote_server_failed",
"ipversion": ipversion,
},
data={"error": str(e)},
status="WARNING",
summary="diagnosis_mail_ehlo_could_not_diagnose",
details=["diagnosis_mail_ehlo_could_not_diagnose_details"],
)
continue
if r["status"] != "ok":
summary = r["status"].replace("error_smtp_", "diagnosis_mail_ehlo_")
yield dict(meta={"test": "mail_ehlo", "ipversion": ipversion},
data={},
status="ERROR",
summary=summary,
details=[summary + "_details"])
yield dict(
meta={"test": "mail_ehlo", "ipversion": ipversion},
data={},
status="ERROR",
summary=summary,
details=[summary + "_details"],
)
elif r["helo"] != self.ehlo_domain:
yield dict(meta={"test": "mail_ehlo", "ipversion": ipversion},
data={"wrong_ehlo": r["helo"], "right_ehlo": self.ehlo_domain},
status="ERROR",
summary="diagnosis_mail_ehlo_wrong",
details=["diagnosis_mail_ehlo_wrong_details"])
yield dict(
meta={"test": "mail_ehlo", "ipversion": ipversion},
data={"wrong_ehlo": r["helo"], "right_ehlo": self.ehlo_domain},
status="ERROR",
summary="diagnosis_mail_ehlo_wrong",
details=["diagnosis_mail_ehlo_wrong_details"],
)
def check_fcrdns(self):
"""
@ -107,43 +129,55 @@ class MailDiagnoser(Diagnoser):
for ip in self.ips:
if ":" in ip:
ipversion = 6
details = ["diagnosis_mail_fcrdns_nok_details",
"diagnosis_mail_fcrdns_nok_alternatives_6"]
details = [
"diagnosis_mail_fcrdns_nok_details",
"diagnosis_mail_fcrdns_nok_alternatives_6",
]
else:
ipversion = 4
details = ["diagnosis_mail_fcrdns_nok_details",
"diagnosis_mail_fcrdns_nok_alternatives_4"]
details = [
"diagnosis_mail_fcrdns_nok_details",
"diagnosis_mail_fcrdns_nok_alternatives_4",
]
rev = dns.reversename.from_address(ip)
subdomain = str(rev.split(3)[0])
query = subdomain
if ipversion == 4:
query += '.in-addr.arpa'
query += ".in-addr.arpa"
else:
query += '.ip6.arpa'
query += ".ip6.arpa"
# Do the DNS Query
status, value = dig(query, 'PTR', resolvers="force_external")
status, value = dig(query, "PTR", resolvers="force_external")
if status == "nok":
yield dict(meta={"test": "mail_fcrdns", "ipversion": ipversion},
data={"ip": ip, "ehlo_domain": self.ehlo_domain},
status="ERROR",
summary="diagnosis_mail_fcrdns_dns_missing",
details=details)
yield dict(
meta={"test": "mail_fcrdns", "ipversion": ipversion},
data={"ip": ip, "ehlo_domain": self.ehlo_domain},
status="ERROR",
summary="diagnosis_mail_fcrdns_dns_missing",
details=details,
)
continue
rdns_domain = ''
rdns_domain = ""
if len(value) > 0:
rdns_domain = value[0][:-1] if value[0].endswith('.') else value[0]
rdns_domain = value[0][:-1] if value[0].endswith(".") else value[0]
if rdns_domain != self.ehlo_domain:
details = ["diagnosis_mail_fcrdns_different_from_ehlo_domain_details"] + details
yield dict(meta={"test": "mail_fcrdns", "ipversion": ipversion},
data={"ip": ip,
"ehlo_domain": self.ehlo_domain,
"rdns_domain": rdns_domain},
status="ERROR",
summary="diagnosis_mail_fcrdns_different_from_ehlo_domain",
details=details)
details = [
"diagnosis_mail_fcrdns_different_from_ehlo_domain_details"
] + details
yield dict(
meta={"test": "mail_fcrdns", "ipversion": ipversion},
data={
"ip": ip,
"ehlo_domain": self.ehlo_domain,
"rdns_domain": rdns_domain,
},
status="ERROR",
summary="diagnosis_mail_fcrdns_different_from_ehlo_domain",
details=details,
)
def check_blacklist(self):
"""
@ -156,9 +190,9 @@ class MailDiagnoser(Diagnoser):
for blacklist in dns_blacklists:
item_type = "domain"
if ":" in item:
item_type = 'ipv6'
elif re.match(r'^\d+\.\d+\.\d+\.\d+$', item):
item_type = 'ipv4'
item_type = "ipv6"
elif re.match(r"^\d+\.\d+\.\d+\.\d+$", item):
item_type = "ipv4"
if not blacklist[item_type]:
continue
@ -168,58 +202,73 @@ class MailDiagnoser(Diagnoser):
if item_type != "domain":
rev = dns.reversename.from_address(item)
subdomain = str(rev.split(3)[0])
query = subdomain + '.' + blacklist['dns_server']
query = subdomain + "." + blacklist["dns_server"]
# Do the DNS Query
status, _ = dig(query, 'A')
if status != 'ok':
status, _ = dig(query, "A")
if status != "ok":
continue
# Try to get the reason
details = []
status, answers = dig(query, 'TXT')
status, answers = dig(query, "TXT")
reason = "-"
if status == 'ok':
reason = ', '.join(answers)
if status == "ok":
reason = ", ".join(answers)
details.append("diagnosis_mail_blacklist_reason")
details.append("diagnosis_mail_blacklist_website")
yield dict(meta={"test": "mail_blacklist", "item": item,
"blacklist": blacklist["dns_server"]},
data={'blacklist_name': blacklist['name'],
'blacklist_website': blacklist['website'],
'reason': reason},
status="ERROR",
summary='diagnosis_mail_blacklist_listed_by',
details=details)
yield dict(
meta={
"test": "mail_blacklist",
"item": item,
"blacklist": blacklist["dns_server"],
},
data={
"blacklist_name": blacklist["name"],
"blacklist_website": blacklist["website"],
"reason": reason,
},
status="ERROR",
summary="diagnosis_mail_blacklist_listed_by",
details=details,
)
def check_queue(self):
"""
Check mail queue is not filled with hundreds of email pending
"""
command = 'postqueue -p | grep -v "Mail queue is empty" | grep -c "^[A-Z0-9]" || true'
command = (
'postqueue -p | grep -v "Mail queue is empty" | grep -c "^[A-Z0-9]" || true'
)
try:
output = check_output(command)
pending_emails = int(output)
except (ValueError, CalledProcessError) as e:
yield dict(meta={"test": "mail_queue"},
data={"error": str(e)},
status="ERROR",
summary="diagnosis_mail_queue_unavailable",
details="diagnosis_mail_queue_unavailable_details")
yield dict(
meta={"test": "mail_queue"},
data={"error": str(e)},
status="ERROR",
summary="diagnosis_mail_queue_unavailable",
details="diagnosis_mail_queue_unavailable_details",
)
else:
if pending_emails > 100:
yield dict(meta={"test": "mail_queue"},
data={'nb_pending': pending_emails},
status="WARNING",
summary="diagnosis_mail_queue_too_big")
yield dict(
meta={"test": "mail_queue"},
data={"nb_pending": pending_emails},
status="WARNING",
summary="diagnosis_mail_queue_too_big",
)
else:
yield dict(meta={"test": "mail_queue"},
data={'nb_pending': pending_emails},
status="SUCCESS",
summary="diagnosis_mail_queue_ok")
yield dict(
meta={"test": "mail_queue"},
data={"nb_pending": pending_emails},
status="SUCCESS",
summary="diagnosis_mail_queue_ok",
)
def get_ips_checked(self):
outgoing_ipversions = []

View file

@ -18,8 +18,13 @@ class ServicesDiagnoser(Diagnoser):
for service, result in sorted(all_result.items()):
item = dict(meta={"service": service},
data={"status": result["status"], "configuration": result["configuration"]})
item = dict(
meta={"service": service},
data={
"status": result["status"],
"configuration": result["configuration"],
},
)
if result["status"] != "running":
item["status"] = "ERROR" if result["status"] != "unknown" else "WARNING"

View file

@ -17,7 +17,7 @@ class SystemResourcesDiagnoser(Diagnoser):
def run(self):
MB = 1024**2
MB = 1024 ** 2
GB = MB * 1024
#
@ -26,10 +26,14 @@ class SystemResourcesDiagnoser(Diagnoser):
ram = psutil.virtual_memory()
ram_available_percent = 100 * ram.available / ram.total
item = dict(meta={"test": "ram"},
data={"total": human_size(ram.total),
"available": human_size(ram.available),
"available_percent": round_(ram_available_percent)})
item = dict(
meta={"test": "ram"},
data={
"total": human_size(ram.total),
"available": human_size(ram.available),
"available_percent": round_(ram_available_percent),
},
)
if ram.available < 100 * MB or ram_available_percent < 5:
item["status"] = "ERROR"
@ -47,8 +51,10 @@ class SystemResourcesDiagnoser(Diagnoser):
#
swap = psutil.swap_memory()
item = dict(meta={"test": "swap"},
data={"total": human_size(swap.total), "recommended": "512 MiB"})
item = dict(
meta={"test": "swap"},
data={"total": human_size(swap.total), "recommended": "512 MiB"},
)
if swap.total <= 1 * MB:
item["status"] = "INFO"
item["summary"] = "diagnosis_swap_none"
@ -70,7 +76,9 @@ class SystemResourcesDiagnoser(Diagnoser):
disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint)
# Ignore /dev/loop stuff which are ~virtual partitions ? (e.g. mounted to /snap/)
disk_partitions = [d for d in disk_partitions if not d.device.startswith("/dev/loop")]
disk_partitions = [
d for d in disk_partitions if not d.device.startswith("/dev/loop")
]
for disk_partition in disk_partitions:
device = disk_partition.device
@ -79,22 +87,30 @@ class SystemResourcesDiagnoser(Diagnoser):
usage = psutil.disk_usage(mountpoint)
free_percent = 100 - round_(usage.percent)
item = dict(meta={"test": "diskusage", "mountpoint": mountpoint},
data={"device": device,
# N.B.: we do not use usage.total because we want
# to take into account the 5% security margin
# correctly (c.f. the doc of psutil ...)
"total": human_size(usage.used + usage.free),
"free": human_size(usage.free),
"free_percent": free_percent})
item = dict(
meta={"test": "diskusage", "mountpoint": mountpoint},
data={
"device": device,
# N.B.: we do not use usage.total because we want
# to take into account the 5% security margin
# correctly (c.f. the doc of psutil ...)
"total": human_size(usage.used + usage.free),
"free": human_size(usage.free),
"free_percent": free_percent,
},
)
# We have an additional absolute constrain on / and /var because
# system partitions are critical, having them full may prevent
# upgrades etc...
if free_percent < 2.5 or (mountpoint in ["/", "/var"] and usage.free < 1 * GB):
if free_percent < 2.5 or (
mountpoint in ["/", "/var"] and usage.free < 1 * GB
):
item["status"] = "ERROR"
item["summary"] = "diagnosis_diskusage_verylow"
elif free_percent < 5 or (mountpoint in ["/", "/var"] and usage.free < 2 * GB):
elif free_percent < 5 or (
mountpoint in ["/", "/var"] and usage.free < 2 * GB
):
item["status"] = "WARNING"
item["summary"] = "diagnosis_diskusage_low"
else:
@ -110,18 +126,26 @@ class SystemResourcesDiagnoser(Diagnoser):
# which later causes issue when it gets full...
#
main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ['/', '/var']]
main_space = sum([psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions])
main_disk_partitions = [
d for d in disk_partitions if d.mountpoint in ["/", "/var"]
]
main_space = sum(
[psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions]
)
if main_space < 10 * GB:
yield dict(meta={"test": "rootfstotalspace"},
data={"space": human_size(main_space)},
status="ERROR",
summary="diagnosis_rootfstotalspace_critical")
yield dict(
meta={"test": "rootfstotalspace"},
data={"space": human_size(main_space)},
status="ERROR",
summary="diagnosis_rootfstotalspace_critical",
)
if main_space < 14 * GB:
yield dict(meta={"test": "rootfstotalspace"},
data={"space": human_size(main_space)},
status="WARNING",
summary="diagnosis_rootfstotalspace_warning")
yield dict(
meta={"test": "rootfstotalspace"},
data={"space": human_size(main_space)},
status="WARNING",
summary="diagnosis_rootfstotalspace_warning",
)
#
# Recent kills by oom_reaper
@ -129,12 +153,16 @@ class SystemResourcesDiagnoser(Diagnoser):
kills_count = self.recent_kills_by_oom_reaper()
if kills_count:
kills_summary = "\n".join(["%s (x%s)" % (proc, count) for proc, count in kills_count])
kills_summary = "\n".join(
["%s (x%s)" % (proc, count) for proc, count in kills_count]
)
yield dict(meta={"test": "oom_reaper"},
status="WARNING",
summary="diagnosis_processes_killed_by_oom_reaper",
data={"kills_summary": kills_summary})
yield dict(
meta={"test": "oom_reaper"},
status="WARNING",
summary="diagnosis_processes_killed_by_oom_reaper",
data={"kills_summary": kills_summary},
)
def recent_kills_by_oom_reaper(self):
if not os.path.exists("/var/log/kern.log"):
@ -152,7 +180,7 @@ class SystemResourcesDiagnoser(Diagnoser):
# Lines look like :
# Aug 25 18:48:21 yolo kernel: [ 9623.613667] oom_reaper: reaped process 11509 (uwsgi), now anon-rss:0kB, file-rss:0kB, shmem-rss:328kB
date_str = str(now.year) + " " + " ".join(line.split()[:3])
date = datetime.datetime.strptime(date_str, '%Y %b %d %H:%M:%S')
date = datetime.datetime.strptime(date_str, "%Y %b %d %H:%M:%S")
diff = now - date
if diff.days >= 1:
break
@ -160,7 +188,9 @@ class SystemResourcesDiagnoser(Diagnoser):
yield process_killed
processes = list(analyzed_kern_log())
kills_count = [(p, len([p_ for p_ in processes if p_ == p])) for p in set(processes)]
kills_count = [
(p, len([p_ for p_ in processes if p_ == p])) for p in set(processes)
]
kills_count = sorted(kills_count, key=lambda p: p[1], reverse=True)
return kills_count
@ -168,11 +198,11 @@ class SystemResourcesDiagnoser(Diagnoser):
def human_size(bytes_):
# Adapted from https://stackoverflow.com/a/1094933
for unit in ['', 'ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
for unit in ["", "ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
if abs(bytes_) < 1024.0:
return "%s %sB" % (round_(bytes_), unit)
bytes_ /= 1024.0
return "%s %sB" % (round_(bytes_), 'Yi')
return "%s %sB" % (round_(bytes_), "Yi")
def round_(n):

View file

@ -17,17 +17,23 @@ class RegenconfDiagnoser(Diagnoser):
regenconf_modified_files = list(self.manually_modified_files())
if not regenconf_modified_files:
yield dict(meta={"test": "regenconf"},
status="SUCCESS",
summary="diagnosis_regenconf_allgood"
)
yield dict(
meta={"test": "regenconf"},
status="SUCCESS",
summary="diagnosis_regenconf_allgood",
)
else:
for f in regenconf_modified_files:
yield dict(meta={"test": "regenconf", "category": f['category'], "file": f['path']},
status="WARNING",
summary="diagnosis_regenconf_manually_modified",
details=["diagnosis_regenconf_manually_modified_details"]
)
yield dict(
meta={
"test": "regenconf",
"category": f["category"],
"file": f["path"],
},
status="WARNING",
summary="diagnosis_regenconf_manually_modified",
details=["diagnosis_regenconf_manually_modified_details"],
)
def manually_modified_files(self):

View file

@ -0,0 +1,28 @@
#!/bin/bash
user=$1
readonly MEDIA_GROUP=multimedia
readonly MEDIA_DIRECTORY=/home/yunohost.multimedia
# We only do this if multimedia directory is enabled (= the folder exists)
[ -e "$MEDIA_DIRECTORY" ] || exit 0
mkdir -p "$MEDIA_DIRECTORY/$user"
mkdir -p "$MEDIA_DIRECTORY/$user/Music"
mkdir -p "$MEDIA_DIRECTORY/$user/Picture"
mkdir -p "$MEDIA_DIRECTORY/$user/Video"
mkdir -p "$MEDIA_DIRECTORY/$user/eBook"
ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share"
# Création du lien symbolique dans le home de l'utilisateur.
ln -sfn "$MEDIA_DIRECTORY/$user" "/home/$user/Multimedia"
# Propriétaires des dossiers utilisateurs.
chown -R $user "$MEDIA_DIRECTORY/$user"
## Application des droits étendus sur le dossier multimedia.
# Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other:
setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY/$user"
# Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers.
setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY/$user"
# Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl.
setfacl -RL -m m::rwx "$MEDIA_DIRECTORY/$user"

View file

@ -0,0 +1,8 @@
#!/bin/bash
user=$1
MEDIA_DIRECTORY=/home/yunohost.multimedia
if [ -n "$user" ] && [ -e "$MEDIA_DIRECTORY/$user" ]; then
sudo rm -r "$MEDIA_DIRECTORY/$user"
fi

View file

@ -7,4 +7,3 @@ cd "$YNH_CWD"
# Restore file if exists
ynh_restore_file --origin_path="/etc/yunohost/dyndns" --not_mandatory
ynh_restore_file --origin_path="/etc/cron.d/yunohost-dyndns" --not_mandatory

33
debian/changelog vendored
View file

@ -4,6 +4,39 @@ yunohost (4.2) unstable; urgency=low
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 20 Jan 2021 05:19:58 +0100
yunohost (4.1.7.2) testing; urgency=low
- [fix] When migration legacy protected permissions, all users were allowed on the new perm (29bd3c4a)
- [fix] Mysql is a fucking joke (... trying to fix the mysql issue on RPi ...) (cd4fdb2b)
- [fix] Replace \t when converting legacy conf.json.persistent... (f398f463)
Thanks to all contributors <3 ! (ljf)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 21 Feb 2021 05:25:49 +0100
yunohost (4.1.7.1) stable; urgency=low
- [enh] helpers: Fix ynh_exec_as regression (ac38e53a7)
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 03 Feb 2021 16:59:05 +0100
yunohost (4.1.7) stable; urgency=low
- [fix] diagnosis: Handle case where DKIM record is split into several pieces (4b876ff0)
- [fix] i18n: de locale was broken (4725e054)
- [enh] diagnosis: Ignore /dev/loop devices in systemresources (536fd9be)
- [fix] backup: fix a small issue dur to var not existing in some edge case ... (2fc016e3)
- [fix] settings: service_regen_conf is deprecated in favor of regen_conf (62e84d8b)
- [fix] users: If uid is less than 1001, nsswitch ignores it (4e335e07, aef3ee14)
- [enh] misc: fixes/enh in yunoprompt (5ab5c83d, 9fbd1a02)
- [enh] helpers: Add ynh_exec_as (b94ff1c2, 6b2d76dd)
- [fix] helpers: Do not ynh_die if systemctl action fails, to avoid exiting during a remove script (29fe7c31)
- [fix] misc: logger.exception -> logger.error (08e7b42c)
Thanks to all contributors <3 ! (ericgaspar, Kayou, ljf)
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 02 Feb 2021 04:18:01 +0100
yunohost (4.1.6) stable; urgency=low
- [fix] Make dyndns update more resilient to ns0.yunohost.org being down ([#1140](https://github.com/yunohost/yunohost/pull/1140))

3
debian/control vendored
View file

@ -26,7 +26,8 @@ Depends: ${python3:Depends}, ${misc:Depends}
, rspamd, opendkim-tools, postsrsd, procmail, mailutils
, redis-server
, metronome (>=3.14.0)
, git, curl, wget, cron, unzip, jq, bc
, acl
, git, curl, wget, cron, unzip, jq, bc, at
, lsb-release, haveged, fake-hwclock, equivs, lsof, whois
Recommends: yunohost-admin
, ntp, inetutils-ping | iputils-ping

3
debian/postinst vendored
View file

@ -6,7 +6,6 @@ do_configure() {
rm -rf /var/cache/moulinette/*
if [ ! -f /etc/yunohost/installed ]; then
# If apps/ is not empty, we're probably already installed in the past and
# something funky happened ...
if [ -d /etc/yunohost/apps/ ] && ls /etc/yunohost/apps/* >/dev/null 2>&1
@ -15,6 +14,8 @@ do_configure() {
else
bash /usr/share/yunohost/hooks/conf_regen/01-yunohost init
bash /usr/share/yunohost/hooks/conf_regen/02-ssl init
bash /usr/share/yunohost/hooks/conf_regen/09-nslcd init
bash /usr/share/yunohost/hooks/conf_regen/46-nsswitch init
bash /usr/share/yunohost/hooks/conf_regen/06-slapd init
bash /usr/share/yunohost/hooks/conf_regen/15-nginx init
fi

View file

@ -5,21 +5,29 @@ import glob
import datetime
import subprocess
def get_current_commit():
p = subprocess.Popen("git rev-parse --verify HEAD", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
p = subprocess.Popen(
"git rev-parse --verify HEAD",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout, stderr = p.communicate()
current_commit = stdout.strip().decode('utf-8')
current_commit = stdout.strip().decode("utf-8")
return current_commit
def render(helpers):
current_commit = get_current_commit()
data = {"helpers": helpers,
"date": datetime.datetime.now().strftime("%m/%d/%Y"),
"version": open("../debian/changelog").readlines()[0].split()[1].strip("()")
}
data = {
"helpers": helpers,
"date": datetime.datetime.now().strftime("%m/%d/%Y"),
"version": open("../debian/changelog").readlines()[0].split()[1].strip("()"),
}
from jinja2 import Template
from ansi2html import Ansi2HTMLConverter
@ -31,17 +39,22 @@ def render(helpers):
def shell_to_html(shell):
return conv.convert(shell, False)
template = open("helper_doc_template.html", "r").read()
template = open("helper_doc_template.md", "r").read()
t = Template(template)
t.globals['now'] = datetime.datetime.utcnow
result = t.render(current_commit=current_commit, data=data, convert=shell_to_html, shell_css=shell_css)
open("helpers.html", "w").write(result)
t.globals["now"] = datetime.datetime.utcnow
result = t.render(
current_commit=current_commit,
data=data,
convert=shell_to_html,
shell_css=shell_css,
)
open("helpers.md", "w").write(result)
##############################################################################
class Parser():
class Parser:
def __init__(self, filename):
self.filename = filename
@ -53,10 +66,7 @@ class Parser():
self.blocks = []
current_reading = "void"
current_block = {"name": None,
"line": -1,
"comments": [],
"code": []}
current_block = {"name": None, "line": -1, "comments": [], "code": []}
for i, line in enumerate(self.file):
@ -73,7 +83,7 @@ class Parser():
current_block["comments"].append(line[2:])
else:
pass
#assert line == "", malformed_error(i)
# assert line == "", malformed_error(i)
continue
elif current_reading == "comments":
@ -84,11 +94,12 @@ class Parser():
elif line.strip() == "":
# Well eh that was not an actual helper definition ... start over ?
current_reading = "void"
current_block = {"name": None,
"line": -1,
"comments": [],
"code": []
}
current_block = {
"name": None,
"line": -1,
"comments": [],
"code": [],
}
elif not (line.endswith("{") or line.endswith("()")):
# Well we're not actually entering a function yet eh
# (c.f. global vars)
@ -96,7 +107,10 @@ class Parser():
else:
# We're getting out of a comment bloc, we should find
# the name of the function
assert len(line.split()) >= 1, "Malformed line %s in %s" % (i, self.filename)
assert len(line.split()) >= 1, "Malformed line %s in %s" % (
i,
self.filename,
)
current_block["line"] = i
current_block["name"] = line.split()[0].strip("(){")
# Then we expect to read the function
@ -110,12 +124,14 @@ class Parser():
# Then we keep this bloc and start a new one
# (we ignore helpers containing [internal] ...)
if not "[internal]" in current_block["comments"]:
if "[internal]" not in current_block["comments"]:
self.blocks.append(current_block)
current_block = {"name": None,
"line": -1,
"comments": [],
"code": []}
current_block = {
"name": None,
"line": -1,
"comments": [],
"code": [],
}
else:
current_block["code"].append(line)
@ -129,7 +145,7 @@ class Parser():
b["args"] = []
b["ret"] = ""
subblocks = '\n'.join(b["comments"]).split("\n\n")
subblocks = "\n".join(b["comments"]).split("\n\n")
for i, subblock in enumerate(subblocks):
subblock = subblock.strip()
@ -192,7 +208,7 @@ class Parser():
def is_global_comment(line):
return line.startswith('#')
return line.startswith("#")
def malformed_error(line_number):

View file

@ -22,20 +22,24 @@ template = Template(open(os.path.join(base_path, "manpage.template")).read())
THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ACTIONSMAP_FILE = os.path.join(THIS_SCRIPT_DIR, '../data/actionsmap/yunohost.yml')
ACTIONSMAP_FILE = os.path.join(THIS_SCRIPT_DIR, "../data/actionsmap/yunohost.yml")
def ordered_yaml_load(stream):
class OrderedLoader(yaml.Loader):
pass
OrderedLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
lambda loader, node: OrderedDict(loader.construct_pairs(node)))
lambda loader, node: OrderedDict(loader.construct_pairs(node)),
)
return yaml.load(stream, OrderedLoader)
def main():
parser = argparse.ArgumentParser(description="generate yunohost manpage based on actionsmap.yml")
parser = argparse.ArgumentParser(
description="generate yunohost manpage based on actionsmap.yml"
)
parser.add_argument("-o", "--output", default="output/yunohost")
parser.add_argument("-z", "--gzip", action="store_true", default=False)
@ -55,7 +59,7 @@ def main():
output_path = args.output
# man pages of "yunohost *"
with open(ACTIONSMAP_FILE, 'r') as actionsmap:
with open(ACTIONSMAP_FILE, "r") as actionsmap:
# Getting the dictionary containning what actions are possible per domain
actionsmap = ordered_yaml_load(actionsmap)
@ -81,5 +85,5 @@ def main():
output.write(result.encode())
if __name__ == '__main__':
if __name__ == "__main__":
main()

View file

@ -1,112 +0,0 @@
<!-- NO_MARKDOWN_PARSING -->
<h1>App helpers</h1>
<p>Doc auto-generated by <a href="https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/doc/generate_helper_doc.py">this script</a> on {{data.date}} (Yunohost version {{data.version}})</p>
{% for category, helpers in data.helpers %}
<h3 style="text-transform: uppercase; font-weight: bold">{{ category }}</h3>
{% for h in helpers %}
<div class="helper-card">
<div class="helper-card-body">
<div data-toggle="collapse" href="#collapse-{{ h.name }}" style="cursor:pointer">
<h5 class="helper-card-title"><tt>{{ h.name }}</tt></h5>
<h6 class="helper-card-subtitle text-muted">{{ h.brief }}</h6>
</div>
<div id="collapse-{{ h.name }}" class="collapse" role="tabpanel">
<hr style="margin-top:25px; margin-bottom:25px;">
<p>
{% if not '\n' in h.usage %}
<strong>Usage</strong>: <code class="helper-code">{{ h.usage }}</code>
{% else %}
<strong>Usage</strong>: <code class="helper-code helper-usage">{{ h.usage }}</code>
{% endif %}
</p>
{% if h.args %}
<p>
<strong>Arguments</strong>:
<ul>
{% for infos in h.args %}
{% if infos|length == 2 %}
<li><code>{{ infos[0] }}</code> : {{ infos[1] }}</li>
{% else %}
<li><code>{{ infos[0] }}</code>, <code>{{ infos[1] }}</code> : {{ infos[2] }}</li>
{% endif %}
{% endfor %}
</ul>
</p>
{% endif %}
{% if h.ret %}
<p>
<strong>Returns</strong>: {{ h.ret }}
</p>
{% endif %}
{% if "example" in h.keys() %}
<p>
<strong>Example</strong>: <code class="helper-code">{{ h.example }}</code>
</p>
{% endif %}
{% if "examples" in h.keys() %}
<p>
<strong>Examples</strong>:<ul>
{% for example in h.examples %}
{% if not example.strip().startswith("# ") %}
<code class="helper-code">{{ example }}</code>
{% else %}
{{ example.strip("# ") }}
{% endif %}
<br>
{% endfor %}
</ul>
</p>
{% endif %}
{% if h.details %}
<p>
<strong>Details</strong>:
<p>
{{ h.details.replace('\n', '</br>') }}
</p>
</p>
{% endif %}
<p>
<a href="https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/data/helpers.d/{{ category }}#L{{ h.line + 1 }}">Dude, show me the code !</a>
</p>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
<style>
/*=================================================
Helper card
=================================================*/
.helper-card {
width:100%;
min-height: 1px;
margin-right: 10px;
margin-left: 10px;
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.5rem;
word-wrap: break-word;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.helper-card-body {
padding: 1.25rem;
padding-top: 0.8rem;
padding-bottom: 0;
}
.helper-code {
word-wrap: break-word;
white-space: normal;
}
/*===============================================*/
</style>

View file

@ -0,0 +1,59 @@
---
title: App helpers
template: docs
taxonomy:
category: docs
routes:
default: '/packaging_apps_helpers'
---
Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/doc/generate_helper_doc.py) on {{data.date}} (Yunohost version {{data.version}})
{% for category, helpers in data.helpers %}
### {{ category.upper() }}
{% for h in helpers %}
**{{ h.name }}**
[details summary="<i>{{ h.brief }}</i>" class="helper-card-subtitle text-muted"]
<p></p>
**Usage**: `{{ h.usage }}`
{% if h.args %}
**Arguments**:
{% for infos in h.args %}
{% if infos|length == 2 %}
- `{{ infos[0] }}`: {{ infos[1] }}
{% else %}
- `{{ infos[0] }}`, `{{ infos[1] }}`: {{ infos[2] }}
{% endif %}
{% endfor %}
{% endif %}
{% if h.ret %}
**Returns**: {{ h.ret }}
{% endif %}
{% if "example" in h.keys() %}
**Example**: `{{ h.example }}`
{% endif %}
{% if "examples" in h.keys() %}
**Examples**:
{% for example in h.examples %}
{% if not example.strip().startswith("# ") %}
- `{{ example }}`
{% else %}
- `{{ example.strip("# ") }}`
{% endif %}
{% endfor %}
{% endif %}
{% if h.details %}
**Details**:
{{ h.details.replace('\n', '</br>').replace('_', '\_') }}
{% endif %}
[Dude, show me the code!](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/data/helpers.d/{{ category }}#L{{ h.line + 1 }})
[/details]
----------------
{% endfor %}
{% endfor %}

View file

@ -714,5 +714,8 @@
"additional_urls_already_removed": "URL addicional «{url:s}» ja ha estat eliminada per al permís «{permission:s}»",
"additional_urls_already_added": "URL addicional «{url:s}» ja ha estat afegida per al permís «{permission:s}»",
"diagnosis_backports_in_sources_list": "Sembla que apt (el gestor de paquets) està configurat per utilitzar el repositori backports. A menys de saber el que esteu fent, recomanem fortament no instal·lar paquets de backports, ja que poder causar inestabilitats o conflictes en el sistema.",
"diagnosis_basesystem_hardware_model": "El model del servidor és {model}"
"diagnosis_basesystem_hardware_model": "El model del servidor és {model}",
"postinstall_low_rootfsspace": "El sistema de fitxers arrel té un total de menys de 10 GB d'espai, el que es preocupant! És molt probable que us quedeu sense espai ràpidament! Es recomana tenir un mínim de 16 GB per al sistema de fitxers arrel. Si voleu instal·lar YunoHost tot i aquest avís, torneu a executar la postinstal·lació amb --force-diskspace",
"diagnosis_rootfstotalspace_critical": "El sistema de fitxers arrel només té {space} en total i és preocupant! És molt probable que us quedeu sense espai ràpidament! Es recomanar tenir un mínim de 16 GB per al sistema de fitxers arrel.",
"diagnosis_rootfstotalspace_warning": "El sistema de fitxers arrel només té {space} en total. Això no hauria de causar cap problema, però haureu de parar atenció ja que us podrieu quedar sense espai ràpidament… Es recomanar tenir un mínim de 16 GB per al sistema de fitxers arrel."
}

View file

@ -1 +1,3 @@
{}
{
"password_too_simple_1": "Heslo musí být aspoň 8 znaků dlouhé"
}

View file

@ -60,20 +60,20 @@
"dyndns_key_generating": "Generierung des DNS-Schlüssels..., das könnte eine Weile dauern.",
"dyndns_registered": "DynDNS Domain registriert",
"dyndns_registration_failed": "DynDNS Domain konnte nicht registriert werden: {error:s}",
"dyndns_unavailable": "DynDNS Subdomain ist nicht verfügbar",
"dyndns_unavailable": "Die Domäne {domain:s} ist nicht verfügbar.",
"executing_command": "Führe den Behfehl '{command:s}' aus…",
"executing_script": "Skript '{script:s}' wird ausgeührt…",
"extracting": "Wird entpackt",
"extracting": "Wird entpackt...",
"field_invalid": "Feld '{:s}' ist unbekannt",
"firewall_reload_failed": "Die Firewall konnte nicht neu geladen werden",
"firewall_reloaded": "Die Firewall wurde neu geladen",
"firewall_rules_cmd_failed": "Einzelne Firewallregeln konnten nicht übernommen werden. Mehr Informationen sind im Log zu finden.",
"hook_exec_failed": "Skriptausführung fehlgeschlagen: {path:s}",
"hook_exec_not_terminated": "Skriptausführung noch nicht beendet: {path:s}",
"hook_list_by_invalid": "Ungültiger Wert zur Anzeige von Hooks",
"firewall_reload_failed": "Firewall konnte nicht neu geladen werden",
"firewall_reloaded": "Firewall neu geladen",
"firewall_rules_cmd_failed": "Einige Befehle für die Firewallregeln sind gescheitert. Mehr Informationen im Log.",
"hook_exec_failed": "Konnte Skript nicht ausführen: {path:s}",
"hook_exec_not_terminated": "Skript ist nicht normal beendet worden: {path:s}",
"hook_list_by_invalid": "Dieser Wert kann nicht verwendet werden, um Hooks anzuzeigen",
"hook_name_unknown": "Hook '{name:s}' ist nicht bekannt",
"installation_complete": "Installation vollständig",
"installation_failed": "Installation fehlgeschlagen",
"installation_failed": "Etwas ist mit der Installation falsch gelaufen",
"ip6tables_unavailable": "ip6tables kann nicht verwendet werden. Du befindest dich entweder in einem Container oder es wird nicht vom Kernel unterstützt",
"iptables_unavailable": "iptables kann nicht verwendet werden. Du befindest dich entweder in einem Container oder es wird nicht vom Kernel unterstützt",
"ldap_initialized": "LDAP wurde initialisiert",
@ -170,9 +170,9 @@
"certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die {domain:s} ist fehlgeschlagen...",
"certmanager_attempt_to_renew_nonLE_cert": "Das Zertifikat der Domain '{domain:s}' wurde nicht von Let's Encrypt ausgestellt. Es kann nicht automatisch erneuert werden!",
"certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain:s} läuft nicht in Kürze ab! (Benutze --force um diese Nachricht zu umgehen)",
"certmanager_domain_http_not_working": "Die Domäne {domain:s} scheint über HTTP nicht erreichbar zu sein. Für weitere Informationen überprüfen Sie bitte die Kategorie 'Web' im Diagnose-Bereich. (Wenn Sie wißen was Sie tun, nutzen Sie '--no-checks' um die Überprüfung zu überspringen.)",
"certmanager_domain_http_not_working": "Es scheint so, dass die Domain {domain:s} nicht über HTTP erreicht werden kann. Bitte überprüfe, ob deine DNS und nginx Konfiguration in Ordnung ist. (Wenn du weißt was du tust, nutze \"--no-checks\" um die überprüfung zu überspringen.)",
"certmanager_error_no_A_record": "Kein DNS 'A' Eintrag für die Domain {domain:s} gefunden. Dein Domainname muss auf diese Maschine weitergeleitet werden, um ein Let's Encrypt Zertifikat installieren zu können! (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen. )",
"certmanager_domain_dns_ip_differs_from_public_ip": "Die DNS-Einträge der Domäne {domain:s} unterscheiden sich von der IP dieses Servers. Wenn Sie gerade Ihren A-Eintrag verändert haben, warten Sie bitte etwas, damit die Änderungen wirksam werden (Sie können die DNS Propagation mittels Website überprüfen) (Wenn Sie wißen was Sie tun, können Sie --no-checks benutzen, um diese Überprüfung zu überspringen. )",
"certmanager_domain_dns_ip_differs_from_public_ip": "Der DNS-A-Eintrag der Domain {domain:s} unterscheidet sich von dieser Server-IP. Für weitere Informationen überprüfen Sie bitte die 'DNS records' (basic) Kategorie in der Diagnose. Wenn Sie gerade Ihren A-Eintrag verändert haben, warten Sie bitte etwas, damit die Änderungen wirksam werden (Sie können die DNS-Propagation mittels Website überprüfen) (Wenn Sie wissen was Sie tun, können Sie --no-checks benutzen, um diese Überprüfung zu überspringen.)",
"certmanager_cannot_read_cert": "Es ist ein Fehler aufgetreten, als es versucht wurde das aktuelle Zertifikat für die Domain {domain:s} zu öffnen (Datei: {file:s}), Grund: {reason:s}",
"certmanager_cert_install_success_selfsigned": "Ein selbstsigniertes Zertifikat für die Domain {domain:s} wurde erfolgreich installiert",
"certmanager_cert_install_success": "Für die Domain {domain:s} wurde erfolgreich ein Let's Encrypt Zertifikat installiert.",
@ -203,11 +203,11 @@
"backup_archive_writing_error": "Die Dateien '{source:s} (im Ordner '{dest:s}') konnten nicht in das komprimierte Archiv-Backup '{archive:s}' hinzugefügt werden",
"app_change_url_success": "{app:s} URL ist nun {domain:s}{path:s}",
"backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup repository...",
"global_settings_bad_type_for_setting": "Falscher Typ r Einstellung {setting:s}. Empfangen: {received_type:s}, aber erwartet: {expected_type:s}",
"global_settings_bad_choice_for_enum": "Falsche Wahl für die Einstellung {setting:s}. Habe '{choice:s}' erhalten, aber es stehen nur folgende Auswahlmöglichkeiten zur Verfügung: {available_choices:s}",
"file_does_not_exist": "Die Datei {path:s} existiert nicht.",
"experimental_feature": "Warnung: Diese Funktion ist experimentell und gilt nicht als stabil. Sie sollten sie nur verwenden, wenn Sie wissen, was Sie tun.",
"dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider:s} kann die Domain(s) {domain:s} nicht bereitstellen.",
"global_settings_bad_type_for_setting": "Falscher Typ der Einstellung {setting:s}. Empfangen: {received_type:s}, aber erwarteter Typ: {expected_type:s}",
"global_settings_bad_choice_for_enum": "Wert des Einstellungsparameters {setting:s} ungültig. Der Wert den Sie eingegeben haben: '{choice:s}', die gültigen Werte für diese Einstellung: {available_choices:s}",
"file_does_not_exist": "Die Datei {path: s} existiert nicht.",
"experimental_feature": "Warnung: Der Maintainer hat diese Funktion als experimentell gekennzeichnet. Sie ist nicht stabil. Sie sollten sie nur verwenden, wenn Sie wissen, was Sie tun.",
"dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider:s} kann die Domäne(n) {domain:s} nicht bereitstellen.",
"dyndns_could_not_check_available": "Konnte nicht überprüfen, ob {domain:s} auf {provider:s} verfügbar ist.",
"dyndns_could_not_check_provide": "Konnte nicht überprüft, ob {provider:s} die Domain(s) {domain:s} bereitstellen kann.",
"domain_dns_conf_is_just_a_recommendation": "Dieser Befehl zeigt Ihnen die * empfohlene * Konfiguration. Die DNS-Konfiguration wird NICHT für Sie eingerichtet. Es liegt in Ihrer Verantwortung, Ihre DNS-Zone in Ihrem Registrar gemäß dieser Empfehlung zu konfigurieren.",
@ -253,14 +253,14 @@
"backup_copying_to_organize_the_archive": "Kopieren von {size:s} MB, um das Archiv zu organisieren",
"global_settings_setting_security_ssh_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den SSH-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)",
"group_deleted": "Gruppe '{group}' gelöscht",
"group_deletion_failed": "Kann Gruppe '{group}' nicht löschen",
"group_deletion_failed": "Konnte Gruppe '{group}' nicht löschen",
"dyndns_provider_unreachable": "DynDNS-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.",
"group_created": "Gruppe '{group}' angelegt",
"group_creation_failed": "Kann Gruppe '{group}' nicht anlegen",
"group_creation_failed": "Konnte Gruppe '{group}' nicht anlegen",
"group_unknown": "Die Gruppe '{group:s}' ist unbekannt",
"group_updated": "Gruppe '{group:s}' erneuert",
"group_update_failed": "Kann Gruppe '{group:s}' nicht aktualisieren: {error}",
"log_does_exists": "Es gibt kein Operationsprotokoll mit dem Namen'{log}', verwende'yunohost log list', um alle verfügbaren Operationsprotokolle anzuzeigen",
"log_does_exists": "Es gibt kein Operationsprotokoll mit dem Namen'{log}', verwende 'yunohost log list', um alle verfügbaren Operationsprotokolle anzuzeigen",
"log_operation_unit_unclosed_properly": "Die Operationseinheit wurde nicht richtig geschlossen",
"global_settings_setting_security_postfix_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den Postfix-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)",
"log_category_404": "Die Log-Kategorie '{category}' existiert nicht",
@ -274,28 +274,28 @@
"backup_php5_to_php7_migration_may_fail": "Dein Archiv konnte nicht für PHP 7 konvertiert werden, Du kannst deine PHP-Anwendungen möglicherweise nicht wiederherstellen (Grund: {error:s})",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Erlaubt die Verwendung eines (veralteten) DSA-Hostkeys für die SSH-Daemon-Konfiguration",
"global_settings_setting_example_string": "Beispiel einer string Option",
"log_app_remove": "Entferne die Anwendung '{}'",
"log_app_remove": "Entferne die Applikation '{}'",
"global_settings_setting_example_int": "Beispiel einer int Option",
"global_settings_cant_open_settings": "Einstellungsdatei konnte nicht geöffnet werden, Grund: {reason:s}",
"global_settings_cant_write_settings": "Einstellungsdatei konnte nicht gespeichert werden, Grund: {reason:s}",
"log_app_install": "Installiere die Anwendung '{}'",
"log_app_install": "Installiere die Applikation '{}'",
"global_settings_reset_success": "Frühere Einstellungen werden nun auf {path:s} gesichert",
"log_app_upgrade": "Upgrade der Anwendung '{}'",
"good_practices_about_admin_password": "Sie sind nun dabei, ein neues Administrationspasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - obwohl es sinnvoll ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.",
"log_app_upgrade": "Upgrade der Applikation '{}'",
"good_practices_about_admin_password": "Sie sind nun dabei, ein neues Administrationspasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein, obwohl es sinnvoll ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.",
"log_corrupted_md_file": "Die mit Protokollen verknüpfte YAML-Metadatendatei ist beschädigt: '{md_file}\nFehler: {error}''",
"global_settings_cant_serialize_settings": "Einstellungsdaten konnten nicht serialisiert werden, Grund: {reason:s}",
"log_help_to_get_failed_log": "Der Vorgang'{desc}' konnte nicht abgeschlossen werden. Bitte teile das vollständige Protokoll dieser Operation mit dem Befehl 'yunohost log share {name}', um Hilfe zu erhalten",
"backup_no_uncompress_archive_dir": "Dieses unkomprimierte Archivverzeichnis gibt es nicht",
"log_app_change_url": "Ändere die URL der Anwendung '{}'",
"log_app_change_url": "Ändere die URL der Applikation '{}'",
"global_settings_setting_security_password_user_strength": "Stärke des Benutzerpassworts",
"good_practices_about_user_password": "Du bist nun dabei, ein neues Benutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - obwohl es ratsam ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.",
"good_practices_about_user_password": "Sie sind dabei, ein neues Benutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein, obwohl es ratsam ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.",
"global_settings_setting_example_enum": "Beispiel einer enum Option",
"log_link_to_failed_log": "Der Vorgang konnte nicht abgeschlossen werden '{desc}'. Bitte gib das vollständige Protokoll dieser Operation mit <a href=\"#/tools/logs/{name}\">Klicken Sie hier</a> an, um Hilfe zu erhalten",
"backup_cant_mount_uncompress_archive": "Das unkomprimierte Archiv konnte nicht als schreibgeschützt gemountet werden",
"backup_csv_addition_failed": "Es konnten keine Dateien zur Sicherung in die CSV-Datei hinzugefügt werden",
"global_settings_setting_security_password_admin_strength": "Stärke des Admin-Passworts",
"global_settings_key_doesnt_exists": "Der Schlüssel'{settings_key:s}' existiert nicht in den globalen Einstellungen, du kannst alle verfügbaren Schlüssel sehen, indem du 'yunohost settings list' ausführst",
"log_app_makedefault": "Mache '{}' zur Standard-Anwendung",
"log_app_makedefault": "Mache '{}' zur Standard-Applikation",
"hook_json_return_error": "Konnte die Rückkehr vom Einsprungpunkt {path:s} nicht lesen. Fehler: {msg:s}. Unformatierter Inhalt: {raw_content}",
"app_full_domain_unavailable": "Es tut uns leid, aber diese Anwendung erfordert die Installation auf einer eigenen Domain, aber einige andere Anwendungen sind bereits auf der Domäne'{domain}' installiert. Eine mögliche Lösung ist das Hinzufügen und Verwenden einer Subdomain, die dieser Anwendung zugeordnet ist.",
"app_install_failed": "{app} kann nicht installiert werden: {error}",
@ -377,7 +377,7 @@
"service_reloaded_or_restarted": "Der Dienst '{service:s}' wurde erfolgreich neu geladen oder gestartet",
"service_restarted": "Der Dienst '{service:s}' wurde neu gestartet",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' ist veraltet! Bitte verwenden Sie stattdessen 'yunohost tools regen-conf'.",
"certmanager_warning_subdomain_dns_record": "Die Subdomain '{subdomain:s}' löst nicht dieselbe IP wie '{domain:s} auf. Einige Funktionen werden nicht verfügbar sein, solange Sie dies nicht beheben und das Zertifikat erneuern.",
"certmanager_warning_subdomain_dns_record": "Die Subdomäne \"{subdomain:s}\" löst nicht zur gleichen IP Adresse auf wie \"{domain:s}\". Einige Funktionen sind nicht verfügbar bis du dies behebst und die Zertifikate neu erzeugst.",
"diagnosis_ports_ok": "Port {port} ist von außen erreichbar.",
"diagnosis_ram_verylow": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total})",
"diagnosis_mail_outgoing_port_25_blocked_details": "Sie sollten zuerst versuchen den ausgehenden Port 25 auf Ihrer Router-Konfigurationsoberfläche oder Ihrer Hosting-Anbieter-Konfigurationsoberfläche zu öffnen. (Bei einigen Hosting-Anbieter kann es sein, daß Sie verlangen, daß man dafür ein Support-Ticket sendet).",
@ -489,5 +489,90 @@
"global_settings_setting_smtp_relay_port": "SMTP Relay Port",
"global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden",
"global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver",
"domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add <andere-domian.de>\", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n <andere-domain.de>\", erst jetzt kannst du die domain \"{domain:s}\" entfernen."
"domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add <andere-domian.de>\", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n <andere-domain.de>\", erst jetzt kannst du die domain \"{domain:s}\" entfernen.",
"diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.",
"diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.",
"global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn Sie in einer der folgenden Situationen sind: Ihr ISP- oder VPS-Provider hat Ihren Port 25 geblockt, eine Ihrer residentiellen IPs ist auf DUHL gelistet, Sie können keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und Sie möchten einen anderen verwenden, um E-Mails zu versenden.",
"global_settings_setting_backup_compress_tar_archives": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen.",
"log_remove_on_failed_restore": "'{}' entfernen nach einer fehlerhaften Wiederherstellung aus einem Backup-Archiv",
"log_backup_restore_app": "'{}' aus einem Backup-Archiv wiederherstellen",
"log_backup_restore_system": "System aus einem Backup-Archiv wiederherstellen",
"log_available_on_yunopaste": "Das Protokoll ist nun via {url} verfügbar",
"log_app_config_apply": "Wende die Konfiguration auf die Applikation '{}' an",
"log_app_config_show_panel": "Zeige das Konfigurations-Panel der Applikation '{}'",
"log_app_action_run": "Führe Aktion der Applikation '{}' aus",
"invalid_regex": "Ungültige Regex:'{regex:s}'",
"migration_description_0016_php70_to_php73_pools": "Migrieren der php7.0-fpm-Konfigurationsdateien zu php7.3",
"mailbox_disabled": "E-Mail für Benutzer {user:s} deaktiviert",
"log_tools_reboot": "Server neustarten",
"log_tools_shutdown": "Server ausschalten",
"log_tools_upgrade": "Systempakete aktualisieren",
"log_tools_postinstall": "Post-Installation des YunoHost-Servers durchführen",
"log_tools_migrations_migrate_forward": "Migrationen durchführen",
"log_domain_main_domain": "Mache '{}' zur Hauptdomäne",
"log_user_permission_reset": "Zurücksetzen der Berechtigung '{}'",
"log_user_permission_update": "Aktualisiere Zugriffe für Berechtigung '{}'",
"log_user_update": "Aktualisiere Information für Benutzer '{}'",
"log_user_group_update": "Aktualisiere Gruppe '{}'",
"log_user_group_delete": "Lösche Gruppe '{}'",
"log_user_group_create": "Erstelle Gruppe '{}'",
"log_user_delete": "Lösche Benutzer '{}'",
"log_user_create": "Füge Benutzer '{}' hinzu",
"log_permission_url": "Aktualisiere URL, die mit der Berechtigung '{}' verknüpft ist",
"log_permission_delete": "Lösche Berechtigung '{}'",
"log_permission_create": "Erstelle Berechtigung '{}'",
"log_dyndns_update": "Die IP, die mit der YunoHost-Subdomain '{}' verbunden ist, aktualisieren",
"log_dyndns_subscribe": "Für eine YunoHost-Subdomain registrieren '{}'",
"log_domain_remove": "Entfernen der Domäne '{}' aus der Systemkonfiguration",
"log_domain_add": "Hinzufügen der Domäne '{}' zur Systemkonfiguration",
"log_remove_on_failed_install": "Entfernen von '{}' nach einer fehlgeschlagenen Installation",
"migration_0015_still_on_stretch_after_main_upgrade": "Etwas ist schiefgelaufen während dem Haupt-Upgrade. Das System scheint immer noch auf Debian Stretch zu laufen",
"migration_0015_yunohost_upgrade": "Beginne YunoHost-Core-Upgrade...",
"migration_description_0019_extend_permissions_features": "Erweitern und überarbeiten des Applikationsberechtigungs-Managementsystems",
"migrating_legacy_permission_settings": "Migrieren der Legacy-Berechtigungseinstellungen...",
"migration_description_0017_postgresql_9p6_to_11": "Migrieren der Datenbanken von PostgreSQL 9.6 nach 11",
"migration_0015_main_upgrade": "Beginne Haupt-Upgrade...",
"migration_0015_not_stretch": "Die aktuelle Debian-Distribution ist nicht Stretch!",
"migration_0015_not_enough_free_space": "Der freie Speicher in /var/ ist sehr gering! Sie sollten minimal 1GB frei haben, um diese Migration durchzuführen.",
"domain_remove_confirm_apps_removal": "Wenn Sie diese Domäne löschen, werden folgende Applikationen entfernt:\n{apps}\n\nSind Sie sicher? [{answers}]",
"migration_0015_cleaning_up": "Bereinigung des Cache und der Pakete, welche nicht mehr benötigt werden...",
"migration_0017_postgresql_96_not_installed": "PostgreSQL wurde auf ihrem System nicht installiert. Nichts zu tun.",
"migration_0015_system_not_fully_up_to_date": "Ihr System ist nicht vollständig auf dem neuesten Stand. Bitte führen Sie ein reguläres Upgrade durch, bevor Sie die Migration auf Buster durchführen.",
"migration_0015_modified_files": "Bitte beachten Sie, dass die folgenden Dateien als manuell bearbeitet erkannt wurden und beim nächsten Upgrade überschrieben werden könnten: {manually_modified_files}",
"migration_0015_general_warning": "Bitte beachten Sie, dass diese Migration eine heikle Angelegenheit darstellt. Das YunoHost-Team hat alles unternommen, um sie zu testen und zu überarbeiten. Dennoch ist es möglich, dass diese Migration Teile des Systems oder Applikationen beschädigen könnte.\n\nDeshalb ist folgendes zu empfehlen:\n…- Führen Sie ein Backup aller kritischen Daten und Applikationen durch. Mehr unter https://yunohost.org/backup;\n…- Seien Sie geduldig nachdem Sie die Migration gestartet haben: Abhängig von Ihrer Internetverbindung und Ihrer Hardware kann es einige Stunden dauern, bis das Upgrade fertig ist.",
"migration_0015_problematic_apps_warning": "Bitte beachten Sie, dass folgende möglicherweise problematischen Applikationen auf Ihrer Installation erkannt wurden. Es scheint, als ob sie nicht aus dem YunoHost-Applikationskatalog installiert oder nicht als 'working' gekennzeichnet worden sind. Folglich kann nicht garantiert werden, dass sie nach dem Upgrade immer noch funktionieren: {problematic_apps}",
"migration_0015_specific_upgrade": "Start des Upgrades der Systempakete, deren Upgrade separat durchgeführt werden muss...",
"migration_0015_weak_certs": "Die folgenden Zertifikate verwenden immer noch schwache Signierungsalgorithmen und müssen aktualisiert werden um mit der nächsten Version von nginx kompatibel zu sein: {certs}",
"migrations_pending_cant_rerun": "Diese Migrationen sind immer noch anstehend und können deshalb nicht erneut durchgeführt werden: {ids}",
"migration_0019_add_new_attributes_in_ldap": "Hinzufügen neuer Attribute für die Berechtigungen in der LDAP-Datenbank",
"migration_0019_can_not_backup_before_migration": "Das Backup des Systems konnte nicht abgeschlossen werden bevor die Migration fehlschlug. Fehlermeldung: {error}",
"migration_0019_migration_failed_trying_to_rollback": "Konnte nicht migrieren... versuche ein Rollback des Systems.",
"migrations_not_pending_cant_skip": "Diese Migrationen sind nicht anstehend und können deshalb nicht übersprungen werden: {ids}",
"migration_0018_failed_to_reset_legacy_rules": "Zurücksetzen der veralteten iptables-Regeln fehlgeschlagen: {error}",
"migration_0019_rollback_success": "Rollback des Systems durchgeführt.",
"migration_0019_slapd_config_will_be_overwritten": "Es schaut aus, als ob Sie die slapd-Konfigurationsdatei manuell bearbeitet haben. Für diese kritische Migration muss das Update der slapd-Konfiguration erzwungen werden. Von der Originaldatei wird ein Backup gemacht in {conf_backup_folder}.",
"migrations_success_forward": "Migration {id} abgeschlossen",
"migrations_cant_reach_migration_file": "Die Migrationsdateien konnten nicht aufgerufen werden im Verzeichnis '%s'",
"migrations_dependencies_not_satisfied": "Führen Sie diese Migrationen aus: '{dependencies_id}', vor der Migration {id}.",
"migrations_failed_to_load_migration": "Konnte Migration nicht laden {id}: {error}",
"migrations_list_conflict_pending_done": "Sie können nicht '--previous' und '--done' gleichzeitig benützen.",
"migrations_already_ran": "Diese Migrationen wurden bereits durchgeführt: {ids}",
"migrations_loading_migration": "Lade Migrationen {id}...",
"migrations_migration_has_failed": "Migration {id} gescheitert mit der Ausnahme {exception}: Abbruch",
"migrations_must_provide_explicit_targets": "Sie müssen konkrete Ziele angeben, wenn Sie '--skip' oder '--force-rerun' verwenden",
"migrations_need_to_accept_disclaimer": "Um die Migration {id} durchzuführen, müssen Sie den Disclaimer akzeptieren.\n---\n{disclaimer}\n---\n Wenn Sie bestätigen, dass Sie die Migration durchführen wollen, wiederholen Sie bitte den Befehl mit der Option '--accept-disclaimer'.",
"migrations_no_migrations_to_run": "Keine Migrationen durchzuführen",
"migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 ist installiert aber nicht postgreSQL 11? Etwas komisches ist Ihrem System zugestossen :(...",
"migration_0017_not_enough_space": "Stellen Siea ausreichend Speicherplatz im Verzeichnis {path} zur Verfügung um die Migration durchzuführen.",
"migration_0018_failed_to_migrate_iptables_rules": "Migration der veralteten iptables-Regeln zu nftables fehlgeschlagen: {error}",
"migration_0019_backup_before_migration": "Ein Backup der LDAP-Datenbank und der Applikationseinstellungen erstellen vor der Migration.",
"migrations_exclusive_options": "'--auto', '--skip' und '--force-rerun' sind Optionen, die sich gegenseitig ausschliessen.",
"migrations_no_such_migration": "Es existiert keine Migration genannt '{id}'",
"migrations_running_forward": "Durchführen der Migrationen {id}...",
"migrations_skip_migration": "Überspringe Migrationen {id}...",
"password_too_simple_2": "Dieses Passwort gehört zu den meistverwendeten der Welt. Bitte nehmen Sie etwas einzigartigeres.",
"password_listed": "Dieses Passwort gehört zu den meistverwendeten der Welt. Bitte nehmen Sie etwas einzigartigeres.",
"operation_interrupted": "Wurde die Operation manuell unterbrochen?",
"invalid_number": "Muss eine Zahl sein",
"migrations_to_be_ran_manually": "Die Migration {id} muss manuell durchgeführt werden. Bitte gehen Sie zu Werkzeuge → Migrationen auf der Webadmin-Seite oder führen Sie 'yunohost tools migrations run' aus."
}

View file

@ -280,6 +280,7 @@
"domain_dyndns_root_unknown": "Unknown DynDNS root domain",
"domain_exists": "The domain already exists",
"domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).",
"domain_remove_confirm_apps_removal": "Removing this domain will remove those applications:\n{apps}\n\nAre you sure you want to do that? [{answers}]",
"domain_uninstall_app_first": "Those applications are still installed on your domain:\n{apps}\n\nPlease uninstall them using 'yunohost app remove the_app_id' or move them to another domain using 'yunohost app change-url the_app_id' before proceeding to domain removal",
"domain_name_unknown": "Domain '{domain}' unknown",
"domain_unknown": "Unknown domain",
@ -290,9 +291,6 @@
"dpkg_lock_not_available": "This command can't be run right now because another program seems to be using the lock of dpkg (the system package manager)",
"dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.",
"dyndns_could_not_check_available": "Could not check if {domain:s} is available on {provider:s}.",
"dyndns_cron_installed": "DynDNS cron job created",
"dyndns_cron_remove_failed": "Could not remove the DynDNS cron job because: {error}",
"dyndns_cron_removed": "DynDNS cron job removed",
"dyndns_ip_update_failed": "Could not update IP address to DynDNS",
"dyndns_ip_updated": "Updated your IP on DynDNS",
"dyndns_key_generating": "Generating DNS key... It may take a while.",
@ -627,8 +625,6 @@
"user_update_failed": "Could not update user {user}: {error}",
"user_updated": "User info changed",
"yunohost_already_installed": "YunoHost is already installed",
"yunohost_ca_creation_failed": "Could not create certificate authority",
"yunohost_ca_creation_success": "Local certification authority created.",
"yunohost_configured": "YunoHost is now configured",
"yunohost_installing": "Installing YunoHost...",
"yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'",

View file

@ -54,7 +54,7 @@
"domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS",
"domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu",
"domain_exists": "Le domaine existe déjà",
"domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nAfin de pouvoir procéder à la suppression du domaine, vous devez préalablement :\n- soit désinstaller toutes ces applications avec la commande 'yunohost app remove nom-de-l-application' ;\n- soit déplacer toutes ces applications vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application'",
"domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nVeuillez les désinstaller avec la commande 'yunohost app remove nom-de-l-application' ou les déplacer vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application' avant de procéder à la suppression du domaine",
"domain_unknown": "Domaine inconnu",
"done": "Terminé",
"downloading": "Téléchargement en cours …",
@ -327,7 +327,7 @@
"password_too_simple_3": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux",
"password_too_simple_4": "Le mot de passe doit comporter au moins 12 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux",
"root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost na pas pu le propager au mot de passe root !",
"aborting": "Annulation.",
"aborting": "Annulation en cours.",
"app_not_upgraded": "Lapplication {failed_app} na pas été mise à jour et par conséquence les applications suivantes nont pas été mises à jour : {apps}",
"app_start_install": "Installation de {app}...",
"app_start_remove": "Suppression de {app}...",
@ -650,7 +650,7 @@
"migration_description_0018_xtable_to_nftable": "Migrer les anciennes règles de trafic réseau vers le nouveau système basé sur nftables",
"service_description_php7.3-fpm": "Exécute les applications écrites en PHP avec NGINX",
"migration_0018_failed_to_reset_legacy_rules": "La réinitialisation des règles iptable par défaut a échoué : {error}",
"migration_0018_failed_to_migrate_iptables_rules": "La migration des règles iptables héritées vers nftables a échoué: {error}",
"migration_0018_failed_to_migrate_iptables_rules": "Échec de la migration des anciennes règles iptables vers nftables : {error}",
"migration_0017_not_enough_space": "Laissez suffisamment d'espace disponible dans {path} avant de lancer la migration.",
"migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 est installé mais pas posgreSQL 11 ? Il s'est sans doute passé quelque chose d'étrange sur votre système :(...",
"migration_0017_postgresql_96_not_installed": "PostgreSQL n'a pas été installé sur votre système. Aucune opération à effectuer.",
@ -692,5 +692,9 @@
"invalid_number": "Doit être un nombre",
"migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives",
"diagnosis_basesystem_hardware_model": "Le modèle du serveur est {model}",
"diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système."
"diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système.",
"postinstall_low_rootfsspace": "Le système de fichiers racine a une taille totale inférieure à 10 GB, ce qui est inquiétant ! Vous allez certainement arriver à court d'espace disque rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez le postinstall avec --force-diskspace",
"domain_remove_confirm_apps_removal": "Le retrait de ce domaine retirera aussi ces applications :\n{apps}\n\nÊtes vous sûr de vouloir cela ? [{answers}]",
"diagnosis_rootfstotalspace_critical": "Le système de fichiers racine ne fait que {space} ! Vous allez certainement les remplir très rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.",
"diagnosis_rootfstotalspace_warning": "Le système de fichiers racine n'est que de {space}. Ça peut suffire, mais faites attention car vous risquez de les remplire rapidement... Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers."
}

View file

@ -591,7 +591,7 @@
"log_user_permission_update": "Aggiorna gli accessi del permesso '{}'",
"log_user_group_update": "Aggiorna il gruppo '{}'",
"log_user_group_delete": "Cancella il gruppo '{}'",
"log_user_group_create": "Crea il gruppo '[}'",
"log_user_group_create": "Crea il gruppo '{}'",
"log_permission_url": "Aggiorna l'URL collegato al permesso '{}'",
"log_permission_delete": "Cancella permesso '{}'",
"log_permission_create": "Crea permesso '{}'",

View file

@ -3,15 +3,15 @@
"admin_password": "Administrator wachtwoord",
"admin_password_changed": "Het administratie wachtwoord werd gewijzigd",
"app_already_installed": "{app:s} is al geïnstalleerd",
"app_argument_invalid": "'{name:s}' bevat ongeldige waarde: {error:s}",
"app_argument_invalid": "Kies een geldige waarde voor '{name:s}': {error:s}",
"app_argument_required": "Het '{name:s}' moet ingevuld worden",
"app_extraction_failed": "Kan installatiebestanden niet uitpakken",
"app_id_invalid": "Ongeldige app-id",
"app_install_files_invalid": "Ongeldige installatiebestanden",
"app_install_files_invalid": "Deze bestanden kunnen niet worden geïnstalleerd",
"app_manifest_invalid": "Ongeldig app-manifest",
"app_not_installed": "{app:s} is niet geïnstalleerd",
"app_removed": "{app:s} succesvol verwijderd",
"app_sources_fetch_failed": "Kan bronbestanden niet ophalen",
"app_sources_fetch_failed": "Kan bronbestanden niet ophalen, klopt de URL?",
"app_unknown": "Onbekende app",
"app_upgrade_failed": "Kan app {app:s} niet updaten",
"app_upgraded": "{app:s} succesvol geüpgraded",
@ -82,7 +82,7 @@
"app_argument_choice_invalid": "Ongeldige keuze voor argument '{name:s}'. Het moet een van de volgende keuzes zijn {choices:s}",
"app_not_correctly_installed": "{app:s} schijnt niet juist geïnstalleerd te zijn",
"app_not_properly_removed": "{app:s} werd niet volledig verwijderd",
"app_requirements_checking": "Controleer noodzakelijke pakketten...",
"app_requirements_checking": "Noodzakelijke pakketten voor {app} aan het controleren...",
"app_requirements_unmeet": "Er wordt niet aan de aanvorderingen voldaan, het pakket {pkgname} ({version}) moet {spec} zijn",
"app_unsupported_remote_type": "Niet ondersteund besturings type voor de app",
"ask_main_domain": "Hoofd-domein",
@ -101,5 +101,25 @@
"already_up_to_date": "Er is niets te doen, alles is al up-to-date.",
"admin_password_too_long": "Gelieve een wachtwoord te kiezen met minder dan 127 karakters",
"app_action_cannot_be_ran_because_required_services_down": "De volgende diensten moeten actief zijn om deze actie uit te voeren: {services}. Probeer om deze te herstarten om verder te gaan (en om eventueel te onderzoeken waarom ze niet werken).",
"aborting": "Annulatie."
}
"aborting": "Annulatie.",
"app_upgrade_app_name": "Bezig {app} te upgraden...",
"app_make_default_location_already_used": "Kan '{app}' niet de standaardapp maken op het domein, '{domain}' wordt al gebruikt door '{other_app}'",
"app_install_failed": "Kan {app} niet installeren: {error}",
"app_remove_after_failed_install": "Bezig de app te verwijderen na gefaalde installatie...",
"app_manifest_install_ask_domain": "Kies het domein waar deze app op geïnstalleerd moet worden",
"app_manifest_install_ask_path": "Kies het pad waar deze app geïnstalleerd moet worden",
"app_manifest_install_ask_admin": "Kies een administrator voor deze app",
"app_change_url_failed_nginx_reload": "Kon NGINX niet opnieuw laden. Hier is de output van 'nginx -t':\n{nginx_errors:s}",
"app_change_url_success": "{app:s} URL is nu {domain:s}{path:s}",
"app_full_domain_unavailable": "Sorry, deze app moet op haar eigen domein geïnstalleerd worden, maar andere apps zijn al geïnstalleerd op het domein '{domain}'. U kunt wel een subdomein aan deze app toewijden.",
"app_install_script_failed": "Er is een fout opgetreden in het installatiescript van de app",
"app_location_unavailable": "Deze URL is niet beschikbaar of is in conflict met de al geïnstalleerde app(s):\n{apps:s}",
"app_manifest_install_ask_password": "Kies een administratiewachtwoord voor deze app",
"app_manifest_install_ask_is_public": "Moet deze app zichtbaar zijn voor anomieme bezoekers?",
"app_not_upgraded": "De app '{failed_app}' kon niet upgraden en daardoor zijn de upgrades van de volgende apps geannuleerd: {apps}",
"app_start_install": "{app} installeren...",
"app_start_remove": "{app} verwijderen...",
"app_start_backup": "Bestanden aan het verzamelen voor de backup van {app}...",
"app_start_restore": "{app} herstellen...",
"app_upgrade_several_apps": "De volgende apps zullen worden geüpgraded: {apps}"
}

View file

@ -2,7 +2,7 @@
"password_too_simple_1": "密码长度至少为8个字符",
"backup_created": "备份已创建",
"app_start_remove": "正在删除{app}……",
"admin_password_change_failed": "不能修改密码",
"admin_password_change_failed": "无法修改密码",
"admin_password_too_long": "请选择一个小于127个字符的密码",
"app_upgrade_failed": "不能升级{app:s}{error}",
"app_id_invalid": "无效 app ID",

View file

@ -11,7 +11,7 @@ from moulinette.interfaces.cli import colorize, get_locale
def is_installed():
return os.path.isfile('/etc/yunohost/installed')
return os.path.isfile("/etc/yunohost/installed")
def cli(debug, quiet, output_as, timeout, args, parser):
@ -22,12 +22,7 @@ def cli(debug, quiet, output_as, timeout, args, parser):
if not is_installed():
check_command_is_valid_before_postinstall(args)
ret = moulinette.cli(
args,
output_as=output_as,
timeout=timeout,
top_parser=parser
)
ret = moulinette.cli(args, output_as=output_as, timeout=timeout, top_parser=parser)
sys.exit(ret)
@ -36,7 +31,7 @@ def api(debug, host, port):
init_logging(interface="api", debug=debug)
def is_installed_api():
return {'installed': is_installed()}
return {"installed": is_installed()}
# FIXME : someday, maybe find a way to disable route /postinstall if
# postinstall already done ...
@ -44,22 +39,25 @@ def api(debug, host, port):
ret = moulinette.api(
host=host,
port=port,
routes={('GET', '/installed'): is_installed_api},
routes={("GET", "/installed"): is_installed_api},
)
sys.exit(ret)
def check_command_is_valid_before_postinstall(args):
allowed_if_not_postinstalled = ['tools postinstall',
'tools versions',
'backup list',
'backup restore',
'log display']
allowed_if_not_postinstalled = [
"tools postinstall",
"tools versions",
"tools shell",
"backup list",
"backup restore",
"log display",
]
if (len(args) < 2 or (args[0] + ' ' + args[1] not in allowed_if_not_postinstalled)):
if len(args) < 2 or (args[0] + " " + args[1] not in allowed_if_not_postinstalled):
init_i18n()
print(colorize(m18n.g('error'), 'red') + " " + m18n.n('yunohost_not_installed'))
print(colorize(m18n.g("error"), "red") + " " + m18n.n("yunohost_not_installed"))
sys.exit(1)
@ -71,6 +69,7 @@ def init(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"):
init_logging(interface=interface, debug=debug, quiet=quiet, logdir=logdir)
init_i18n()
from moulinette.core import MoulinetteLock
lock = MoulinetteLock("yunohost", timeout=30)
lock.acquire()
return lock
@ -79,14 +78,11 @@ def init(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"):
def init_i18n():
# This should only be called when not willing to go through moulinette.cli
# or moulinette.api but still willing to call m18n.n/g...
m18n.load_namespace('yunohost')
m18n.load_namespace("yunohost")
m18n.set_locale(get_locale())
def init_logging(interface="cli",
debug=False,
quiet=False,
logdir="/var/log/yunohost"):
def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"):
logfile = os.path.join(logdir, "yunohost-%s.log" % interface)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -45,7 +45,7 @@ from yunohost.service import _run_service_command
from yunohost.regenconf import regen_conf
from yunohost.log import OperationLogger
logger = getActionLogger('yunohost.certmanager')
logger = getActionLogger("yunohost.certmanager")
CERT_FOLDER = "/etc/yunohost/certs/"
TMP_FOLDER = "/tmp/acme-challenge-private/"
@ -54,7 +54,7 @@ WEBROOT_FOLDER = "/tmp/acme-challenge-public/"
SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem"
ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem"
SSL_DIR = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
SSL_DIR = "/usr/share/yunohost/yunohost-config/ssl/yunoCA"
KEY_SIZE = 3072
@ -83,14 +83,14 @@ def certificate_status(domain_list, full=False):
# If no domains given, consider all yunohost domains
if domain_list == []:
domain_list = yunohost.domain.domain_list()['domains']
domain_list = yunohost.domain.domain_list()["domains"]
# Else, validate that yunohost knows the domains given
else:
yunohost_domains_list = yunohost.domain.domain_list()['domains']
yunohost_domains_list = yunohost.domain.domain_list()["domains"]
for domain in domain_list:
# Is it in Yunohost domain list?
if domain not in yunohost_domains_list:
raise YunohostError('domain_name_unknown', domain=domain)
raise YunohostError("domain_name_unknown", domain=domain)
certificates = {}
@ -107,7 +107,7 @@ def certificate_status(domain_list, full=False):
try:
_check_domain_is_ready_for_ACME(domain)
status["ACME_eligible"] = True
except:
except Exception:
status["ACME_eligible"] = False
del status["domain"]
@ -116,7 +116,9 @@ def certificate_status(domain_list, full=False):
return {"certificates": certificates}
def certificate_install(domain_list, force=False, no_checks=False, self_signed=False, staging=False):
def certificate_install(
domain_list, force=False, no_checks=False, self_signed=False, staging=False
):
"""
Install a Let's Encrypt certificate for given domains (all by default)
@ -131,21 +133,24 @@ def certificate_install(domain_list, force=False, no_checks=False, self_signed=F
if self_signed:
_certificate_install_selfsigned(domain_list, force)
else:
_certificate_install_letsencrypt(
domain_list, force, no_checks, staging)
_certificate_install_letsencrypt(domain_list, force, no_checks, staging)
def _certificate_install_selfsigned(domain_list, force=False):
for domain in domain_list:
operation_logger = OperationLogger('selfsigned_cert_install', [('domain', domain)],
args={'force': force})
operation_logger = OperationLogger(
"selfsigned_cert_install", [("domain", domain)], args={"force": force}
)
# Paths of files and folder we'll need
date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S")
new_cert_folder = "%s/%s-history/%s-selfsigned" % (
CERT_FOLDER, domain, date_tag)
CERT_FOLDER,
domain,
date_tag,
)
conf_template = os.path.join(SSL_DIR, "openssl.cnf")
@ -160,8 +165,10 @@ def _certificate_install_selfsigned(domain_list, force=False):
if not force and os.path.isfile(current_cert_file):
status = _get_status(domain)
if status["summary"]["code"] in ('good', 'great'):
raise YunohostError('certmanager_attempt_to_replace_valid_cert', domain=domain)
if status["summary"]["code"] in ("good", "great"):
raise YunohostError(
"certmanager_attempt_to_replace_valid_cert", domain=domain
)
operation_logger.start()
@ -185,13 +192,14 @@ def _certificate_install_selfsigned(domain_list, force=False):
for command in commands:
p = subprocess.Popen(
command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
out, _ = p.communicate()
if p.returncode != 0:
logger.warning(out)
raise YunohostError('domain_cert_gen_failed')
raise YunohostError("domain_cert_gen_failed")
else:
logger.debug(out)
@ -217,17 +225,27 @@ def _certificate_install_selfsigned(domain_list, force=False):
# Check new status indicate a recently created self-signed certificate
status = _get_status(domain)
if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648:
if (
status
and status["CA_type"]["code"] == "self-signed"
and status["validity"] > 3648
):
logger.success(
m18n.n("certmanager_cert_install_success_selfsigned", domain=domain))
m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)
)
operation_logger.success()
else:
msg = "Installation of self-signed certificate installation for %s failed !" % (domain)
msg = (
"Installation of self-signed certificate installation for %s failed !"
% (domain)
)
logger.error(msg)
operation_logger.error(msg)
def _certificate_install_letsencrypt(domain_list, force=False, no_checks=False, staging=False):
def _certificate_install_letsencrypt(
domain_list, force=False, no_checks=False, staging=False
):
import yunohost.domain
if not os.path.exists(ACCOUNT_KEY_FILE):
@ -236,7 +254,7 @@ def _certificate_install_letsencrypt(domain_list, force=False, no_checks=False,
# If no domains given, consider all yunohost domains with self-signed
# certificates
if domain_list == []:
for domain in yunohost.domain.domain_list()['domains']:
for domain in yunohost.domain.domain_list()["domains"]:
status = _get_status(domain)
if status["CA_type"]["code"] != "self-signed":
@ -247,18 +265,21 @@ def _certificate_install_letsencrypt(domain_list, force=False, no_checks=False,
# Else, validate that yunohost knows the domains given
else:
for domain in domain_list:
yunohost_domains_list = yunohost.domain.domain_list()['domains']
yunohost_domains_list = yunohost.domain.domain_list()["domains"]
if domain not in yunohost_domains_list:
raise YunohostError('domain_name_unknown', domain=domain)
raise YunohostError("domain_name_unknown", domain=domain)
# Is it self-signed?
status = _get_status(domain)
if not force and status["CA_type"]["code"] != "self-signed":
raise YunohostError('certmanager_domain_cert_not_selfsigned', domain=domain)
raise YunohostError(
"certmanager_domain_cert_not_selfsigned", domain=domain
)
if staging:
logger.warning(
"Please note that you used the --staging option, and that no new certificate will actually be enabled !")
"Please note that you used the --staging option, and that no new certificate will actually be enabled !"
)
# Actual install steps
for domain in domain_list:
@ -270,32 +291,40 @@ def _certificate_install_letsencrypt(domain_list, force=False, no_checks=False,
logger.error(e)
continue
logger.info(
"Now attempting install of certificate for domain %s!", domain)
logger.info("Now attempting install of certificate for domain %s!", domain)
operation_logger = OperationLogger('letsencrypt_cert_install', [('domain', domain)],
args={'force': force, 'no_checks': no_checks,
'staging': staging})
operation_logger = OperationLogger(
"letsencrypt_cert_install",
[("domain", domain)],
args={"force": force, "no_checks": no_checks, "staging": staging},
)
operation_logger.start()
try:
_fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks)
except Exception as e:
msg = "Certificate installation for %s failed !\nException: %s" % (domain, e)
msg = "Certificate installation for %s failed !\nException: %s" % (
domain,
e,
)
logger.error(msg)
operation_logger.error(msg)
if no_checks:
logger.error("Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s." % domain)
logger.error(
"Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s."
% domain
)
else:
_install_cron(no_checks=no_checks)
logger.success(
m18n.n("certmanager_cert_install_success", domain=domain))
logger.success(m18n.n("certmanager_cert_install_success", domain=domain))
operation_logger.success()
def certificate_renew(domain_list, force=False, no_checks=False, email=False, staging=False):
def certificate_renew(
domain_list, force=False, no_checks=False, email=False, staging=False
):
"""
Renew Let's Encrypt certificate for given domains (all by default)
@ -312,7 +341,7 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st
# If no domains given, consider all yunohost domains with Let's Encrypt
# certificates
if domain_list == []:
for domain in yunohost.domain.domain_list()['domains']:
for domain in yunohost.domain.domain_list()["domains"]:
# Does it have a Let's Encrypt cert?
status = _get_status(domain)
@ -325,8 +354,9 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st
# Check ACME challenge configured for given domain
if not _check_acme_challenge_configuration(domain):
logger.warning(m18n.n(
'certmanager_acme_not_configured_for_domain', domain=domain))
logger.warning(
m18n.n("certmanager_acme_not_configured_for_domain", domain=domain)
)
continue
domain_list.append(domain)
@ -339,26 +369,33 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st
for domain in domain_list:
# Is it in Yunohost dmomain list?
if domain not in yunohost.domain.domain_list()['domains']:
raise YunohostError('domain_name_unknown', domain=domain)
if domain not in yunohost.domain.domain_list()["domains"]:
raise YunohostError("domain_name_unknown", domain=domain)
status = _get_status(domain)
# Does it expire soon?
if status["validity"] > VALIDITY_LIMIT and not force:
raise YunohostError('certmanager_attempt_to_renew_valid_cert', domain=domain)
raise YunohostError(
"certmanager_attempt_to_renew_valid_cert", domain=domain
)
# Does it have a Let's Encrypt cert?
if status["CA_type"]["code"] != "lets-encrypt":
raise YunohostError('certmanager_attempt_to_renew_nonLE_cert', domain=domain)
raise YunohostError(
"certmanager_attempt_to_renew_nonLE_cert", domain=domain
)
# Check ACME challenge configured for given domain
if not _check_acme_challenge_configuration(domain):
raise YunohostError('certmanager_acme_not_configured_for_domain', domain=domain)
raise YunohostError(
"certmanager_acme_not_configured_for_domain", domain=domain
)
if staging:
logger.warning(
"Please note that you used the --staging option, and that no new certificate will actually be enabled !")
"Please note that you used the --staging option, and that no new certificate will actually be enabled !"
)
# Actual renew steps
for domain in domain_list:
@ -373,12 +410,18 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st
_email_renewing_failed(domain, e)
continue
logger.info(
"Now attempting renewing of certificate for domain %s !", domain)
logger.info("Now attempting renewing of certificate for domain %s !", domain)
operation_logger = OperationLogger('letsencrypt_cert_renew', [('domain', domain)],
args={'force': force, 'no_checks': no_checks,
'staging': staging, 'email': email})
operation_logger = OperationLogger(
"letsencrypt_cert_renew",
[("domain", domain)],
args={
"force": force,
"no_checks": no_checks,
"staging": staging,
"email": email,
},
)
operation_logger.start()
try:
@ -386,11 +429,15 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st
except Exception as e:
import traceback
from io import StringIO
stack = StringIO()
traceback.print_exc(file=stack)
msg = "Certificate renewing for %s failed !" % (domain)
if no_checks:
msg += "\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s." % domain
msg += (
"\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s."
% domain
)
logger.error(msg)
operation_logger.error(msg)
logger.error(stack.getvalue())
@ -398,12 +445,12 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st
if email:
logger.error("Sending email with details to root ...")
_email_renewing_failed(domain, msg + "\n" + e, stack.getvalue())
_email_renewing_failed(domain, msg + "\n" + str(e), stack.getvalue())
else:
logger.success(
m18n.n("certmanager_cert_renew_success", domain=domain))
logger.success(m18n.n("certmanager_cert_renew_success", domain=domain))
operation_logger.success()
#
# Back-end stuff #
#
@ -454,7 +501,12 @@ investigate :
-- Certificate Manager
""" % (domain, exception_message, stack, logs)
""" % (
domain,
exception_message,
stack,
logs,
)
message = """\
From: %s
@ -462,9 +514,15 @@ To: %s
Subject: %s
%s
""" % (from_, to_, subject_, text)
""" % (
from_,
to_,
subject_,
text,
)
import smtplib
smtp = smtplib.SMTP("localhost")
smtp.sendmail(from_, [to_], message)
smtp.quit()
@ -503,8 +561,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
_regen_dnsmasq_if_needed()
# Prepare certificate signing request
logger.debug(
"Prepare key and certificate signing request (CSR) for %s...", domain)
logger.debug("Prepare key and certificate signing request (CSR) for %s...", domain)
domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain)
_generate_key(domain_key_file)
@ -523,23 +580,25 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
certification_authority = PRODUCTION_CERTIFICATION_AUTHORITY
try:
signed_certificate = sign_certificate(ACCOUNT_KEY_FILE,
domain_csr_file,
WEBROOT_FOLDER,
log=logger,
disable_check=no_checks,
CA=certification_authority)
signed_certificate = sign_certificate(
ACCOUNT_KEY_FILE,
domain_csr_file,
WEBROOT_FOLDER,
log=logger,
disable_check=no_checks,
CA=certification_authority,
)
except ValueError as e:
if "urn:acme:error:rateLimited" in str(e):
raise YunohostError('certmanager_hit_rate_limit', domain=domain)
raise YunohostError("certmanager_hit_rate_limit", domain=domain)
else:
logger.error(str(e))
raise YunohostError('certmanager_cert_signing_failed')
raise YunohostError("certmanager_cert_signing_failed")
except Exception as e:
logger.error(str(e))
raise YunohostError('certmanager_cert_signing_failed')
raise YunohostError("certmanager_cert_signing_failed")
# Now save the key and signed certificate
logger.debug("Saving the key and signed certificate...")
@ -553,7 +612,11 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
folder_flag = "letsencrypt"
new_cert_folder = "%s/%s-history/%s-%s" % (
CERT_FOLDER, domain, date_tag, folder_flag)
CERT_FOLDER,
domain,
date_tag,
folder_flag,
)
os.makedirs(new_cert_folder)
@ -581,11 +644,14 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
status_summary = _get_status(domain)["summary"]
if status_summary["code"] != "great":
raise YunohostError('certmanager_certificate_fetching_or_enabling_failed', domain=domain)
raise YunohostError(
"certmanager_certificate_fetching_or_enabling_failed", domain=domain
)
def _prepare_certificate_signing_request(domain, key_file, output_folder):
from OpenSSL import crypto # lazy loading this module for performance reasons
# Init a request
csr = crypto.X509Req()
@ -593,17 +659,37 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder):
csr.get_subject().CN = domain
from yunohost.domain import domain_list
# For "parent" domains, include xmpp-upload subdomain in subject alternate names
if domain in domain_list(exclude_subdomains=True)["domains"]:
subdomain = "xmpp-upload." + domain
xmpp_records = Diagnoser.get_cached_report("dnsrecords", item={"domain": domain, "category": "xmpp"}).get("data") or {}
xmpp_records = (
Diagnoser.get_cached_report(
"dnsrecords", item={"domain": domain, "category": "xmpp"}
).get("data")
or {}
)
if xmpp_records.get("CNAME:xmpp-upload") == "OK":
csr.add_extensions([crypto.X509Extension("subjectAltName".encode('utf8'), False, ("DNS:" + subdomain).encode('utf8'))])
csr.add_extensions(
[
crypto.X509Extension(
"subjectAltName".encode("utf8"),
False,
("DNS:" + subdomain).encode("utf8"),
)
]
)
else:
logger.warning(m18n.n('certmanager_warning_subdomain_dns_record', subdomain=subdomain, domain=domain))
logger.warning(
m18n.n(
"certmanager_warning_subdomain_dns_record",
subdomain=subdomain,
domain=domain,
)
)
# Set the key
with open(key_file, 'rt') as f:
with open(key_file, "rt") as f:
key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
csr.set_pubkey(key)
@ -624,24 +710,32 @@ def _get_status(domain):
cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem")
if not os.path.isfile(cert_file):
raise YunohostError('certmanager_no_cert_file', domain=domain, file=cert_file)
raise YunohostError("certmanager_no_cert_file", domain=domain, file=cert_file)
from OpenSSL import crypto # lazy loading this module for performance reasons
try:
cert = crypto.load_certificate(
crypto.FILETYPE_PEM, open(cert_file).read())
cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read())
except Exception as exception:
import traceback
traceback.print_exc(file=sys.stdout)
raise YunohostError('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)
raise YunohostError(
"certmanager_cannot_read_cert",
domain=domain,
file=cert_file,
reason=exception,
)
cert_subject = cert.get_subject().CN
cert_issuer = cert.get_issuer().CN
organization_name = cert.get_issuer().O
valid_up_to = datetime.strptime(cert.get_notAfter().decode('utf-8'), "%Y%m%d%H%M%SZ")
valid_up_to = datetime.strptime(
cert.get_notAfter().decode("utf-8"), "%Y%m%d%H%M%SZ"
)
days_remaining = (valid_up_to - datetime.utcnow()).days
if cert_issuer == _name_self_CA():
if cert_issuer == "yunohost.org" or cert_issuer == _name_self_CA():
CA_type = {
"code": "self-signed",
"verbose": "Self-signed",
@ -710,6 +804,7 @@ def _get_status(domain):
"summary": status_summary,
}
#
# Misc small stuff ... #
#
@ -723,12 +818,14 @@ def _generate_account_key():
def _generate_key(destination_path):
from OpenSSL import crypto # lazy loading this module for performance reasons
k = crypto.PKey()
k.generate_key(crypto.TYPE_RSA, KEY_SIZE)
with open(destination_path, "wb") as f:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
def _set_permissions(path, user, group, permissions):
uid = pwd.getpwnam(user).pw_uid
gid = grp.getgrnam(group).gr_gid
@ -760,15 +857,16 @@ def _enable_certificate(domain, new_cert_folder):
for service in ("postfix", "dovecot", "metronome"):
_run_service_command("restart", service)
if os.path.isfile('/etc/yunohost/installed'):
if os.path.isfile("/etc/yunohost/installed"):
# regen nginx conf to be sure it integrates OCSP Stapling
# (We don't do this yet if postinstall is not finished yet)
regen_conf(names=['nginx'])
regen_conf(names=["nginx"])
_run_service_command("reload", "nginx")
from yunohost.hook import hook_callback
hook_callback('post_cert_update', args=[domain])
hook_callback("post_cert_update", args=[domain])
def _backup_current_cert(domain):
@ -784,19 +882,36 @@ def _backup_current_cert(domain):
def _check_domain_is_ready_for_ACME(domain):
dnsrecords = Diagnoser.get_cached_report("dnsrecords", item={"domain": domain, "category": "basic"}, warn_if_no_cache=False) or {}
httpreachable = Diagnoser.get_cached_report("web", item={"domain": domain}, warn_if_no_cache=False) or {}
dnsrecords = (
Diagnoser.get_cached_report(
"dnsrecords",
item={"domain": domain, "category": "basic"},
warn_if_no_cache=False,
)
or {}
)
httpreachable = (
Diagnoser.get_cached_report(
"web", item={"domain": domain}, warn_if_no_cache=False
)
or {}
)
if not dnsrecords or not httpreachable:
raise YunohostError('certmanager_domain_not_diagnosed_yet', domain=domain)
raise YunohostError("certmanager_domain_not_diagnosed_yet", domain=domain)
# Check if IP from DNS matches public IP
if not dnsrecords.get("status") in ["SUCCESS", "WARNING"]: # Warning is for missing IPv6 record which ain't critical for ACME
raise YunohostError('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)
if not dnsrecords.get("status") in [
"SUCCESS",
"WARNING",
]: # Warning is for missing IPv6 record which ain't critical for ACME
raise YunohostError(
"certmanager_domain_dns_ip_differs_from_public_ip", domain=domain
)
# Check if domain seems to be accessible through HTTP?
if not httpreachable.get("status") == "SUCCESS":
raise YunohostError('certmanager_domain_http_not_working', domain=domain)
raise YunohostError("certmanager_domain_http_not_working", domain=domain)
# FIXME / TODO : ideally this should not be needed. There should be a proper
@ -840,7 +955,7 @@ def _name_self_CA():
ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf")
if not os.path.exists(ca_conf):
logger.warning(m18n.n('certmanager_self_ca_conf_file_not_found', file=ca_conf))
logger.warning(m18n.n("certmanager_self_ca_conf_file_not_found", file=ca_conf))
return ""
with open(ca_conf) as f:
@ -850,7 +965,7 @@ def _name_self_CA():
if line.startswith("commonName_default"):
return line.split()[2]
logger.warning(m18n.n('certmanager_unable_to_parse_self_CA_name', file=ca_conf))
logger.warning(m18n.n("certmanager_unable_to_parse_self_CA_name", file=ca_conf))
return ""

View file

@ -1,4 +1,3 @@
import glob
import os
@ -12,9 +11,12 @@ from yunohost.tools import Migration, tools_update, tools_upgrade
from yunohost.app import unstable_apps
from yunohost.regenconf import manually_modified_files
from yunohost.utils.filesystem import free_space_in_directory
from yunohost.utils.packages import get_ynh_package_version, _list_upgradable_apt_packages
from yunohost.utils.packages import (
get_ynh_package_version,
_list_upgradable_apt_packages,
)
logger = getActionLogger('yunohost.migration')
logger = getActionLogger("yunohost.migration")
class MyMigration(Migration):
@ -44,10 +46,14 @@ class MyMigration(Migration):
tools_update(system=True)
# Tell libc6 it's okay to restart system stuff during the upgrade
os.system("echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections")
os.system(
"echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections"
)
# Don't send an email to root about the postgresql migration. It should be handled automatically after.
os.system("echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections")
os.system(
"echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections"
)
#
# Specific packages upgrades
@ -56,16 +62,22 @@ class MyMigration(Migration):
# Update unscd independently, was 0.53-1+yunohost on stretch (custom build of ours) but now it's 0.53-1+b1 on vanilla buster,
# which for apt appears as a lower version (hence the --allow-downgrades and the hardcoded version number)
unscd_version = check_output('dpkg -s unscd | grep "^Version: " | cut -d " " -f 2')
unscd_version = check_output(
'dpkg -s unscd | grep "^Version: " | cut -d " " -f 2'
)
if "yunohost" in unscd_version:
new_version = check_output("LC_ALL=C apt policy unscd 2>/dev/null | grep -v '\\*\\*\\*' | grep http -B1 | head -n 1 | awk '{print $1}'").strip()
new_version = check_output(
"LC_ALL=C apt policy unscd 2>/dev/null | grep -v '\\*\\*\\*' | grep http -B1 | head -n 1 | awk '{print $1}'"
).strip()
if new_version:
self.apt_install('unscd=%s --allow-downgrades' % new_version)
self.apt_install("unscd=%s --allow-downgrades" % new_version)
else:
logger.warning("Could not identify which version of unscd to install")
# Upgrade libpam-modules independently, small issue related to willing to overwrite a file previously provided by Yunohost
libpammodules_version = check_output('dpkg -s libpam-modules | grep "^Version: " | cut -d " " -f 2')
libpammodules_version = check_output(
'dpkg -s libpam-modules | grep "^Version: " | cut -d " " -f 2'
)
if not libpammodules_version.startswith("1.3"):
self.apt_install('libpam-modules -o Dpkg::Options::="--force-overwrite"')
@ -100,10 +112,14 @@ class MyMigration(Migration):
# with /etc/lsb-release for instance -_-)
# Instead, we rely on /etc/os-release which should be the raw info from
# the distribution...
return int(check_output("grep VERSION_ID /etc/os-release | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2"))
return int(
check_output(
"grep VERSION_ID /etc/os-release | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2"
)
)
def yunohost_major_version(self):
return int(get_ynh_package_version("yunohost")["version"].split('.')[0])
return int(get_ynh_package_version("yunohost")["version"].split(".")[0])
def check_assertions(self):
@ -111,12 +127,14 @@ class MyMigration(Migration):
# NB : we do both check to cover situations where the upgrade crashed
# in the middle and debian version could be > 9.x but yunohost package
# would still be in 3.x...
if not self.debian_major_version() == 9 \
and not self.yunohost_major_version() == 3:
if (
not self.debian_major_version() == 9
and not self.yunohost_major_version() == 3
):
raise YunohostError("migration_0015_not_stretch")
# 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 YunohostError("migration_0015_not_enough_free_space")
# Check system is up to date
@ -136,8 +154,10 @@ class MyMigration(Migration):
# NB : we do both check to cover situations where the upgrade crashed
# in the middle and debian version could be >= 10.x but yunohost package
# would still be in 3.x...
if not self.debian_major_version() == 9 \
and not self.yunohost_major_version() == 3:
if (
not self.debian_major_version() == 9
and not self.yunohost_major_version() == 3
):
return None
# Get list of problematic apps ? I.e. not official or community+working
@ -150,13 +170,21 @@ class MyMigration(Migration):
message = m18n.n("migration_0015_general_warning")
message = "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/12195\n\n" + message
message = (
"N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/12195\n\n"
+ message
)
if problematic_apps:
message += "\n\n" + m18n.n("migration_0015_problematic_apps_warning", problematic_apps=problematic_apps)
message += "\n\n" + m18n.n(
"migration_0015_problematic_apps_warning",
problematic_apps=problematic_apps,
)
if modified_files:
message += "\n\n" + m18n.n("migration_0015_modified_files", manually_modified_files=modified_files)
message += "\n\n" + m18n.n(
"migration_0015_modified_files", manually_modified_files=modified_files
)
return message
@ -170,23 +198,27 @@ class MyMigration(Migration):
# - comments lines containing "backports"
# - replace 'stretch/updates' by 'strech/updates' (or same with -)
for f in sources_list:
command = "sed -i -e 's@ stretch @ buster @g' " \
"-e '/backports/ s@^#*@#@' " \
"-e 's@ stretch/updates @ buster/updates @g' " \
"-e 's@ stretch-@ buster-@g' " \
"{}".format(f)
command = (
"sed -i -e 's@ stretch @ buster @g' "
"-e '/backports/ s@^#*@#@' "
"-e 's@ stretch/updates @ buster/updates @g' "
"-e 's@ stretch-@ buster-@g' "
"{}".format(f)
)
os.system(command)
def get_apps_equivs_packages(self):
command = "dpkg --get-selections" \
" | grep -v deinstall" \
" | awk '{print $1}'" \
" | { grep 'ynh-deps$' || true; }"
command = (
"dpkg --get-selections"
" | grep -v deinstall"
" | awk '{print $1}'"
" | { grep 'ynh-deps$' || true; }"
)
output = check_output(command)
return output.split('\n') if output else []
return output.split("\n") if output else []
def hold(self, packages):
for package in packages:
@ -197,16 +229,20 @@ class MyMigration(Migration):
os.system("apt-mark unhold {}".format(package))
def apt_install(self, cmd):
def is_relevant(l):
return "Reading database ..." not in l.rstrip()
def is_relevant(line):
return "Reading database ..." not in line.rstrip()
callbacks = (
lambda l: logger.info("+ " + l.rstrip() + "\r") if is_relevant(l) else logger.debug(l.rstrip() + "\r"),
lambda l: logger.info("+ " + l.rstrip() + "\r")
if is_relevant(l)
else logger.debug(l.rstrip() + "\r"),
lambda l: logger.warning(l.rstrip()),
)
cmd = "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes " + cmd
cmd = (
"LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes "
+ cmd
)
logger.debug("Running: %s" % cmd)
@ -214,15 +250,24 @@ class MyMigration(Migration):
def validate_and_upgrade_cert_if_necessary(self):
active_certs = set(check_output("grep -roh '/.*crt.pem' /etc/nginx/").split("\n"))
active_certs = set(
check_output("grep -roh '/.*crt.pem' /etc/nginx/").split("\n")
)
cmd = "LC_ALL=C openssl x509 -in %s -text -noout | grep -i 'Signature Algorithm:' | awk '{print $3}' | uniq"
default_crt = '/etc/yunohost/certs/yunohost.org/crt.pem'
default_key = '/etc/yunohost/certs/yunohost.org/key.pem'
default_signature = check_output(cmd % default_crt) if default_crt in active_certs else None
if default_signature is not None and (default_signature.startswith("md5") or default_signature.startswith("sha1")):
logger.warning("%s is using a pretty old certificate incompatible with newer versions of nginx ... attempting to regenerate a fresh one" % default_crt)
default_crt = "/etc/yunohost/certs/yunohost.org/crt.pem"
default_key = "/etc/yunohost/certs/yunohost.org/key.pem"
default_signature = (
check_output(cmd % default_crt) if default_crt in active_certs else None
)
if default_signature is not None and (
default_signature.startswith("md5") or default_signature.startswith("sha1")
):
logger.warning(
"%s is using a pretty old certificate incompatible with newer versions of nginx ... attempting to regenerate a fresh one"
% default_crt
)
os.system("mv %s %s.old" % (default_crt, default_crt))
os.system("mv %s %s.old" % (default_key, default_key))
@ -241,4 +286,6 @@ class MyMigration(Migration):
weak_certs = [cert for cert in signatures.keys() if cert_is_weak(cert)]
if weak_certs:
raise YunohostError("migration_0015_weak_certs", certs=", ".join(weak_certs))
raise YunohostError(
"migration_0015_weak_certs", certs=", ".join(weak_certs)
)

View file

@ -8,7 +8,7 @@ from yunohost.app import _is_installed, _patch_legacy_php_versions_in_settings
from yunohost.tools import Migration
from yunohost.service import _run_service_command
logger = getActionLogger('yunohost.migration')
logger = getActionLogger("yunohost.migration")
PHP70_POOLS = "/etc/php/7.0/fpm/pool.d"
PHP73_POOLS = "/etc/php/7.3/fpm/pool.d"
@ -16,7 +16,9 @@ PHP73_POOLS = "/etc/php/7.3/fpm/pool.d"
PHP70_SOCKETS_PREFIX = "/run/php/php7.0-fpm"
PHP73_SOCKETS_PREFIX = "/run/php/php7.3-fpm"
MIGRATION_COMMENT = "; YunoHost note : this file was automatically moved from {}".format(PHP70_POOLS)
MIGRATION_COMMENT = (
"; YunoHost note : this file was automatically moved from {}".format(PHP70_POOLS)
)
class MyMigration(Migration):
@ -43,7 +45,9 @@ class MyMigration(Migration):
copy2(src, dest)
# Replace the socket prefix if it's found
c = "sed -i -e 's@{}@{}@g' {}".format(PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, dest)
c = "sed -i -e 's@{}@{}@g' {}".format(
PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, dest
)
os.system(c)
# Also add a comment that it was automatically moved from php7.0
@ -51,17 +55,23 @@ class MyMigration(Migration):
c = "sed -i '1i {}' {}".format(MIGRATION_COMMENT, dest)
os.system(c)
app_id = os.path.basename(f)[:-len(".conf")]
app_id = os.path.basename(f)[: -len(".conf")]
if _is_installed(app_id):
_patch_legacy_php_versions_in_settings("/etc/yunohost/apps/%s/" % app_id)
_patch_legacy_php_versions_in_settings(
"/etc/yunohost/apps/%s/" % app_id
)
nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/%s.conf" % app_id)
for f in nginx_conf_files:
# Replace the socket prefix if it's found
c = "sed -i -e 's@{}@{}@g' {}".format(PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, f)
c = "sed -i -e 's@{}@{}@g' {}".format(
PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, f
)
os.system(c)
os.system("rm /etc/logrotate.d/php7.0-fpm") # We remove this otherwise the logrotate cron will be unhappy
os.system(
"rm /etc/logrotate.d/php7.0-fpm"
) # We remove this otherwise the logrotate cron will be unhappy
# Reload/restart the php pools
_run_service_command("restart", "php7.3-fpm")

View file

@ -7,7 +7,7 @@ from moulinette.utils.log import getActionLogger
from yunohost.tools import Migration
from yunohost.utils.filesystem import free_space_in_directory, space_used_by_directory
logger = getActionLogger('yunohost.migration')
logger = getActionLogger("yunohost.migration")
class MyMigration(Migration):
@ -29,37 +29,54 @@ class MyMigration(Migration):
try:
self.runcmd("pg_lsclusters | grep -q '^9.6 '")
except Exception:
logger.warning("It looks like there's not active 9.6 cluster, so probably don't need to run this migration")
logger.warning(
"It looks like there's not active 9.6 cluster, so probably don't need to run this migration"
)
return
if not space_used_by_directory("/var/lib/postgresql/9.6") > free_space_in_directory("/var/lib/postgresql"):
raise YunohostError("migration_0017_not_enough_space", path="/var/lib/postgresql/")
if not space_used_by_directory(
"/var/lib/postgresql/9.6"
) > free_space_in_directory("/var/lib/postgresql"):
raise YunohostError(
"migration_0017_not_enough_space", path="/var/lib/postgresql/"
)
self.runcmd("systemctl stop postgresql")
self.runcmd("LC_ALL=C pg_dropcluster --stop 11 main || true") # We do not trigger an exception if the command fails because that probably means cluster 11 doesn't exists, which is fine because it's created during the pg_upgradecluster)
self.runcmd(
"LC_ALL=C pg_dropcluster --stop 11 main || true"
) # We do not trigger an exception if the command fails because that probably means cluster 11 doesn't exists, which is fine because it's created during the pg_upgradecluster)
self.runcmd("LC_ALL=C pg_upgradecluster -m upgrade 9.6 main")
self.runcmd("LC_ALL=C pg_dropcluster --stop 9.6 main")
self.runcmd("systemctl start postgresql")
def package_is_installed(self, package_name):
(returncode, out, err) = self.runcmd("dpkg --list | grep '^ii ' | grep -q -w {}".format(package_name), raise_on_errors=False)
(returncode, out, err) = self.runcmd(
"dpkg --list | grep '^ii ' | grep -q -w {}".format(package_name),
raise_on_errors=False,
)
return returncode == 0
def runcmd(self, cmd, raise_on_errors=True):
logger.debug("Running command: " + cmd)
p = subprocess.Popen(cmd,
shell=True,
executable='/bin/bash',
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
p = subprocess.Popen(
cmd,
shell=True,
executable="/bin/bash",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = p.communicate()
returncode = p.returncode
if raise_on_errors and returncode != 0:
raise YunohostError("Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format(cmd, returncode, out, err))
raise YunohostError(
"Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format(
cmd, returncode, out, err
)
)
out = out.strip().split("\n")
return (returncode, out, err)

View file

@ -9,7 +9,7 @@ from yunohost.firewall import firewall_reload
from yunohost.service import service_restart
from yunohost.tools import Migration
logger = getActionLogger('yunohost.migration')
logger = getActionLogger("yunohost.migration")
class MyMigration(Migration):
@ -24,9 +24,9 @@ class MyMigration(Migration):
self.do_ipv6 = os.system("ip6tables -w -L >/dev/null") == 0
if not self.do_ipv4:
logger.warning(m18n.n('iptables_unavailable'))
logger.warning(m18n.n("iptables_unavailable"))
if not self.do_ipv6:
logger.warning(m18n.n('ip6tables_unavailable'))
logger.warning(m18n.n("ip6tables_unavailable"))
backup_folder = "/home/yunohost.backup/premigration/xtable_to_nftable/"
if not os.path.exists(backup_folder):
@ -36,13 +36,21 @@ class MyMigration(Migration):
# Backup existing legacy rules to be able to rollback
if self.do_ipv4 and not os.path.exists(self.backup_rules_ipv4):
self.runcmd("iptables-legacy -L >/dev/null") # For some reason if we don't do this, iptables-legacy-save is empty ?
self.runcmd(
"iptables-legacy -L >/dev/null"
) # For some reason if we don't do this, iptables-legacy-save is empty ?
self.runcmd("iptables-legacy-save > %s" % self.backup_rules_ipv4)
assert open(self.backup_rules_ipv4).read().strip(), "Uhoh backup of legacy ipv4 rules is empty !?"
assert (
open(self.backup_rules_ipv4).read().strip()
), "Uhoh backup of legacy ipv4 rules is empty !?"
if self.do_ipv6 and not os.path.exists(self.backup_rules_ipv6):
self.runcmd("ip6tables-legacy -L >/dev/null") # For some reason if we don't do this, iptables-legacy-save is empty ?
self.runcmd(
"ip6tables-legacy -L >/dev/null"
) # For some reason if we don't do this, iptables-legacy-save is empty ?
self.runcmd("ip6tables-legacy-save > %s" % self.backup_rules_ipv6)
assert open(self.backup_rules_ipv6).read().strip(), "Uhoh backup of legacy ipv6 rules is empty !?"
assert (
open(self.backup_rules_ipv6).read().strip()
), "Uhoh backup of legacy ipv6 rules is empty !?"
# We inject the legacy rules (iptables-legacy) into the new iptable (just "iptables")
try:
@ -52,23 +60,27 @@ class MyMigration(Migration):
self.runcmd("ip6tables-legacy-save | ip6tables-restore")
except Exception as e:
self.rollback()
raise YunohostError("migration_0018_failed_to_migrate_iptables_rules", error=e)
raise YunohostError(
"migration_0018_failed_to_migrate_iptables_rules", error=e
)
# Reset everything in iptables-legacy
# Stolen from https://serverfault.com/a/200642
try:
if self.do_ipv4:
self.runcmd(
"iptables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat
" /^:[A-Z]+ [^-]/ { print $1 \" ACCEPT\" ; }" # Turn all policies to accept
" /COMMIT/ { print $0; }'" # Keep the line COMMIT
" | iptables-legacy-restore")
"iptables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat
' /^:[A-Z]+ [^-]/ { print $1 " ACCEPT" ; }' # Turn all policies to accept
" /COMMIT/ { print $0; }'" # Keep the line COMMIT
" | iptables-legacy-restore"
)
if self.do_ipv6:
self.runcmd(
"ip6tables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat
" /^:[A-Z]+ [^-]/ { print $1 \" ACCEPT\" ; }" # Turn all policies to accept
" /COMMIT/ { print $0; }'" # Keep the line COMMIT
" | ip6tables-legacy-restore")
"ip6tables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat
' /^:[A-Z]+ [^-]/ { print $1 " ACCEPT" ; }' # Turn all policies to accept
" /COMMIT/ { print $0; }'" # Keep the line COMMIT
" | ip6tables-legacy-restore"
)
except Exception as e:
self.rollback()
raise YunohostError("migration_0018_failed_to_reset_legacy_rules", error=e)
@ -93,16 +105,22 @@ class MyMigration(Migration):
logger.debug("Running command: " + cmd)
p = subprocess.Popen(cmd,
shell=True,
executable='/bin/bash',
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
p = subprocess.Popen(
cmd,
shell=True,
executable="/bin/bash",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = p.communicate()
returncode = p.returncode
if raise_on_errors and returncode != 0:
raise YunohostError("Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format(cmd, returncode, out, err))
raise YunohostError(
"Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format(
cmd, returncode, out, err
)
)
out = out.strip().split("\n")
return (returncode, out, err)

View file

@ -9,12 +9,12 @@ from yunohost.tools import Migration
from yunohost.permission import user_permission_list
from yunohost.utils.legacy import migrate_legacy_permission_settings
logger = getActionLogger('yunohost.migration')
logger = getActionLogger("yunohost.migration")
class MyMigration(Migration):
"""
Add protected attribute in LDAP permission
Add protected attribute in LDAP permission
"""
required = True
@ -25,14 +25,19 @@ class MyMigration(Migration):
from yunohost.regenconf import regen_conf, BACKUP_CONF_DIR
# Check if the migration can be processed
ldap_regen_conf_status = regen_conf(names=['slapd'], dry_run=True)
ldap_regen_conf_status = regen_conf(names=["slapd"], dry_run=True)
# By this we check if the have been customized
if ldap_regen_conf_status and ldap_regen_conf_status['slapd']['pending']:
logger.warning(m18n.n("migration_0019_slapd_config_will_be_overwritten", conf_backup_folder=BACKUP_CONF_DIR))
if ldap_regen_conf_status and ldap_regen_conf_status["slapd"]["pending"]:
logger.warning(
m18n.n(
"migration_0019_slapd_config_will_be_overwritten",
conf_backup_folder=BACKUP_CONF_DIR,
)
)
# Update LDAP schema restart slapd
logger.info(m18n.n("migration_0011_update_LDAP_schema"))
regen_conf(names=['slapd'], force=True)
regen_conf(names=["slapd"], force=True)
logger.info(m18n.n("migration_0019_add_new_attributes_in_ldap"))
ldap = _get_ldap_interface()
@ -43,33 +48,35 @@ class MyMigration(Migration):
"mail": "E-mail",
"xmpp": "XMPP",
"ssh": "SSH",
"sftp": "STFP"
"sftp": "STFP",
}
if permission.split('.')[0] in system_perms:
if permission.split(".")[0] in system_perms:
update = {
'authHeader': ["FALSE"],
'label': [system_perms[permission.split('.')[0]]],
'showTile': ["FALSE"],
'isProtected': ["TRUE"],
"authHeader": ["FALSE"],
"label": [system_perms[permission.split(".")[0]]],
"showTile": ["FALSE"],
"isProtected": ["TRUE"],
}
else:
app, subperm_name = permission.split('.')
app, subperm_name = permission.split(".")
if permission.endswith(".main"):
update = {
'authHeader': ["TRUE"],
'label': [app], # Note that this is later re-changed during the call to migrate_legacy_permission_settings() if a 'label' setting exists
'showTile': ["TRUE"],
'isProtected': ["FALSE"]
"authHeader": ["TRUE"],
"label": [
app
], # Note that this is later re-changed during the call to migrate_legacy_permission_settings() if a 'label' setting exists
"showTile": ["TRUE"],
"isProtected": ["FALSE"],
}
else:
update = {
'authHeader': ["TRUE"],
'label': [subperm_name.title()],
'showTile': ["FALSE"],
'isProtected': ["TRUE"]
"authHeader": ["TRUE"],
"label": [subperm_name.title()],
"showTile": ["FALSE"],
"isProtected": ["TRUE"],
}
ldap.update('cn=%s,ou=permission' % permission, update)
ldap.update("cn=%s,ou=permission" % permission, update)
def run(self):
@ -80,14 +87,20 @@ class MyMigration(Migration):
# Backup LDAP and the apps settings before to do the migration
logger.info(m18n.n("migration_0019_backup_before_migration"))
try:
backup_folder = "/home/yunohost.backup/premigration/" + time.strftime('%Y%m%d-%H%M%S', time.gmtime())
backup_folder = "/home/yunohost.backup/premigration/" + time.strftime(
"%Y%m%d-%H%M%S", time.gmtime()
)
os.makedirs(backup_folder, 0o750)
os.system("systemctl stop slapd")
os.system("cp -r --preserve /etc/ldap %s/ldap_config" % backup_folder)
os.system("cp -r --preserve /var/lib/ldap %s/ldap_db" % backup_folder)
os.system("cp -r --preserve /etc/yunohost/apps %s/apps_settings" % backup_folder)
os.system(
"cp -r --preserve /etc/yunohost/apps %s/apps_settings" % backup_folder
)
except Exception as e:
raise YunohostError("migration_0019_can_not_backup_before_migration", error=e)
raise YunohostError(
"migration_0019_can_not_backup_before_migration", error=e
)
finally:
os.system("systemctl start slapd")
@ -101,10 +114,15 @@ class MyMigration(Migration):
except Exception:
logger.warn(m18n.n("migration_0019_migration_failed_trying_to_rollback"))
os.system("systemctl stop slapd")
os.system("rm -r /etc/ldap/slapd.d") # To be sure that we don't keep some part of the old config
os.system(
"rm -r /etc/ldap/slapd.d"
) # To be sure that we don't keep some part of the old config
os.system("cp -r --preserve %s/ldap_config/. /etc/ldap/" % backup_folder)
os.system("cp -r --preserve %s/ldap_db/. /var/lib/ldap/" % backup_folder)
os.system("cp -r --preserve %s/apps_settings/. /etc/yunohost/apps/" % backup_folder)
os.system(
"cp -r --preserve %s/apps_settings/. /etc/yunohost/apps/"
% backup_folder
)
os.system("systemctl start slapd")
os.system("rm -r " + backup_folder)
logger.info(m18n.n("migration_0019_rollback_success"))

View file

@ -30,15 +30,20 @@ import time
from moulinette import m18n, msettings
from moulinette.utils import log
from moulinette.utils.filesystem import read_json, write_to_json, read_yaml, write_to_yaml
from moulinette.utils.filesystem import (
read_json,
write_to_json,
read_yaml,
write_to_yaml,
)
from yunohost.utils.error import YunohostError
from yunohost.hook import hook_list, hook_exec
logger = log.getActionLogger('yunohost.diagnosis')
logger = log.getActionLogger("yunohost.diagnosis")
DIAGNOSIS_CACHE = "/var/cache/yunohost/diagnosis/"
DIAGNOSIS_CONFIG_FILE = '/etc/yunohost/diagnosis.yml'
DIAGNOSIS_CONFIG_FILE = "/etc/yunohost/diagnosis.yml"
DIAGNOSIS_SERVER = "diagnosis.yunohost.org"
@ -54,11 +59,13 @@ def diagnosis_get(category, item):
all_categories_names = [c for c, _ in all_categories]
if category not in all_categories_names:
raise YunohostError('diagnosis_unknown_categories', categories=category)
raise YunohostError("diagnosis_unknown_categories", categories=category)
if isinstance(item, list):
if any("=" not in criteria for criteria in item):
raise YunohostError("Criterias should be of the form key=value (e.g. domain=yolo.test)")
raise YunohostError(
"Criterias should be of the form key=value (e.g. domain=yolo.test)"
)
# Convert the provided criteria into a nice dict
item = {c.split("=")[0]: c.split("=")[1] for c in item}
@ -66,7 +73,9 @@ def diagnosis_get(category, item):
return Diagnoser.get_cached_report(category, item=item)
def diagnosis_show(categories=[], issues=False, full=False, share=False, human_readable=False):
def diagnosis_show(
categories=[], issues=False, full=False, share=False, human_readable=False
):
if not os.path.exists(DIAGNOSIS_CACHE):
logger.warning(m18n.n("diagnosis_never_ran_yet"))
@ -82,7 +91,9 @@ def diagnosis_show(categories=[], issues=False, full=False, share=False, human_r
else:
unknown_categories = [c for c in categories if c not in all_categories_names]
if unknown_categories:
raise YunohostError('diagnosis_unknown_categories', categories=", ".join(unknown_categories))
raise YunohostError(
"diagnosis_unknown_categories", categories=", ".join(unknown_categories)
)
# Fetch all reports
all_reports = []
@ -107,7 +118,11 @@ def diagnosis_show(categories=[], issues=False, full=False, share=False, human_r
if "data" in item:
del item["data"]
if issues:
report["items"] = [item for item in report["items"] if item["status"] in ["WARNING", "ERROR"]]
report["items"] = [
item
for item in report["items"]
if item["status"] in ["WARNING", "ERROR"]
]
# Ignore this category if no issue was found
if not report["items"]:
continue
@ -116,11 +131,12 @@ def diagnosis_show(categories=[], issues=False, full=False, share=False, human_r
if share:
from yunohost.utils.yunopaste import yunopaste
content = _dump_human_readable_reports(all_reports)
url = yunopaste(content)
logger.info(m18n.n("log_available_on_yunopaste", url=url))
if msettings.get('interface') == 'api':
if msettings.get("interface") == "api":
return {"url": url}
else:
return
@ -145,10 +161,12 @@ def _dump_human_readable_reports(reports):
output += "\n"
output += "\n\n"
return(output)
return output
def diagnosis_run(categories=[], force=False, except_if_never_ran_yet=False, email=False):
def diagnosis_run(
categories=[], force=False, except_if_never_ran_yet=False, email=False
):
if (email or except_if_never_ran_yet) and not os.path.exists(DIAGNOSIS_CACHE):
return
@ -163,7 +181,9 @@ def diagnosis_run(categories=[], force=False, except_if_never_ran_yet=False, ema
else:
unknown_categories = [c for c in categories if c not in all_categories_names]
if unknown_categories:
raise YunohostError('diagnosis_unknown_categories', categories=", ".join(unknown_categories))
raise YunohostError(
"diagnosis_unknown_categories", categories=", ".join(unknown_categories)
)
issues = []
# Call the hook ...
@ -176,11 +196,24 @@ def diagnosis_run(categories=[], force=False, except_if_never_ran_yet=False, ema
code, report = hook_exec(path, args={"force": force}, env=None)
except Exception:
import traceback
logger.error(m18n.n("diagnosis_failed_for_category", category=category, error='\n' + traceback.format_exc()))
logger.error(
m18n.n(
"diagnosis_failed_for_category",
category=category,
error="\n" + traceback.format_exc(),
)
)
else:
diagnosed_categories.append(category)
if report != {}:
issues.extend([item for item in report["items"] if item["status"] in ["WARNING", "ERROR"]])
issues.extend(
[
item
for item in report["items"]
if item["status"] in ["WARNING", "ERROR"]
]
)
if email:
_email_diagnosis_issues()
@ -237,12 +270,16 @@ def diagnosis_ignore(add_filter=None, remove_filter=None, list=False):
# Sanity checks for the provided arguments
if len(filter_) == 0:
raise YunohostError("You should provide at least one criteria being the diagnosis category to ignore")
raise YunohostError(
"You should provide at least one criteria being the diagnosis category to ignore"
)
category = filter_[0]
if category not in all_categories_names:
raise YunohostError("%s is not a diagnosis category" % category)
if any("=" not in criteria for criteria in filter_[1:]):
raise YunohostError("Criterias should be of the form key=value (e.g. domain=yolo.test)")
raise YunohostError(
"Criterias should be of the form key=value (e.g. domain=yolo.test)"
)
# Convert the provided criteria into a nice dict
criterias = {c.split("=")[0]: c.split("=")[1] for c in filter_[1:]}
@ -254,11 +291,18 @@ def diagnosis_ignore(add_filter=None, remove_filter=None, list=False):
category, criterias = validate_filter_criterias(add_filter)
# Fetch current issues for the requested category
current_issues_for_this_category = diagnosis_show(categories=[category], issues=True, full=True)
current_issues_for_this_category = current_issues_for_this_category["reports"][0].get("items", {})
current_issues_for_this_category = diagnosis_show(
categories=[category], issues=True, full=True
)
current_issues_for_this_category = current_issues_for_this_category["reports"][
0
].get("items", {})
# Accept the given filter only if the criteria effectively match an existing issue
if not any(issue_matches_criterias(i, criterias) for i in current_issues_for_this_category):
if not any(
issue_matches_criterias(i, criterias)
for i in current_issues_for_this_category
):
raise YunohostError("No issues was found matching the given criteria.")
# Make sure the subdicts/lists exists
@ -332,7 +376,9 @@ def add_ignore_flag_to_issues(report):
every item in the report
"""
ignore_filters = _diagnosis_read_configuration().get("ignore_filters", {}).get(report["id"], [])
ignore_filters = (
_diagnosis_read_configuration().get("ignore_filters", {}).get(report["id"], [])
)
for report_item in report["items"]:
report_item["ignored"] = False
@ -347,8 +393,7 @@ def add_ignore_flag_to_issues(report):
############################################################
class Diagnoser():
class Diagnoser:
def __init__(self, args, env, loggers):
# FIXME ? That stuff with custom loggers is weird ... (mainly inherited from the bash hooks, idk)
@ -371,9 +416,14 @@ class Diagnoser():
def diagnose(self):
if not self.args.get("force", False) and self.cached_time_ago() < self.cache_duration:
if (
not self.args.get("force", False)
and self.cached_time_ago() < self.cache_duration
):
self.logger_debug("Cache still valid : %s" % self.cache_file)
logger.info(m18n.n("diagnosis_cache_still_valid", category=self.description))
logger.info(
m18n.n("diagnosis_cache_still_valid", category=self.description)
)
return 0, {}
for dependency in self.dependencies:
@ -382,10 +432,18 @@ class Diagnoser():
if dep_report["timestamp"] == -1: # No cache yet for this dep
dep_errors = True
else:
dep_errors = [item for item in dep_report["items"] if item["status"] == "ERROR"]
dep_errors = [
item for item in dep_report["items"] if item["status"] == "ERROR"
]
if dep_errors:
logger.error(m18n.n("diagnosis_cant_run_because_of_dep", category=self.description, dep=Diagnoser.get_description(dependency)))
logger.error(
m18n.n(
"diagnosis_cant_run_because_of_dep",
category=self.description,
dep=Diagnoser.get_description(dependency),
)
)
return 1, {}
items = list(self.run())
@ -394,29 +452,76 @@ class Diagnoser():
if "details" in item and not item["details"]:
del item["details"]
new_report = {"id": self.id_,
"cached_for": self.cache_duration,
"items": items}
new_report = {"id": self.id_, "cached_for": self.cache_duration, "items": items}
self.logger_debug("Updating cache %s" % self.cache_file)
self.write_cache(new_report)
Diagnoser.i18n(new_report)
add_ignore_flag_to_issues(new_report)
errors = [item for item in new_report["items"] if item["status"] == "ERROR" and not item["ignored"]]
warnings = [item for item in new_report["items"] if item["status"] == "WARNING" and not item["ignored"]]
errors_ignored = [item for item in new_report["items"] if item["status"] == "ERROR" and item["ignored"]]
warning_ignored = [item for item in new_report["items"] if item["status"] == "WARNING" and item["ignored"]]
ignored_msg = " " + m18n.n("diagnosis_ignored_issues", nb_ignored=len(errors_ignored + warning_ignored)) if errors_ignored or warning_ignored else ""
errors = [
item
for item in new_report["items"]
if item["status"] == "ERROR" and not item["ignored"]
]
warnings = [
item
for item in new_report["items"]
if item["status"] == "WARNING" and not item["ignored"]
]
errors_ignored = [
item
for item in new_report["items"]
if item["status"] == "ERROR" and item["ignored"]
]
warning_ignored = [
item
for item in new_report["items"]
if item["status"] == "WARNING" and item["ignored"]
]
ignored_msg = (
" "
+ m18n.n(
"diagnosis_ignored_issues",
nb_ignored=len(errors_ignored + warning_ignored),
)
if errors_ignored or warning_ignored
else ""
)
if errors and warnings:
logger.error(m18n.n("diagnosis_found_errors_and_warnings", errors=len(errors), warnings=len(warnings), category=new_report["description"]) + ignored_msg)
logger.error(
m18n.n(
"diagnosis_found_errors_and_warnings",
errors=len(errors),
warnings=len(warnings),
category=new_report["description"],
)
+ ignored_msg
)
elif errors:
logger.error(m18n.n("diagnosis_found_errors", errors=len(errors), category=new_report["description"]) + ignored_msg)
logger.error(
m18n.n(
"diagnosis_found_errors",
errors=len(errors),
category=new_report["description"],
)
+ ignored_msg
)
elif warnings:
logger.warning(m18n.n("diagnosis_found_warnings", warnings=len(warnings), category=new_report["description"]) + ignored_msg)
logger.warning(
m18n.n(
"diagnosis_found_warnings",
warnings=len(warnings),
category=new_report["description"],
)
+ ignored_msg
)
else:
logger.success(m18n.n("diagnosis_everything_ok", category=new_report["description"]) + ignored_msg)
logger.success(
m18n.n("diagnosis_everything_ok", category=new_report["description"])
+ ignored_msg
)
return 0, new_report
@ -430,10 +535,7 @@ class Diagnoser():
if not os.path.exists(cache_file):
if warn_if_no_cache:
logger.warning(m18n.n("diagnosis_no_cache", category=id_))
report = {"id": id_,
"cached_for": -1,
"timestamp": -1,
"items": []}
report = {"id": id_, "cached_for": -1, "timestamp": -1, "items": []}
else:
report = read_json(cache_file)
report["timestamp"] = int(os.path.getmtime(cache_file))
@ -475,7 +577,7 @@ class Diagnoser():
meta_data = item.get("meta", {}).copy()
meta_data.update(item.get("data", {}))
html_tags = re.compile(r'<[^>]+>')
html_tags = re.compile(r"<[^>]+>")
def m18n_(info):
if not isinstance(info, tuple) and not isinstance(info, list):
@ -485,11 +587,15 @@ class Diagnoser():
# In cli, we remove the html tags
if msettings.get("interface") != "api" or force_remove_html_tags:
s = s.replace("<cmd>", "'").replace("</cmd>", "'")
s = html_tags.sub('', s.replace("<br>", "\n"))
s = html_tags.sub("", s.replace("<br>", "\n"))
else:
s = s.replace("<cmd>", "<code class='cmd'>").replace("</cmd>", "</code>")
s = s.replace("<cmd>", "<code class='cmd'>").replace(
"</cmd>", "</code>"
)
# Make it so that links open in new tabs
s = s.replace("<a href=", "<a target='_blank' rel='noopener noreferrer' href=")
s = s.replace(
"<a href=", "<a target='_blank' rel='noopener noreferrer' href="
)
return s
item["summary"] = m18n_(item["summary"])
@ -511,36 +617,40 @@ class Diagnoser():
def getaddrinfo_ipv4_only(*args, **kwargs):
responses = old_getaddrinfo(*args, **kwargs)
return [response
for response in responses
if response[0] == socket.AF_INET]
return [response for response in responses if response[0] == socket.AF_INET]
def getaddrinfo_ipv6_only(*args, **kwargs):
responses = old_getaddrinfo(*args, **kwargs)
return [response
for response in responses
if response[0] == socket.AF_INET6]
return [
response for response in responses if response[0] == socket.AF_INET6
]
if ipversion == 4:
socket.getaddrinfo = getaddrinfo_ipv4_only
elif ipversion == 6:
socket.getaddrinfo = getaddrinfo_ipv6_only
url = 'https://%s/%s' % (DIAGNOSIS_SERVER, uri)
url = "https://%s/%s" % (DIAGNOSIS_SERVER, uri)
try:
r = requests.post(url, json=data, timeout=timeout)
finally:
socket.getaddrinfo = old_getaddrinfo
if r.status_code not in [200, 400]:
raise Exception("The remote diagnosis server failed miserably while trying to diagnose your server. This is most likely an error on Yunohost's infrastructure and not on your side. Please contact the YunoHost team an provide them with the following information.<br>URL: <code>%s</code><br>Status code: <code>%s</code>" % (url, r.status_code))
raise Exception(
"The remote diagnosis server failed miserably while trying to diagnose your server. This is most likely an error on Yunohost's infrastructure and not on your side. Please contact the YunoHost team an provide them with the following information.<br>URL: <code>%s</code><br>Status code: <code>%s</code>"
% (url, r.status_code)
)
if r.status_code == 400:
raise Exception("Diagnosis request was refused: %s" % r.content)
try:
r = r.json()
except Exception as e:
raise Exception("Failed to parse json from diagnosis server response.\nError: %s\nOriginal content: %s" % (e, r.content))
raise Exception(
"Failed to parse json from diagnosis server response.\nError: %s\nOriginal content: %s"
% (e, r.content)
)
return r
@ -557,6 +667,7 @@ def _list_diagnosis_categories():
def _email_diagnosis_issues():
from yunohost.domain import _get_maindomain
maindomain = _get_maindomain()
from_ = "diagnosis@%s (Automatic diagnosis on %s)" % (maindomain, maindomain)
to_ = "root"
@ -580,9 +691,16 @@ Subject: %s
---
%s
""" % (from_, to_, subject_, disclaimer, content)
""" % (
from_,
to_,
subject_,
disclaimer,
content,
)
import smtplib
smtp = smtplib.SMTP("localhost")
smtp.sendmail(from_, [to_], message)
smtp.quit()

View file

@ -26,18 +26,24 @@
import os
import re
from moulinette import m18n, msettings
from moulinette import m18n, msettings, msignals
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import write_to_file
from yunohost.app import app_ssowatconf, _installed_apps, _get_app_settings, _get_conflicting_apps
from yunohost.app import (
app_ssowatconf,
_installed_apps,
_get_app_settings,
_get_conflicting_apps,
)
from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf
from yunohost.utils.network import get_public_ip
from yunohost.log import is_unit_operation
from yunohost.hook import hook_callback
logger = getActionLogger('yunohost.domain')
logger = getActionLogger("yunohost.domain")
def domain_list(exclude_subdomains=False):
@ -51,7 +57,12 @@ def domain_list(exclude_subdomains=False):
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
result = [entry['virtualdomain'][0] for entry in ldap.search('ou=domains,dc=yunohost,dc=org', 'virtualdomain=*', ['virtualdomain'])]
result = [
entry["virtualdomain"][0]
for entry in ldap.search(
"ou=domains,dc=yunohost,dc=org", "virtualdomain=*", ["virtualdomain"]
)
]
result_list = []
for domain in result:
@ -65,17 +76,14 @@ def domain_list(exclude_subdomains=False):
def cmp_domain(domain):
# Keep the main part of the domain and the extension together
# eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this']
domain = domain.split('.')
domain = domain.split(".")
domain[-1] = domain[-2] + domain.pop()
domain = list(reversed(domain))
return domain
result_list = sorted(result_list, key=cmp_domain)
return {
'domains': result_list,
'main': _get_maindomain()
}
return {"domains": result_list, "main": _get_maindomain()}
@is_unit_operation()
@ -98,9 +106,9 @@ def domain_add(operation_logger, domain, dyndns=False):
ldap = _get_ldap_interface()
try:
ldap.validate_uniqueness({'virtualdomain': domain})
ldap.validate_uniqueness({"virtualdomain": domain})
except MoulinetteError:
raise YunohostError('domain_exists')
raise YunohostError("domain_exists")
operation_logger.start()
@ -111,36 +119,37 @@ def domain_add(operation_logger, domain, dyndns=False):
# DynDNS domain
if dyndns:
# Do not allow to subscribe to multiple dyndns domains...
if os.path.exists('/etc/cron.d/yunohost-dyndns'):
raise YunohostError('domain_dyndns_already_subscribed')
from yunohost.dyndns import dyndns_subscribe, _dyndns_provides, _guess_current_dyndns_domain
from yunohost.dyndns import dyndns_subscribe, _dyndns_provides
# Do not allow to subscribe to multiple dyndns domains...
if _guess_current_dyndns_domain("dyndns.yunohost.org") != (None, None):
raise YunohostError('domain_dyndns_already_subscribed')
# Check that this domain can effectively be provided by
# dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st)
if not _dyndns_provides("dyndns.yunohost.org", domain):
raise YunohostError('domain_dyndns_root_unknown')
raise YunohostError("domain_dyndns_root_unknown")
# Actually subscribe
dyndns_subscribe(domain=domain)
try:
import yunohost.certificate
yunohost.certificate._certificate_install_selfsigned([domain], False)
attr_dict = {
'objectClass': ['mailDomain', 'top'],
'virtualdomain': domain,
"objectClass": ["mailDomain", "top"],
"virtualdomain": domain,
}
try:
ldap.add('virtualdomain=%s,ou=domains' % domain, attr_dict)
ldap.add("virtualdomain=%s,ou=domains" % domain, attr_dict)
except Exception as e:
raise YunohostError('domain_creation_failed', domain=domain, error=e)
raise YunohostError("domain_creation_failed", domain=domain, error=e)
# 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"):
# Sometime we have weird issues with the regenconf where some files
# appears as manually modified even though they weren't touched ...
# There are a few ideas why this happens (like backup/restore nginx
@ -152,36 +161,41 @@ def domain_add(operation_logger, domain, dyndns=False):
# because it's one of the major service, but in the long term we
# should identify the root of this bug...
_force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain])
regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix', 'rspamd'])
regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd"])
app_ssowatconf()
except Exception:
# Force domain removal silently
try:
domain_remove(domain, True)
domain_remove(domain, force=True)
except Exception:
pass
raise
hook_callback('post_domain_add', args=[domain])
hook_callback("post_domain_add", args=[domain])
logger.success(m18n.n('domain_created'))
logger.success(m18n.n("domain_created"))
@is_unit_operation()
def domain_remove(operation_logger, domain, force=False):
def domain_remove(operation_logger, domain, remove_apps=False, force=False):
"""
Delete domains
Keyword argument:
domain -- Domain to delete
force -- Force the domain removal
remove_apps -- Remove applications installed on the domain
force -- Force the domain removal and don't not ask confirmation to
remove apps if remove_apps is specified
"""
from yunohost.hook import hook_callback
from yunohost.app import app_ssowatconf, app_info
from yunohost.app import app_ssowatconf, app_info, app_remove
from yunohost.utils.ldap import _get_ldap_interface
# the 'force' here is related to the exception happening in domain_add ...
# we don't want to check the domain exists because the ldap add may have
# failed
if not force and domain not in domain_list()['domains']:
raise YunohostError('domain_name_unknown', domain=domain)
@ -191,10 +205,13 @@ def domain_remove(operation_logger, domain, force=False):
other_domains.remove(domain)
if other_domains:
raise YunohostError('domain_cannot_remove_main',
domain=domain, other_domains="\n * " + ("\n * ".join(other_domains)))
raise YunohostError(
"domain_cannot_remove_main",
domain=domain,
other_domains="\n * " + ("\n * ".join(other_domains)),
)
else:
raise YunohostError('domain_cannot_remove_main_add_new_one', domain=domain)
raise YunohostError("domain_cannot_remove_main_add_new_one", domain=domain)
# Check if apps are installed on the domain
apps_on_that_domain = []
@ -203,19 +220,33 @@ def domain_remove(operation_logger, domain, force=False):
settings = _get_app_settings(app)
label = app_info(app)["name"]
if settings.get("domain") == domain:
apps_on_that_domain.append(" - %s \"%s\" on https://%s%s" % (app, label, domain, settings["path"]) if "path" in settings else app)
apps_on_that_domain.append((app, " - %s \"%s\" on https://%s%s" % (app, label, domain, settings["path"]) if "path" in settings else app))
if apps_on_that_domain:
raise YunohostError('domain_uninstall_app_first', apps="\n".join(apps_on_that_domain))
if remove_apps:
if msettings.get('interface') == "cli" and not force:
answer = msignals.prompt(m18n.n('domain_remove_confirm_apps_removal',
apps="\n".join([x[1] for x in apps_on_that_domain]),
answers='y/N'), color="yellow")
if answer.upper() != "Y":
raise YunohostError("aborting")
for app, _ in apps_on_that_domain:
app_remove(app)
else:
raise YunohostError('domain_uninstall_app_first', apps="\n".join([x[1] for x in apps_on_that_domain]))
operation_logger.start()
ldap = _get_ldap_interface()
try:
ldap.remove('virtualdomain=' + domain + ',ou=domains')
ldap.remove("virtualdomain=" + domain + ",ou=domains")
except Exception as e:
raise YunohostError('domain_deletion_failed', domain=domain, error=e)
raise YunohostError("domain_deletion_failed", domain=domain, error=e)
os.system('rm -rf /etc/yunohost/certs/%s' % domain)
os.system("rm -rf /etc/yunohost/certs/%s" % domain)
# Delete dyndns keys for this domain (if any)
os.system('rm -rf /etc/yunohost/dyndns/K%s.+*' % domain)
# Sometime we have weird issues with the regenconf where some files
# appears as manually modified even though they weren't touched ...
@ -234,14 +265,16 @@ def domain_remove(operation_logger, domain, force=False):
# catastrophic consequences of nginx breaking because it can't load the
# cert file which disappeared etc..
if os.path.exists("/etc/nginx/conf.d/%s.conf" % domain):
_process_regen_conf("/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True)
_process_regen_conf(
"/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True
)
regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix'])
regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix"])
app_ssowatconf()
hook_callback('post_domain_remove', args=[domain])
hook_callback("post_domain_remove", args=[domain])
logger.success(m18n.n('domain_deleted'))
logger.success(m18n.n("domain_deleted"))
def domain_dns_conf(domain, ttl=None):
@ -254,8 +287,8 @@ def domain_dns_conf(domain, ttl=None):
"""
if domain not in domain_list()['domains']:
raise YunohostError('domain_name_unknown', domain=domain)
if domain not in domain_list()["domains"]:
raise YunohostError("domain_name_unknown", domain=domain)
ttl = 3600 if ttl is None else ttl
@ -289,7 +322,7 @@ def domain_dns_conf(domain, ttl=None):
for record in record_list:
result += "\n{name} {ttl} IN {type} {value}".format(**record)
if msettings.get('interface') == 'cli':
if msettings.get("interface") == "cli":
logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation"))
return result
@ -308,63 +341,58 @@ def domain_main_domain(operation_logger, new_main_domain=None):
# If no new domain specified, we return the current main domain
if not new_main_domain:
return {'current_main_domain': _get_maindomain()}
return {"current_main_domain": _get_maindomain()}
# Check domain exists
if new_main_domain not in domain_list()['domains']:
raise YunohostError('domain_name_unknown', domain=new_main_domain)
if new_main_domain not in domain_list()["domains"]:
raise YunohostError("domain_name_unknown", domain=new_main_domain)
operation_logger.related_to.append(('domain', new_main_domain))
operation_logger.related_to.append(("domain", new_main_domain))
operation_logger.start()
# Apply changes to ssl certs
ssl_key = "/etc/ssl/private/yunohost_key.pem"
ssl_crt = "/etc/ssl/private/yunohost_crt.pem"
new_ssl_key = "/etc/yunohost/certs/%s/key.pem" % new_main_domain
new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_main_domain
try:
if os.path.exists(ssl_key) or os.path.lexists(ssl_key):
os.remove(ssl_key)
if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt):
os.remove(ssl_crt)
write_to_file("/etc/yunohost/current_host", new_main_domain)
os.symlink(new_ssl_key, ssl_key)
os.symlink(new_ssl_crt, ssl_crt)
_set_maindomain(new_main_domain)
_set_hostname(new_main_domain)
except Exception as e:
logger.warning("%s" % e, exc_info=1)
raise YunohostError('main_domain_change_failed')
_set_hostname(new_main_domain)
raise YunohostError("main_domain_change_failed")
# Generate SSOwat configuration file
app_ssowatconf()
# Regen configurations
try:
with open('/etc/yunohost/installed', 'r'):
regen_conf()
except IOError:
pass
if os.path.exists("/etc/yunohost/installed"):
regen_conf()
logger.success(m18n.n('main_domain_changed'))
logger.success(m18n.n("main_domain_changed"))
def domain_cert_status(domain_list, full=False):
import yunohost.certificate
return yunohost.certificate.certificate_status(domain_list, full)
def domain_cert_install(domain_list, force=False, no_checks=False, self_signed=False, staging=False):
def domain_cert_install(
domain_list, force=False, no_checks=False, self_signed=False, staging=False
):
import yunohost.certificate
return yunohost.certificate.certificate_install(domain_list, force, no_checks, self_signed, staging)
return yunohost.certificate.certificate_install(
domain_list, force, no_checks, self_signed, staging
)
def domain_cert_renew(domain_list, force=False, no_checks=False, email=False, staging=False):
def domain_cert_renew(
domain_list, force=False, no_checks=False, email=False, staging=False
):
import yunohost.certificate
return yunohost.certificate.certificate_renew(domain_list, force, no_checks, email, staging)
return yunohost.certificate.certificate_renew(
domain_list, force, no_checks, email, staging
)
def domain_url_available(domain, path):
@ -380,16 +408,11 @@ def domain_url_available(domain, path):
def _get_maindomain():
with open('/etc/yunohost/current_host', 'r') as f:
with open("/etc/yunohost/current_host", "r") as f:
maindomain = f.readline().rstrip()
return maindomain
def _set_maindomain(domain):
with open('/etc/yunohost/current_host', 'w') as f:
f.write(domain)
def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False):
"""
Internal function that will returns a data structure containing the needed
@ -498,10 +521,22 @@ def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False):
####################
records = {
"basic": [{"name": name, "ttl": ttl_, "type": type_, "value": value} for name, ttl_, type_, value in basic],
"xmpp": [{"name": name, "ttl": ttl_, "type": type_, "value": value} for name, ttl_, type_, value in xmpp],
"mail": [{"name": name, "ttl": ttl_, "type": type_, "value": value} for name, ttl_, type_, value in mail],
"extra": [{"name": name, "ttl": ttl_, "type": type_, "value": value} for name, ttl_, type_, value in extra],
"basic": [
{"name": name, "ttl": ttl_, "type": type_, "value": value}
for name, ttl_, type_, value in basic
],
"xmpp": [
{"name": name, "ttl": ttl_, "type": type_, "value": value}
for name, ttl_, type_, value in xmpp
],
"mail": [
{"name": name, "ttl": ttl_, "type": type_, "value": value}
for name, ttl_, type_, value in mail
],
"extra": [
{"name": name, "ttl": ttl_, "type": type_, "value": value}
for name, ttl_, type_, value in extra
],
}
##################
@ -510,7 +545,7 @@ def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False):
# Defined by custom hooks ships in apps for example ...
hook_results = hook_callback('custom_dns_rules', args=[domain])
hook_results = hook_callback("custom_dns_rules", args=[domain])
for hook_name, results in hook_results.items():
#
# There can be multiple results per hook name, so results look like
@ -526,18 +561,28 @@ def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False):
# [...]
#
# Loop over the sub-results
custom_records = [v['stdreturn'] for v in results.values()
if v and v['stdreturn']]
custom_records = [
v["stdreturn"] for v in results.values() if v and v["stdreturn"]
]
records[hook_name] = []
for record_list in custom_records:
# Check that record_list is indeed a list of dict
# with the required keys
if not isinstance(record_list, list) \
or any(not isinstance(record, dict) for record in record_list) \
or any(key not in record for record in record_list for key in ["name", "ttl", "type", "value"]):
if (
not isinstance(record_list, list)
or any(not isinstance(record, dict) for record in record_list)
or any(
key not in record
for record in record_list
for key in ["name", "ttl", "type", "value"]
)
):
# Display an error, mainly for app packagers trying to implement a hook
logger.warning("Ignored custom record from hook '%s' because the data is not a *list* of dict with keys name, ttl, type and value. Raw data : %s" % (hook_name, record_list))
logger.warning(
"Ignored custom record from hook '%s' because the data is not a *list* of dict with keys name, ttl, type and value. Raw data : %s"
% (hook_name, record_list)
)
continue
records[hook_name].extend(record_list)
@ -546,7 +591,7 @@ def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False):
def _get_DKIM(domain):
DKIM_file = '/etc/dkim/{domain}.mail.txt'.format(domain=domain)
DKIM_file = "/etc/dkim/{domain}.mail.txt".format(domain=domain)
if not os.path.isfile(DKIM_file):
return (None, None)
@ -572,19 +617,27 @@ def _get_DKIM(domain):
# Legacy DKIM format
if is_legacy_format:
dkim = re.match((
r'^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+'
'[^"]*"v=(?P<v>[^";]+);'
r'[\s"]*k=(?P<k>[^";]+);'
'[\s"]*p=(?P<p>[^";]+)'), dkim_content, re.M | re.S
dkim = re.match(
(
r"^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+"
r'[^"]*"v=(?P<v>[^";]+);'
r'[\s"]*k=(?P<k>[^";]+);'
r'[\s"]*p=(?P<p>[^";]+)'
),
dkim_content,
re.M | re.S,
)
else:
dkim = re.match((
r'^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+'
'[^"]*"v=(?P<v>[^";]+);'
r'[\s"]*h=(?P<h>[^";]+);'
r'[\s"]*k=(?P<k>[^";]+);'
'[\s"]*p=(?P<p>[^";]+)'), dkim_content, re.M | re.S
dkim = re.match(
(
r"^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+"
r'[^"]*"v=(?P<v>[^";]+);'
r'[\s"]*h=(?P<h>[^";]+);'
r'[\s"]*k=(?P<k>[^";]+);'
r'[\s"]*p=(?P<p>[^";]+)'
),
dkim_content,
re.M | re.S,
)
if not dkim:
@ -592,16 +645,18 @@ def _get_DKIM(domain):
if is_legacy_format:
return (
dkim.group('host'),
'"v={v}; k={k}; p={p}"'.format(v=dkim.group('v'),
k=dkim.group('k'),
p=dkim.group('p'))
dkim.group("host"),
'"v={v}; k={k}; p={p}"'.format(
v=dkim.group("v"), k=dkim.group("k"), p=dkim.group("p")
),
)
else:
return (
dkim.group('host'),
'"v={v}; h={h}; k={k}; p={p}"'.format(v=dkim.group('v'),
h=dkim.group('h'),
k=dkim.group('k'),
p=dkim.group('p'))
dkim.group("host"),
'"v={v}; h={h}; k={k}; p={p}"'.format(
v=dkim.group("v"),
h=dkim.group("h"),
k=dkim.group("k"),
p=dkim.group("p"),
),
)

View file

@ -40,17 +40,16 @@ from yunohost.utils.error import YunohostError
from yunohost.domain import _get_maindomain, _build_dns_conf
from yunohost.utils.network import get_public_ip, dig
from yunohost.log import is_unit_operation
from yunohost.regenconf import regen_conf
logger = getActionLogger('yunohost.dyndns')
logger = getActionLogger("yunohost.dyndns")
DYNDNS_ZONE = '/etc/yunohost/dyndns/zone'
DYNDNS_ZONE = "/etc/yunohost/dyndns/zone"
RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile(
r'.*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$'
)
RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile(r".*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$")
RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile(
r'.*/K(?P<domain>[^\s\+]+)\.\+165.+\.private$'
r".*/K(?P<domain>[^\s\+]+)\.\+165.+\.private$"
)
@ -71,13 +70,15 @@ def _dyndns_provides(provider, domain):
try:
# Dyndomains will be a list of domains supported by the provider
# e.g. [ "nohost.me", "noho.st" ]
dyndomains = download_json('https://%s/domains' % provider, timeout=30)
dyndomains = download_json("https://%s/domains" % provider, timeout=30)
except MoulinetteError as e:
logger.error(str(e))
raise YunohostError('dyndns_could_not_check_provide', domain=domain, provider=provider)
raise YunohostError(
"dyndns_could_not_check_provide", domain=domain, provider=provider
)
# Extract 'dyndomain' from 'domain', e.g. 'nohost.me' from 'foo.nohost.me'
dyndomain = '.'.join(domain.split('.')[1:])
dyndomain = ".".join(domain.split(".")[1:])
return dyndomain in dyndomains
@ -93,22 +94,25 @@ def _dyndns_available(provider, domain):
Returns:
True if the domain is available, False otherwise.
"""
logger.debug("Checking if domain %s is available on %s ..."
% (domain, provider))
logger.debug("Checking if domain %s is available on %s ..." % (domain, provider))
try:
r = download_json('https://%s/test/%s' % (provider, domain),
expected_status_code=None)
r = download_json(
"https://%s/test/%s" % (provider, domain), expected_status_code=None
)
except MoulinetteError as e:
logger.error(str(e))
raise YunohostError('dyndns_could_not_check_available',
domain=domain, provider=provider)
raise YunohostError(
"dyndns_could_not_check_available", domain=domain, provider=provider
)
return r == "Domain %s is available" % domain
@is_unit_operation()
def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", domain=None, key=None):
def dyndns_subscribe(
operation_logger, subscribe_host="dyndns.yunohost.org", domain=None, key=None
):
"""
Subscribe to a DynDNS service
@ -118,64 +122,95 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom
subscribe_host -- Dynette HTTP API to subscribe to
"""
if len(glob.glob('/etc/yunohost/dyndns/*.key')) != 0 or os.path.exists('/etc/cron.d/yunohost-dyndns'):
if _guess_current_dyndns_domain(subscribe_host) != (None, None):
raise YunohostError('domain_dyndns_already_subscribed')
if domain is None:
domain = _get_maindomain()
operation_logger.related_to.append(('domain', domain))
operation_logger.related_to.append(("domain", domain))
# Verify if domain is provided by subscribe_host
if not _dyndns_provides(subscribe_host, domain):
raise YunohostError('dyndns_domain_not_provided', domain=domain, provider=subscribe_host)
raise YunohostError(
"dyndns_domain_not_provided", domain=domain, provider=subscribe_host
)
# Verify if domain is available
if not _dyndns_available(subscribe_host, domain):
raise YunohostError('dyndns_unavailable', domain=domain)
raise YunohostError("dyndns_unavailable", domain=domain)
operation_logger.start()
if key is None:
if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0:
if not os.path.exists('/etc/yunohost/dyndns'):
os.makedirs('/etc/yunohost/dyndns')
if len(glob.glob("/etc/yunohost/dyndns/*.key")) == 0:
if not os.path.exists("/etc/yunohost/dyndns"):
os.makedirs("/etc/yunohost/dyndns")
logger.debug(m18n.n('dyndns_key_generating'))
logger.debug(m18n.n("dyndns_key_generating"))
os.system('cd /etc/yunohost/dyndns && '
'dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s' % domain)
os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private')
os.system(
"cd /etc/yunohost/dyndns && "
"dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s"
% domain
)
os.system(
"chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private"
)
private_file = glob.glob('/etc/yunohost/dyndns/*%s*.private' % domain)[0]
key_file = glob.glob('/etc/yunohost/dyndns/*%s*.key' % domain)[0]
private_file = glob.glob("/etc/yunohost/dyndns/*%s*.private" % domain)[0]
key_file = glob.glob("/etc/yunohost/dyndns/*%s*.key" % domain)[0]
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
# Send subscription
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.encode()).decode()),
data={"subdomain": domain},
timeout=30,
)
except Exception as e:
os.system("rm -f %s" % private_file)
os.system("rm -f %s" % key_file)
raise YunohostError('dyndns_registration_failed', error=str(e))
raise YunohostError("dyndns_registration_failed", error=str(e))
if r.status_code != 201:
os.system("rm -f %s" % private_file)
os.system("rm -f %s" % key_file)
try:
error = json.loads(r.text)['error']
except:
error = "Server error, code: %s. (Message: \"%s\")" % (r.status_code, r.text)
raise YunohostError('dyndns_registration_failed', error=error)
error = json.loads(r.text)["error"]
except Exception:
error = 'Server error, code: %s. (Message: "%s")' % (r.status_code, r.text)
raise YunohostError("dyndns_registration_failed", error=error)
# Yunohost regen conf will add the dyndns cron job if a private key exists
# in /etc/yunohost/dyndns
regen_conf(["yunohost"])
# Add some dyndns update in 2 and 4 minutes from now such that user should
# not have to wait 10ish minutes for the conf to propagate
cmd = "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\""
# For some reason subprocess doesn't like the redirections so we have to use bash -c explicity...
subprocess.check_call(["bash", "-c", cmd.format(t="2 min")])
subprocess.check_call(["bash", "-c", cmd.format(t="4 min")])
logger.success(m18n.n('dyndns_registered'))
dyndns_installcron()
@is_unit_operation()
def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, key=None,
ipv4=None, ipv6=None, force=False, dry_run=False):
def dyndns_update(
operation_logger,
dyn_host="dyndns.yunohost.org",
domain=None,
key=None,
ipv4=None,
ipv6=None,
force=False,
dry_run=False,
):
"""
Update IP on DynDNS platform
@ -194,28 +229,31 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
# If domain is not given, try to guess it from keys available...
if domain is None:
(domain, key) = _guess_current_dyndns_domain(dyn_host)
if domain is None:
raise YunohostError('dyndns_no_domain_registered')
# If key is not given, pick the first file we find with the domain given
else:
if key is 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:
raise YunohostError('dyndns_key_not_found')
raise YunohostError("dyndns_key_not_found")
key = keys[0]
# Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me'
host = domain.split('.')[1:]
host = '.'.join(host)
host = domain.split(".")[1:]
host = ".".join(host)
logger.debug("Building zone update file ...")
lines = [
'server %s' % dyn_host,
'zone %s' % host,
"server %s" % dyn_host,
"zone %s" % host,
]
def resolve_domain(domain, rdtype):
# FIXME make this work for IPv6-only hosts too..
@ -223,12 +261,15 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
dyn_host_ip = result[0] if ok == "ok" and len(result) else None
if not dyn_host_ip:
raise YunohostError("Failed to resolve %s" % dyn_host)
ok, result = dig(domain, rdtype, resolvers=[dyn_host_ip])
if ok == "ok":
return result[0] if len(result) else None
elif result[0] == "Timeout":
logger.debug("Timed-out while trying to resolve %s record for %s using %s" % (rdtype, domain, dyn_host))
logger.debug(
"Timed-out while trying to resolve %s record for %s using %s"
% (rdtype, domain, dyn_host)
)
else:
return None
@ -237,11 +278,16 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
if ok == "ok":
return result[0] if len(result) else None
elif result[0] == "Timeout":
logger.debug("Timed-out while trying to resolve %s record for %s using external resolvers : %s" % (rdtype, domain, result))
logger.debug(
"Timed-out while trying to resolve %s record for %s using external resolvers : %s"
% (rdtype, domain, result)
)
else:
return None
raise YunohostError("Failed to resolve %s for %s" % (rdtype, domain), raw_msg=True)
raise YunohostError(
"Failed to resolve %s for %s" % (rdtype, domain), raw_msg=True
)
old_ipv4 = resolve_domain(domain, "A")
old_ipv6 = resolve_domain(domain, "AAAA")
@ -264,7 +310,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
logger.info("No updated needed.")
return
else:
operation_logger.related_to.append(('domain', domain))
operation_logger.related_to.append(("domain", domain))
operation_logger.start()
logger.info("Updated needed, going on...")
@ -297,18 +343,17 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
record["value"] = domain
record["value"] = record["value"].replace(";", r"\;")
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(" @.", " ")
lines.append(action)
lines += [
'show',
'send'
]
lines += ["show", "send"]
# Write the actions to do to update to a file, to be able to pass it
# to nsupdate as argument
write_to_file(DYNDNS_ZONE, '\n'.join(lines))
write_to_file(DYNDNS_ZONE, "\n".join(lines))
logger.debug("Now pushing new conf to DynDNS host...")
@ -317,39 +362,23 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE]
subprocess.check_call(command)
except subprocess.CalledProcessError:
raise YunohostError('dyndns_ip_update_failed')
raise YunohostError("dyndns_ip_update_failed")
logger.success(m18n.n('dyndns_ip_updated'))
logger.success(m18n.n("dyndns_ip_updated"))
else:
print(read_file(DYNDNS_ZONE))
print("")
print("Warning: dry run, this is only the generated config, it won't be applied")
print(
"Warning: dry run, this is only the generated config, it won't be applied"
)
def dyndns_installcron():
"""
Install IP update cron
"""
with open('/etc/cron.d/yunohost-dyndns', 'w+') as f:
f.write('*/2 * * * * root yunohost dyndns update >> /dev/null\n')
logger.success(m18n.n('dyndns_cron_installed'))
logger.warning("This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'.")
def dyndns_removecron():
"""
Remove IP update cron
"""
try:
os.remove("/etc/cron.d/yunohost-dyndns")
except Exception as e:
raise YunohostError('dyndns_cron_remove_failed', error=e)
logger.success(m18n.n('dyndns_cron_removed'))
logger.warning("This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'.")
def _guess_current_dyndns_domain(dyn_host):
@ -362,14 +391,14 @@ def _guess_current_dyndns_domain(dyn_host):
"""
# Retrieve the first registered domain
paths = list(glob.iglob('/etc/yunohost/dyndns/K*.private'))
paths = list(glob.iglob("/etc/yunohost/dyndns/K*.private"))
for path in paths:
match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path)
if not match:
match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path)
if not match:
continue
_domain = match.group('domain')
_domain = match.group("domain")
# Verify if domain is registered (i.e., if it's available, skip
# current domain beause that's not the one we want to update..)
@ -380,4 +409,4 @@ def _guess_current_dyndns_domain(dyn_host):
else:
return (_domain, path)
raise YunohostError('dyndns_no_domain_registered')
return (None, None)

View file

@ -33,14 +33,15 @@ from moulinette.utils import process
from moulinette.utils.log import getActionLogger
from moulinette.utils.text import prependlines
FIREWALL_FILE = '/etc/yunohost/firewall.yml'
UPNP_CRON_JOB = '/etc/cron.d/yunohost-firewall-upnp'
FIREWALL_FILE = "/etc/yunohost/firewall.yml"
UPNP_CRON_JOB = "/etc/cron.d/yunohost-firewall-upnp"
logger = getActionLogger('yunohost.firewall')
logger = getActionLogger("yunohost.firewall")
def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False,
no_upnp=False, no_reload=False):
def firewall_allow(
protocol, port, ipv4_only=False, ipv6_only=False, no_upnp=False, no_reload=False
):
"""
Allow connections on a port
@ -56,20 +57,26 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False,
firewall = firewall_list(raw=True)
# Validate port
if not isinstance(port, int) and ':' not in port:
if not isinstance(port, int) and ":" not in port:
port = int(port)
# Validate protocols
protocols = ['TCP', 'UDP']
if protocol != 'Both' and protocol in protocols:
protocols = [protocol, ]
protocols = ["TCP", "UDP"]
if protocol != "Both" and protocol in protocols:
protocols = [
protocol,
]
# Validate IP versions
ipvs = ['ipv4', 'ipv6']
ipvs = ["ipv4", "ipv6"]
if ipv4_only and not ipv6_only:
ipvs = ['ipv4', ]
ipvs = [
"ipv4",
]
elif ipv6_only and not ipv4_only:
ipvs = ['ipv6', ]
ipvs = [
"ipv6",
]
for p in protocols:
# Iterate over IP versions to add port
@ -78,12 +85,15 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False,
firewall[i][p].append(port)
else:
ipv = "IPv%s" % i[3]
logger.warning(m18n.n('port_already_opened', port=port, ip_version=ipv))
logger.warning(m18n.n("port_already_opened", port=port, ip_version=ipv))
# Add port forwarding with UPnP
if not no_upnp and port not in firewall['uPnP'][p]:
firewall['uPnP'][p].append(port)
if firewall['uPnP'][p + "_TO_CLOSE"] and port in firewall['uPnP'][p + "_TO_CLOSE"]:
firewall['uPnP'][p + "_TO_CLOSE"].remove(port)
if not no_upnp and port not in firewall["uPnP"][p]:
firewall["uPnP"][p].append(port)
if (
p + "_TO_CLOSE" in firewall["uPnP"]
and port in firewall["uPnP"][p + "_TO_CLOSE"]
):
firewall["uPnP"][p + "_TO_CLOSE"].remove(port)
# Update and reload firewall
_update_firewall_file(firewall)
@ -91,8 +101,9 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False,
return firewall_reload()
def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False,
upnp_only=False, no_reload=False):
def firewall_disallow(
protocol, port, ipv4_only=False, ipv6_only=False, upnp_only=False, no_reload=False
):
"""
Disallow connections on a port
@ -108,24 +119,30 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False,
firewall = firewall_list(raw=True)
# Validate port
if not isinstance(port, int) and ':' not in port:
if not isinstance(port, int) and ":" not in port:
port = int(port)
# Validate protocols
protocols = ['TCP', 'UDP']
if protocol != 'Both' and protocol in protocols:
protocols = [protocol, ]
protocols = ["TCP", "UDP"]
if protocol != "Both" and protocol in protocols:
protocols = [
protocol,
]
# Validate IP versions and UPnP
ipvs = ['ipv4', 'ipv6']
ipvs = ["ipv4", "ipv6"]
upnp = True
if ipv4_only and ipv6_only:
upnp = True # automatically disallow UPnP
elif ipv4_only:
ipvs = ['ipv4', ]
ipvs = [
"ipv4",
]
upnp = upnp_only
elif ipv6_only:
ipvs = ['ipv6', ]
ipvs = [
"ipv6",
]
upnp = upnp_only
elif upnp_only:
ipvs = []
@ -137,13 +154,13 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False,
firewall[i][p].remove(port)
else:
ipv = "IPv%s" % i[3]
logger.warning(m18n.n('port_already_closed', port=port, ip_version=ipv))
logger.warning(m18n.n("port_already_closed", port=port, ip_version=ipv))
# Remove port forwarding with UPnP
if upnp and port in firewall['uPnP'][p]:
firewall['uPnP'][p].remove(port)
if not firewall['uPnP'][p + "_TO_CLOSE"]:
firewall['uPnP'][p + "_TO_CLOSE"] = []
firewall['uPnP'][p + "_TO_CLOSE"].append(port)
if upnp and port in firewall["uPnP"][p]:
firewall["uPnP"][p].remove(port)
if p + "_TO_CLOSE" not in firewall["uPnP"]:
firewall["uPnP"][p + "_TO_CLOSE"] = []
firewall["uPnP"][p + "_TO_CLOSE"].append(port)
# Update and reload firewall
_update_firewall_file(firewall)
@ -168,21 +185,22 @@ def firewall_list(raw=False, by_ip_version=False, list_forwarded=False):
# Retrieve all ports for IPv4 and IPv6
ports = {}
for i in ['ipv4', 'ipv6']:
for i in ["ipv4", "ipv6"]:
f = firewall[i]
# Combine TCP and UDP ports
ports[i] = sorted(set(f['TCP']) | set(f['UDP']))
ports[i] = sorted(set(f["TCP"]) | set(f["UDP"]))
if not by_ip_version:
# Combine IPv4 and IPv6 ports
ports = sorted(set(ports['ipv4']) | set(ports['ipv6']))
ports = sorted(set(ports["ipv4"]) | set(ports["ipv6"]))
# Format returned dict
ret = {"opened_ports": ports}
if list_forwarded:
# Combine TCP and UDP forwarded ports
ret['forwarded_ports'] = sorted(
set(firewall['uPnP']['TCP']) | set(firewall['uPnP']['UDP']))
ret["forwarded_ports"] = sorted(
set(firewall["uPnP"]["TCP"]) | set(firewall["uPnP"]["UDP"])
)
return ret
@ -202,20 +220,22 @@ def firewall_reload(skip_upnp=False):
# Check if SSH port is allowed
ssh_port = _get_ssh_port()
if ssh_port not in firewall_list()['opened_ports']:
firewall_allow('TCP', ssh_port, no_reload=True)
if ssh_port not in firewall_list()["opened_ports"]:
firewall_allow("TCP", ssh_port, no_reload=True)
# Retrieve firewall rules and UPnP status
firewall = firewall_list(raw=True)
upnp = firewall_upnp()['enabled'] if not skip_upnp else False
upnp = firewall_upnp()["enabled"] if not skip_upnp else False
# IPv4
try:
process.check_output("iptables -w -L")
except process.CalledProcessError as e:
logger.debug('iptables seems to be not available, it outputs:\n%s',
prependlines(e.output.rstrip(), '> '))
logger.warning(m18n.n('iptables_unavailable'))
logger.debug(
"iptables seems to be not available, it outputs:\n%s",
prependlines(e.output.rstrip(), "> "),
)
logger.warning(m18n.n("iptables_unavailable"))
else:
rules = [
"iptables -w -F",
@ -223,10 +243,12 @@ def firewall_reload(skip_upnp=False):
"iptables -w -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT",
]
# Iterate over ports and add rule
for protocol in ['TCP', 'UDP']:
for port in firewall['ipv4'][protocol]:
rules.append("iptables -w -A INPUT -p %s --dport %s -j ACCEPT"
% (protocol, process.quote(str(port))))
for protocol in ["TCP", "UDP"]:
for port in firewall["ipv4"][protocol]:
rules.append(
"iptables -w -A INPUT -p %s --dport %s -j ACCEPT"
% (protocol, process.quote(str(port)))
)
rules += [
"iptables -w -A INPUT -i lo -j ACCEPT",
"iptables -w -A INPUT -p icmp -j ACCEPT",
@ -242,9 +264,11 @@ def firewall_reload(skip_upnp=False):
try:
process.check_output("ip6tables -L")
except process.CalledProcessError as e:
logger.debug('ip6tables seems to be not available, it outputs:\n%s',
prependlines(e.output.rstrip(), '> '))
logger.warning(m18n.n('ip6tables_unavailable'))
logger.debug(
"ip6tables seems to be not available, it outputs:\n%s",
prependlines(e.output.rstrip(), "> "),
)
logger.warning(m18n.n("ip6tables_unavailable"))
else:
rules = [
"ip6tables -w -F",
@ -252,10 +276,12 @@ def firewall_reload(skip_upnp=False):
"ip6tables -w -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT",
]
# Iterate over ports and add rule
for protocol in ['TCP', 'UDP']:
for port in firewall['ipv6'][protocol]:
rules.append("ip6tables -w -A INPUT -p %s --dport %s -j ACCEPT"
% (protocol, process.quote(str(port))))
for protocol in ["TCP", "UDP"]:
for port in firewall["ipv6"][protocol]:
rules.append(
"ip6tables -w -A INPUT -p %s --dport %s -j ACCEPT"
% (protocol, process.quote(str(port)))
)
rules += [
"ip6tables -w -A INPUT -i lo -j ACCEPT",
"ip6tables -w -A INPUT -p icmpv6 -j ACCEPT",
@ -268,10 +294,11 @@ def firewall_reload(skip_upnp=False):
reloaded = True
if not reloaded:
raise YunohostError('firewall_reload_failed')
raise YunohostError("firewall_reload_failed")
hook_callback('post_iptable_rules',
args=[upnp, os.path.exists("/proc/net/if_inet6")])
hook_callback(
"post_iptable_rules", args=[upnp, os.path.exists("/proc/net/if_inet6")]
)
if upnp:
# Refresh port forwarding with UPnP
@ -280,13 +307,13 @@ def firewall_reload(skip_upnp=False):
_run_service_command("reload", "fail2ban")
if errors:
logger.warning(m18n.n('firewall_rules_cmd_failed'))
logger.warning(m18n.n("firewall_rules_cmd_failed"))
else:
logger.success(m18n.n('firewall_reloaded'))
logger.success(m18n.n("firewall_reloaded"))
return firewall_list()
def firewall_upnp(action='status', no_refresh=False):
def firewall_upnp(action="status", no_refresh=False):
"""
Manage port forwarding using UPnP
@ -300,44 +327,46 @@ def firewall_upnp(action='status', no_refresh=False):
"""
firewall = firewall_list(raw=True)
enabled = firewall['uPnP']['enabled']
enabled = firewall["uPnP"]["enabled"]
# Compatibility with previous version
if action == 'reload':
if action == "reload":
logger.debug("'reload' action is deprecated and will be removed")
try:
# Remove old cron job
os.remove('/etc/cron.d/yunohost-firewall')
except:
os.remove("/etc/cron.d/yunohost-firewall")
except Exception:
pass
action = 'status'
action = "status"
no_refresh = False
if action == 'status' and no_refresh:
if action == "status" and no_refresh:
# Only return current state
return {'enabled': enabled}
elif action == 'enable' or (enabled and action == 'status'):
return {"enabled": enabled}
elif action == "enable" or (enabled and action == "status"):
# Add cron job
with open(UPNP_CRON_JOB, 'w+') as f:
f.write('*/50 * * * * root '
'/usr/bin/yunohost firewall upnp status >>/dev/null\n')
with open(UPNP_CRON_JOB, "w+") as f:
f.write(
"*/50 * * * * root "
"/usr/bin/yunohost firewall upnp status >>/dev/null\n"
)
# Open port 1900 to receive discovery message
if 1900 not in firewall['ipv4']['UDP']:
firewall_allow('UDP', 1900, no_upnp=True, no_reload=True)
if 1900 not in firewall["ipv4"]["UDP"]:
firewall_allow("UDP", 1900, no_upnp=True, no_reload=True)
if not enabled:
firewall_reload(skip_upnp=True)
enabled = True
elif action == 'disable' or (not enabled and action == 'status'):
elif action == "disable" or (not enabled and action == "status"):
try:
# Remove cron job
os.remove(UPNP_CRON_JOB)
except:
except Exception:
pass
enabled = False
if action == 'status':
if action == "status":
no_refresh = True
else:
raise YunohostError('action_invalid', action=action)
raise YunohostError("action_invalid", action=action)
# Refresh port mapping using UPnP
if not no_refresh:
@ -345,77 +374,84 @@ def firewall_upnp(action='status', no_refresh=False):
upnpc.discoverdelay = 3000
# Discover UPnP device(s)
logger.debug('discovering UPnP devices...')
logger.debug("discovering UPnP devices...")
nb_dev = upnpc.discover()
logger.debug('found %d UPnP device(s)', int(nb_dev))
logger.debug("found %d UPnP device(s)", int(nb_dev))
if nb_dev < 1:
logger.error(m18n.n('upnp_dev_not_found'))
logger.error(m18n.n("upnp_dev_not_found"))
enabled = False
else:
try:
# Select UPnP device
upnpc.selectigd()
except:
logger.debug('unable to select UPnP device', exc_info=1)
except Exception:
logger.debug("unable to select UPnP device", exc_info=1)
enabled = False
else:
# Iterate over ports
for protocol in ['TCP', 'UDP']:
if firewall['uPnP'][protocol + "_TO_CLOSE"]:
for port in firewall['uPnP'][protocol + "_TO_CLOSE"]:
for protocol in ["TCP", "UDP"]:
if protocol + "_TO_CLOSE" in firewall["uPnP"]:
for port in firewall["uPnP"][protocol + "_TO_CLOSE"]:
# Clean the mapping of this port
if upnpc.getspecificportmapping(port, protocol):
try:
upnpc.deleteportmapping(port, protocol)
except:
except Exception:
pass
firewall['uPnP'][protocol + "_TO_CLOSE"] = []
firewall["uPnP"][protocol + "_TO_CLOSE"] = []
for port in firewall['uPnP'][protocol]:
for port in firewall["uPnP"][protocol]:
# Clean the mapping of this port
if upnpc.getspecificportmapping(port, protocol):
try:
upnpc.deleteportmapping(port, protocol)
except:
except Exception:
pass
if not enabled:
continue
try:
# Add new port mapping
upnpc.addportmapping(port, protocol, upnpc.lanaddr,
port, 'yunohost firewall: port %d' % port, '')
except:
logger.debug('unable to add port %d using UPnP',
port, exc_info=1)
upnpc.addportmapping(
port,
protocol,
upnpc.lanaddr,
port,
"yunohost firewall: port %d" % port,
"",
)
except Exception:
logger.debug(
"unable to add port %d using UPnP", port, exc_info=1
)
enabled = False
_update_firewall_file(firewall)
if enabled != firewall['uPnP']['enabled']:
if enabled != firewall["uPnP"]["enabled"]:
firewall = firewall_list(raw=True)
firewall['uPnP']['enabled'] = enabled
firewall["uPnP"]["enabled"] = enabled
_update_firewall_file(firewall)
if not no_refresh:
# Display success message if needed
if action == 'enable' and enabled:
logger.success(m18n.n('upnp_enabled'))
elif action == 'disable' and not enabled:
logger.success(m18n.n('upnp_disabled'))
if action == "enable" and enabled:
logger.success(m18n.n("upnp_enabled"))
elif action == "disable" and not enabled:
logger.success(m18n.n("upnp_disabled"))
# Make sure to disable UPnP
elif action != 'disable' and not enabled:
firewall_upnp('disable', no_refresh=True)
elif action != "disable" and not enabled:
firewall_upnp("disable", no_refresh=True)
if not enabled and (action == 'enable' or 1900 in firewall['ipv4']['UDP']):
if not enabled and (action == "enable" or 1900 in firewall["ipv4"]["UDP"]):
# Close unused port 1900
firewall_disallow('UDP', 1900, no_reload=True)
firewall_disallow("UDP", 1900, no_reload=True)
if not no_refresh:
firewall_reload(skip_upnp=True)
if action == 'enable' and not enabled:
raise YunohostError('upnp_port_open_failed')
return {'enabled': enabled}
if action == "enable" and not enabled:
raise YunohostError("upnp_port_open_failed")
return {"enabled": enabled}
def firewall_stop():
@ -426,7 +462,7 @@ def firewall_stop():
"""
if os.system("iptables -w -P INPUT ACCEPT") != 0:
raise YunohostError('iptables_unavailable')
raise YunohostError("iptables_unavailable")
os.system("iptables -w -F")
os.system("iptables -w -X")
@ -437,7 +473,7 @@ def firewall_stop():
os.system("ip6tables -X")
if os.path.exists(UPNP_CRON_JOB):
firewall_upnp('disable')
firewall_upnp("disable")
def _get_ssh_port(default=22):
@ -447,12 +483,12 @@ def _get_ssh_port(default=22):
one if it's not defined.
"""
from moulinette.utils.text import searchf
try:
m = searchf(r'^Port[ \t]+([0-9]+)$',
'/etc/ssh/sshd_config', count=-1)
m = searchf(r"^Port[ \t]+([0-9]+)$", "/etc/ssh/sshd_config", count=-1)
if m:
return int(m)
except:
except Exception:
pass
return default
@ -460,13 +496,17 @@ def _get_ssh_port(default=22):
def _update_firewall_file(rules):
"""Make a backup and write new rules to firewall file"""
os.system("cp {0} {0}.old".format(FIREWALL_FILE))
with open(FIREWALL_FILE, 'w') as f:
with open(FIREWALL_FILE, "w") as f:
yaml.safe_dump(rules, f, default_flow_style=False)
def _on_rule_command_error(returncode, cmd, output):
"""Callback for rules commands error"""
# Log error and continue commands execution
logger.debug('"%s" returned non-zero exit status %d:\n%s',
cmd, returncode, prependlines(output.rstrip(), '> '))
logger.debug(
'"%s" returned non-zero exit status %d:\n%s',
cmd,
returncode,
prependlines(output.rstrip(), "> "),
)
return True

View file

@ -36,10 +36,10 @@ from yunohost.utils.error import YunohostError
from moulinette.utils import log
from moulinette.utils.filesystem import read_json
HOOK_FOLDER = '/usr/share/yunohost/hooks/'
CUSTOM_HOOK_FOLDER = '/etc/yunohost/hooks.d/'
HOOK_FOLDER = "/usr/share/yunohost/hooks/"
CUSTOM_HOOK_FOLDER = "/etc/yunohost/hooks.d/"
logger = log.getActionLogger('yunohost.hook')
logger = log.getActionLogger("yunohost.hook")
def hook_add(app, file):
@ -59,11 +59,11 @@ def hook_add(app, file):
except OSError:
os.makedirs(CUSTOM_HOOK_FOLDER + action)
finalpath = CUSTOM_HOOK_FOLDER + action + '/' + priority + '-' + app
os.system('cp %s %s' % (file, finalpath))
os.system('chown -hR admin: %s' % HOOK_FOLDER)
finalpath = CUSTOM_HOOK_FOLDER + action + "/" + priority + "-" + app
os.system("cp %s %s" % (file, finalpath))
os.system("chown -hR admin: %s" % HOOK_FOLDER)
return {'hook': finalpath}
return {"hook": finalpath}
def hook_remove(app):
@ -78,7 +78,7 @@ def hook_remove(app):
for action in os.listdir(CUSTOM_HOOK_FOLDER):
for script in os.listdir(CUSTOM_HOOK_FOLDER + action):
if script.endswith(app):
os.remove(CUSTOM_HOOK_FOLDER + action + '/' + script)
os.remove(CUSTOM_HOOK_FOLDER + action + "/" + script)
except OSError:
pass
@ -96,34 +96,36 @@ def hook_info(action, name):
priorities = set()
# Search in custom folder first
for h in iglob('{:s}{:s}/*-{:s}'.format(
CUSTOM_HOOK_FOLDER, action, name)):
for h in iglob("{:s}{:s}/*-{:s}".format(CUSTOM_HOOK_FOLDER, action, name)):
priority, _ = _extract_filename_parts(os.path.basename(h))
priorities.add(priority)
hooks.append({
'priority': priority,
'path': h,
})
hooks.append(
{
"priority": priority,
"path": h,
}
)
# Append non-overwritten system hooks
for h in iglob('{:s}{:s}/*-{:s}'.format(
HOOK_FOLDER, action, name)):
for h in iglob("{:s}{:s}/*-{:s}".format(HOOK_FOLDER, action, name)):
priority, _ = _extract_filename_parts(os.path.basename(h))
if priority not in priorities:
hooks.append({
'priority': priority,
'path': h,
})
hooks.append(
{
"priority": priority,
"path": h,
}
)
if not hooks:
raise YunohostError('hook_name_unknown', name=name)
raise YunohostError("hook_name_unknown", name=name)
return {
'action': action,
'name': name,
'hooks': hooks,
"action": action,
"name": name,
"hooks": hooks,
}
def hook_list(action, list_by='name', show_info=False):
def hook_list(action, list_by="name", show_info=False):
"""
List available hooks for an action
@ -136,64 +138,75 @@ def hook_list(action, list_by='name', show_info=False):
result = {}
# Process the property to list hook by
if list_by == 'priority':
if list_by == "priority":
if show_info:
def _append_hook(d, priority, name, path):
# Use the priority as key and a dict of hooks names
# with their info as value
value = {'path': path}
value = {"path": path}
try:
d[priority][name] = value
except KeyError:
d[priority] = {name: value}
else:
def _append_hook(d, priority, name, path):
# Use the priority as key and the name as value
try:
d[priority].add(name)
except KeyError:
d[priority] = set([name])
elif list_by == 'name' or list_by == 'folder':
elif list_by == "name" or list_by == "folder":
if show_info:
def _append_hook(d, priority, name, path):
# Use the name as key and a list of hooks info - the
# executed ones with this name - as value
l = d.get(name, list())
for h in l:
name_list = d.get(name, list())
for h in name_list:
# Only one priority for the hook is accepted
if h['priority'] == priority:
if h["priority"] == priority:
# Custom hooks overwrite system ones and they
# are appended at the end - so overwite it
if h['path'] != path:
h['path'] = path
if h["path"] != path:
h["path"] = path
return
l.append({'priority': priority, 'path': path})
d[name] = l
name_list.append({"priority": priority, "path": path})
d[name] = name_list
else:
if list_by == 'name':
if list_by == "name":
result = set()
def _append_hook(d, priority, name, path):
# Add only the name
d.add(name)
else:
raise YunohostError('hook_list_by_invalid')
raise YunohostError("hook_list_by_invalid")
def _append_folder(d, folder):
# Iterate over and add hook from a folder
for f in os.listdir(folder + action):
if f[0] == '.' or f[-1] == '~' or f.endswith(".pyc") \
or (f.startswith("__") and f.endswith("__")):
if (
f[0] == "."
or f[-1] == "~"
or f.endswith(".pyc")
or (f.startswith("__") and f.endswith("__"))
):
continue
path = '%s%s/%s' % (folder, action, f)
path = "%s%s/%s" % (folder, action, f)
priority, name = _extract_filename_parts(f)
_append_hook(d, priority, name, path)
try:
# Append system hooks first
if list_by == 'folder':
result['system'] = dict() if show_info else set()
_append_folder(result['system'], HOOK_FOLDER)
if list_by == "folder":
result["system"] = dict() if show_info else set()
_append_folder(result["system"], HOOK_FOLDER)
else:
_append_folder(result, HOOK_FOLDER)
except OSError:
@ -201,19 +214,26 @@ def hook_list(action, list_by='name', show_info=False):
try:
# Append custom hooks
if list_by == 'folder':
result['custom'] = dict() if show_info else set()
_append_folder(result['custom'], CUSTOM_HOOK_FOLDER)
if list_by == "folder":
result["custom"] = dict() if show_info else set()
_append_folder(result["custom"], CUSTOM_HOOK_FOLDER)
else:
_append_folder(result, CUSTOM_HOOK_FOLDER)
except OSError:
pass
return {'hooks': result}
return {"hooks": result}
def hook_callback(action, hooks=[], args=None, chdir=None,
env=None, pre_callback=None, post_callback=None):
def hook_callback(
action,
hooks=[],
args=None,
chdir=None,
env=None,
pre_callback=None,
post_callback=None,
):
"""
Execute all scripts binded to an action
@ -235,11 +255,9 @@ def hook_callback(action, hooks=[], args=None, chdir=None,
# Retrieve hooks
if not hooks:
hooks_dict = hook_list(action, list_by='priority',
show_info=True)['hooks']
hooks_dict = hook_list(action, list_by="priority", show_info=True)["hooks"]
else:
hooks_names = hook_list(action, list_by='name',
show_info=True)['hooks']
hooks_names = hook_list(action, list_by="name", show_info=True)["hooks"]
# Add similar hooks to the list
# For example: Having a 16-postfix hook in the list will execute a
@ -247,8 +265,7 @@ def hook_callback(action, hooks=[], args=None, chdir=None,
all_hooks = []
for n in hooks:
for key in hooks_names.keys():
if key == n or key.startswith("%s_" % n) \
and key not in all_hooks:
if key == n or key.startswith("%s_" % n) and key not in all_hooks:
all_hooks.append(key)
# Iterate over given hooks names list
@ -256,49 +273,55 @@ def hook_callback(action, hooks=[], args=None, chdir=None,
try:
hl = hooks_names[n]
except KeyError:
raise YunohostError('hook_name_unknown', n)
raise YunohostError("hook_name_unknown", n)
# Iterate over hooks with this name
for h in hl:
# Update hooks dict
d = hooks_dict.get(h['priority'], dict())
d.update({n: {'path': h['path']}})
hooks_dict[h['priority']] = d
d = hooks_dict.get(h["priority"], dict())
d.update({n: {"path": h["path"]}})
hooks_dict[h["priority"]] = d
if not hooks_dict:
return result
# Validate callbacks
if not callable(pre_callback):
def pre_callback(name, priority, path, args): return args
def pre_callback(name, priority, path, args):
return args
if not callable(post_callback):
def post_callback(name, priority, path, succeed): return None
def post_callback(name, priority, path, succeed):
return None
# Iterate over hooks and execute them
for priority in sorted(hooks_dict):
for name, info in iter(hooks_dict[priority].items()):
state = 'succeed'
path = info['path']
state = "succeed"
path = info["path"]
try:
hook_args = pre_callback(name=name, priority=priority,
path=path, args=args)
hook_return = hook_exec(path, args=hook_args, chdir=chdir, env=env,
raise_on_error=True)[1]
hook_args = pre_callback(
name=name, priority=priority, path=path, args=args
)
hook_return = hook_exec(
path, args=hook_args, chdir=chdir, env=env, raise_on_error=True
)[1]
except YunohostError as e:
state = 'failed'
state = "failed"
hook_return = {}
logger.error(e.strerror, exc_info=1)
post_callback(name=name, priority=priority, path=path,
succeed=False)
post_callback(name=name, priority=priority, path=path, succeed=False)
else:
post_callback(name=name, priority=priority, path=path,
succeed=True)
post_callback(name=name, priority=priority, path=path, succeed=True)
if name not in result:
result[name] = {}
result[name][path] = {'state': state, 'stdreturn': hook_return}
result[name][path] = {"state": state, "stdreturn": hook_return}
return result
def hook_exec(path, args=None, raise_on_error=False,
chdir=None, env=None, return_format="json"):
def hook_exec(
path, args=None, raise_on_error=False, chdir=None, env=None, return_format="json"
):
"""
Execute hook from a file with arguments
@ -311,10 +334,10 @@ def hook_exec(path, args=None, raise_on_error=False,
"""
# Validate hook path
if path[0] != '/':
if path[0] != "/":
path = os.path.realpath(path)
if not os.path.isfile(path):
raise YunohostError('file_does_not_exist', path=path)
raise YunohostError("file_does_not_exist", path=path)
def is_relevant_warning(msg):
@ -329,34 +352,38 @@ def hook_exec(path, args=None, raise_on_error=False,
r"Creating config file .* with new version",
r"Created symlink /etc/systemd",
r"dpkg: warning: while removing .* not empty so not removed",
r"apt-key output should not be parsed"
r"apt-key output should not be parsed",
]
return all(not re.search(w, msg) for w in irrelevant_warnings)
# Define output loggers and call command
loggers = (
lambda l: logger.debug(l.rstrip() + "\r"),
lambda l: logger.warning(l.rstrip()) if is_relevant_warning(l.rstrip()) else logger.debug(l.rstrip()),
lambda l: logger.info(l.rstrip())
lambda l: logger.warning(l.rstrip())
if is_relevant_warning(l.rstrip())
else logger.debug(l.rstrip()),
lambda l: logger.info(l.rstrip()),
)
# Check the type of the hook (bash by default)
# For now we support only python and bash hooks.
hook_type = mimetypes.MimeTypes().guess_type(path)[0]
if hook_type == 'text/x-python':
if hook_type == "text/x-python":
returncode, returndata = _hook_exec_python(path, args, env, loggers)
else:
returncode, returndata = _hook_exec_bash(path, args, chdir, env, return_format, loggers)
returncode, returndata = _hook_exec_bash(
path, args, chdir, env, return_format, loggers
)
# Check and return process' return code
if returncode is None:
if raise_on_error:
raise YunohostError('hook_exec_not_terminated', path=path)
raise YunohostError("hook_exec_not_terminated", path=path)
else:
logger.error(m18n.n('hook_exec_not_terminated', path=path))
logger.error(m18n.n("hook_exec_not_terminated", path=path))
return 1, {}
elif raise_on_error and returncode != 0:
raise YunohostError('hook_exec_failed', path=path)
raise YunohostError("hook_exec_failed", path=path)
return returncode, returndata
@ -366,31 +393,31 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers):
from moulinette.utils.process import call_async_output
# Construct command variables
cmd_args = ''
cmd_args = ""
if args and isinstance(args, list):
# Concatenate escaped arguments
cmd_args = ' '.join(shell_quote(s) for s in args)
cmd_args = " ".join(shell_quote(s) for s in args)
if not chdir:
# use the script directory as current one
chdir, cmd_script = os.path.split(path)
cmd_script = './{0}'.format(cmd_script)
cmd_script = "./{0}".format(cmd_script)
else:
cmd_script = path
# Add Execution dir to environment var
if env is None:
env = {}
env['YNH_CWD'] = chdir
env["YNH_CWD"] = chdir
env['YNH_INTERFACE'] = msettings.get('interface')
env["YNH_INTERFACE"] = msettings.get("interface")
stdreturn = os.path.join(tempfile.mkdtemp(), "stdreturn")
with open(stdreturn, 'w') as f:
f.write('')
env['YNH_STDRETURN'] = stdreturn
with open(stdreturn, "w") as f:
f.write("")
env["YNH_STDRETURN"] = stdreturn
# use xtrace on fd 7 which is redirected to stdout
env['BASH_XTRACEFD'] = "7"
env["BASH_XTRACEFD"] = "7"
cmd = '/bin/bash -x "{script}" {args} 7>&1'
cmd = cmd.format(script=cmd_script, args=cmd_args)
@ -399,25 +426,25 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers):
_env = os.environ.copy()
_env.update(env)
returncode = call_async_output(
cmd, loggers, shell=True, cwd=chdir,
env=_env
)
returncode = call_async_output(cmd, loggers, shell=True, cwd=chdir, env=_env)
raw_content = None
try:
with open(stdreturn, 'r') as f:
with open(stdreturn, "r") as f:
raw_content = f.read()
returncontent = {}
if return_format == "json":
if raw_content != '':
if raw_content != "":
try:
returncontent = read_json(stdreturn)
except Exception as e:
raise YunohostError('hook_json_return_error',
path=path, msg=str(e),
raw_content=raw_content)
raise YunohostError(
"hook_json_return_error",
path=path,
msg=str(e),
raw_content=raw_content,
)
elif return_format == "plain_dict":
for line in raw_content.split("\n"):
@ -426,7 +453,10 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers):
returncontent[key] = value
else:
raise YunohostError("Expected value for return_format is either 'json' or 'plain_dict', got '%s'" % return_format)
raise YunohostError(
"Expected value for return_format is either 'json' or 'plain_dict', got '%s'"
% return_format
)
finally:
stdreturndir = os.path.split(stdreturn)[0]
os.remove(stdreturn)
@ -446,20 +476,21 @@ def _hook_exec_python(path, args, env, loggers):
ret = module.main(args, env, loggers)
# # Assert that the return is a (int, dict) tuple
assert isinstance(ret, tuple) \
and len(ret) == 2 \
and isinstance(ret[0], int) \
and isinstance(ret[1], dict), \
"Module %s did not return a (int, dict) tuple !" % module
assert (
isinstance(ret, tuple)
and len(ret) == 2
and isinstance(ret[0], int)
and isinstance(ret[1], dict)
), ("Module %s did not return a (int, dict) tuple !" % module)
return ret
def _extract_filename_parts(filename):
"""Extract hook parts from filename"""
if '-' in filename:
priority, action = filename.split('-', 1)
if "-" in filename:
priority, action = filename.split("-", 1)
else:
priority = '50'
priority = "50"
action = filename
# Remove extension if there's one
@ -469,7 +500,7 @@ def _extract_filename_parts(filename):
# Taken from Python 3 shlex module --------------------------------------------
_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.UNICODE).search
_find_unsafe = re.compile(r"[^\w@%+=:,./-]", re.UNICODE).search
def shell_quote(s):

View file

@ -40,13 +40,13 @@ from yunohost.utils.packages import get_ynh_package_version
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, read_yaml
CATEGORIES_PATH = '/var/log/yunohost/categories/'
OPERATIONS_PATH = '/var/log/yunohost/categories/operation/'
METADATA_FILE_EXT = '.yml'
LOG_FILE_EXT = '.log'
RELATED_CATEGORIES = ['app', 'domain', 'group', 'service', 'user']
CATEGORIES_PATH = "/var/log/yunohost/categories/"
OPERATIONS_PATH = "/var/log/yunohost/categories/operation/"
METADATA_FILE_EXT = ".yml"
LOG_FILE_EXT = ".log"
RELATED_CATEGORIES = ["app", "domain", "group", "service", "user"]
logger = getActionLogger('yunohost.log')
logger = getActionLogger("yunohost.log")
def log_list(limit=None, with_details=False, with_suboperations=False):
@ -73,7 +73,7 @@ def log_list(limit=None, with_details=False, with_suboperations=False):
for log in logs:
base_filename = log[:-len(METADATA_FILE_EXT)]
base_filename = log[: -len(METADATA_FILE_EXT)]
md_path = os.path.join(OPERATIONS_PATH, log)
entry = {
@ -88,10 +88,12 @@ def log_list(limit=None, with_details=False, with_suboperations=False):
pass
try:
metadata = read_yaml(md_path) or {} # Making sure this is a dict and not None..?
metadata = (
read_yaml(md_path) or {}
) # Making sure this is a dict and not None..?
except Exception as e:
# If we can't read the yaml for some reason, report an error and ignore this entry...
logger.error(m18n.n('log_corrupted_md_file', md_file=md_path, error=e))
logger.error(m18n.n("log_corrupted_md_file", md_file=md_path, error=e))
continue
if with_details:
@ -123,14 +125,16 @@ def log_list(limit=None, with_details=False, with_suboperations=False):
operations = list(reversed(sorted(operations, key=lambda o: o["name"])))
# Reverse the order of log when in cli, more comfortable to read (avoid
# unecessary scrolling)
is_api = msettings.get('interface') == 'api'
is_api = msettings.get("interface") == "api"
if not is_api:
operations = list(reversed(operations))
return {"operation": operations}
def log_show(path, number=None, share=False, filter_irrelevant=False, with_suboperations=False):
def log_show(
path, number=None, share=False, filter_irrelevant=False, with_suboperations=False
):
"""
Display a log file enriched with metadata if any.
@ -156,7 +160,7 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop
r"args_array=.*$",
r"local -A args_array$",
r"ynh_handle_getopts_args",
r"ynh_script_progression"
r"ynh_script_progression",
]
else:
filters = []
@ -164,19 +168,21 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop
def _filter_lines(lines, filters=[]):
filters = [re.compile(f) for f in filters]
return [l for l in lines if not any(f.search(l.strip()) for f in filters)]
return [
line for line in lines if not any(f.search(line.strip()) for f in filters)
]
# Normalize log/metadata paths and filenames
abs_path = path
log_path = None
if not path.startswith('/'):
if not path.startswith("/"):
abs_path = os.path.join(OPERATIONS_PATH, path)
if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT):
log_path = abs_path
if abs_path.endswith(METADATA_FILE_EXT) or abs_path.endswith(LOG_FILE_EXT):
base_path = ''.join(os.path.splitext(abs_path)[:-1])
base_path = "".join(os.path.splitext(abs_path)[:-1])
else:
base_path = abs_path
base_filename = os.path.basename(base_path)
@ -185,17 +191,18 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop
log_path = base_path + LOG_FILE_EXT
if not os.path.exists(md_path) and not os.path.exists(log_path):
raise YunohostError('log_does_exists', log=path)
raise YunohostError("log_does_exists", log=path)
infos = {}
# If it's a unit operation, display the name and the description
if base_path.startswith(CATEGORIES_PATH):
infos["description"] = _get_description_from_name(base_filename)
infos['name'] = base_filename
infos["name"] = base_filename
if share:
from yunohost.utils.yunopaste import yunopaste
content = ""
if os.path.exists(md_path):
content += read_file(md_path)
@ -207,7 +214,7 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop
url = yunopaste(content)
logger.info(m18n.n("log_available_on_yunopaste", url=url))
if msettings.get('interface') == 'api':
if msettings.get("interface") == "api":
return {"url": url}
else:
return
@ -217,17 +224,17 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop
try:
metadata = read_yaml(md_path)
except MoulinetteError as e:
error = m18n.n('log_corrupted_md_file', md_file=md_path, error=e)
error = m18n.n("log_corrupted_md_file", md_file=md_path, error=e)
if os.path.exists(log_path):
logger.warning(error)
else:
raise YunohostError(error)
else:
infos['metadata_path'] = md_path
infos['metadata'] = metadata
infos["metadata_path"] = md_path
infos["metadata"] = metadata
if 'log_path' in metadata:
log_path = metadata['log_path']
if "log_path" in metadata:
log_path = metadata["log_path"]
if with_suboperations:
@ -248,19 +255,25 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop
date = _get_datetime_from_name(base_filename)
except ValueError:
continue
if (date < log_start) or (date > log_start + timedelta(hours=48)):
if (date < log_start) or (
date > log_start + timedelta(hours=48)
):
continue
try:
submetadata = read_yaml(os.path.join(OPERATIONS_PATH, filename))
submetadata = read_yaml(
os.path.join(OPERATIONS_PATH, filename)
)
except Exception:
continue
if submetadata and submetadata.get("parent") == base_filename:
yield {
"name": filename[:-len(METADATA_FILE_EXT)],
"description": _get_description_from_name(filename[:-len(METADATA_FILE_EXT)]),
"success": submetadata.get("success", "?")
"name": filename[: -len(METADATA_FILE_EXT)],
"description": _get_description_from_name(
filename[: -len(METADATA_FILE_EXT)]
),
"success": submetadata.get("success", "?"),
}
metadata["suboperations"] = list(suboperations())
@ -268,6 +281,7 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop
# Display logs if exist
if os.path.exists(log_path):
from yunohost.service import _tail
if number and filters:
logs = _tail(log_path, int(number * 4))
elif number:
@ -277,17 +291,21 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop
logs = _filter_lines(logs, filters)
if number:
logs = logs[-number:]
infos['log_path'] = log_path
infos['logs'] = logs
infos["log_path"] = log_path
infos["logs"] = logs
return infos
def log_share(path):
return log_show(path, share=True)
def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'],
exclude=['password'], operation_key=None):
def is_unit_operation(
entities=["app", "domain", "group", "service", "user"],
exclude=["password"],
operation_key=None,
):
"""
Configure quickly a unit operation
@ -309,6 +327,7 @@ def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'],
'log_' is present in locales/en.json otherwise it won't be translatable.
"""
def decorate(func):
def func_wrapper(*args, **kwargs):
op_key = operation_key
@ -322,9 +341,10 @@ def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'],
# know name of each args (so we need to use kwargs instead of args)
if len(args) > 0:
from inspect import getargspec
keys = getargspec(func).args
if 'operation_logger' in keys:
keys.remove('operation_logger')
if "operation_logger" in keys:
keys.remove("operation_logger")
for k, arg in enumerate(args):
kwargs[keys[k]] = arg
args = ()
@ -364,12 +384,13 @@ def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'],
else:
operation_logger.success()
return result
return func_wrapper
return decorate
class RedactingFormatter(Formatter):
def __init__(self, format_string, data_to_redact):
super(RedactingFormatter, self).__init__(format_string)
self.data_to_redact = data_to_redact
@ -378,7 +399,11 @@ class RedactingFormatter(Formatter):
msg = super(RedactingFormatter, self).format(record)
self.identify_data_to_redact(msg)
for data in self.data_to_redact:
msg = msg.replace(data, "**********")
# we check that data is not empty string,
# otherwise this may lead to super epic stuff
# (try to run "foo".replace("", "bar"))
if data:
msg = msg.replace(data, "**********")
return msg
def identify_data_to_redact(self, record):
@ -389,11 +414,19 @@ class RedactingFormatter(Formatter):
# This matches stuff like db_pwd=the_secret or admin_password=other_secret
# (the secret part being at least 3 chars to avoid catching some lines like just "db_pwd=")
# Some names like "key" or "manifest_key" are ignored, used in helpers like ynh_app_setting_set or ynh_read_manifest
match = re.search(r'(pwd|pass|password|secret|\w+key|token)=(\S{3,})$', record.strip())
if match and match.group(2) not in self.data_to_redact and match.group(1) not in ["key", "manifest_key"]:
match = re.search(
r"(pwd|pass|password|secret|\w+key|token)=(\S{3,})$", record.strip()
)
if (
match
and match.group(2) not in self.data_to_redact
and match.group(1) not in ["key", "manifest_key"]
):
self.data_to_redact.append(match.group(2))
except Exception as e:
logger.warning("Failed to parse line to try to identify data to redact ... : %s" % e)
logger.warning(
"Failed to parse line to try to identify data to redact ... : %s" % e
)
class OperationLogger(object):
@ -462,13 +495,19 @@ class OperationLogger(object):
# 4. if among those file, there's an operation log file, we use the id
# of the most recent file
recent_operation_logs = sorted(glob.iglob(OPERATIONS_PATH + "*.log"), key=os.path.getctime, reverse=True)[:20]
recent_operation_logs = sorted(
glob.iglob(OPERATIONS_PATH + "*.log"), key=os.path.getctime, reverse=True
)[:20]
proc = psutil.Process().parent()
while proc is not None:
# We use proc.open_files() to list files opened / actively used by this proc
# We only keep files matching a recent yunohost operation log
active_logs = sorted([f.path for f in proc.open_files() if f.path in recent_operation_logs], key=os.path.getctime, reverse=True)
active_logs = sorted(
[f.path for f in proc.open_files() if f.path in recent_operation_logs],
key=os.path.getctime,
reverse=True,
)
if active_logs != []:
# extra the log if from the full path
return os.path.basename(active_logs[0])[:-4]
@ -514,10 +553,12 @@ class OperationLogger(object):
# N.B. : the subtle thing here is that the class will remember a pointer to the list,
# so we can directly append stuff to self.data_to_redact and that'll be automatically
# propagated to the RedactingFormatter
self.file_handler.formatter = RedactingFormatter('%(asctime)s: %(levelname)s - %(message)s', self.data_to_redact)
self.file_handler.formatter = RedactingFormatter(
"%(asctime)s: %(levelname)s - %(message)s", self.data_to_redact
)
# Listen to the root logger
self.logger = getLogger('yunohost')
self.logger = getLogger("yunohost")
self.logger.addHandler(self.file_handler)
def flush(self):
@ -529,7 +570,7 @@ class OperationLogger(object):
for data in self.data_to_redact:
# N.B. : we need quotes here, otherwise yaml isn't happy about loading the yml later
dump = dump.replace(data, "'**********'")
with open(self.md_path, 'w') as outfile:
with open(self.md_path, "w") as outfile:
outfile.write(dump)
@property
@ -553,7 +594,7 @@ class OperationLogger(object):
# We use the name of the first related thing
name.append(self.related_to[0][1])
self._name = '-'.join(name)
self._name = "-".join(name)
return self._name
@property
@ -563,19 +604,19 @@ class OperationLogger(object):
"""
data = {
'started_at': self.started_at,
'operation': self.operation,
'parent': self.parent,
'yunohost_version': get_ynh_package_version("yunohost")["version"],
'interface': msettings.get('interface'),
"started_at": self.started_at,
"operation": self.operation,
"parent": self.parent,
"yunohost_version": get_ynh_package_version("yunohost")["version"],
"interface": msettings.get("interface"),
}
if self.related_to is not None:
data['related_to'] = self.related_to
data["related_to"] = self.related_to
if self.ended_at is not None:
data['ended_at'] = self.ended_at
data['success'] = self._success
data["ended_at"] = self.ended_at
data["success"] = self._success
if self.error is not None:
data['error'] = self._error
data["error"] = self._error
# TODO: detect if 'extra' erase some key of 'data'
data.update(self.extra)
return data
@ -608,21 +649,23 @@ class OperationLogger(object):
self.logger.removeHandler(self.file_handler)
self.file_handler.close()
is_api = msettings.get('interface') == 'api'
is_api = msettings.get("interface") == "api"
desc = _get_description_from_name(self.name)
if error is None:
if is_api:
msg = m18n.n('log_link_to_log', name=self.name, desc=desc)
msg = m18n.n("log_link_to_log", name=self.name, desc=desc)
else:
msg = m18n.n('log_help_to_get_log', name=self.name, desc=desc)
msg = m18n.n("log_help_to_get_log", name=self.name, desc=desc)
logger.debug(msg)
else:
if is_api:
msg = "<strong>" + m18n.n('log_link_to_failed_log',
name=self.name, desc=desc) + "</strong>"
msg = (
"<strong>"
+ m18n.n("log_link_to_failed_log", name=self.name, desc=desc)
+ "</strong>"
)
else:
msg = m18n.n('log_help_to_get_failed_log', name=self.name,
desc=desc)
msg = m18n.n("log_help_to_get_failed_log", name=self.name, desc=desc)
logger.info(msg)
self.flush()
return msg
@ -636,7 +679,7 @@ class OperationLogger(object):
if self.ended_at is not None or self.started_at is None:
return
else:
self.error(m18n.n('log_operation_unit_unclosed_properly'))
self.error(m18n.n("log_operation_unit_unclosed_properly"))
def _get_datetime_from_name(name):

View file

@ -34,7 +34,7 @@ from moulinette.utils.log import getActionLogger
from yunohost.utils.error import YunohostError
from yunohost.log import is_unit_operation
logger = getActionLogger('yunohost.user')
logger = getActionLogger("yunohost.user")
SYSTEM_PERMS = ["mail", "xmpp", "sftp", "ssh"]
@ -45,7 +45,9 @@ SYSTEM_PERMS = ["mail", "xmpp", "sftp", "ssh"]
#
def user_permission_list(short=False, full=False, ignore_system_perms=False, absolute_urls=False):
def user_permission_list(
short=False, full=False, ignore_system_perms=False, absolute_urls=False
):
"""
List permissions and corresponding accesses
"""
@ -53,32 +55,50 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False, abs
# Fetch relevant informations
from yunohost.app import app_setting, _installed_apps
from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract
ldap = _get_ldap_interface()
permissions_infos = ldap.search('ou=permission,dc=yunohost,dc=org',
'(objectclass=permissionYnh)',
["cn", 'groupPermission', 'inheritPermission',
'URL', 'additionalUrls', 'authHeader', 'label', 'showTile', 'isProtected'])
permissions_infos = ldap.search(
"ou=permission,dc=yunohost,dc=org",
"(objectclass=permissionYnh)",
[
"cn",
"groupPermission",
"inheritPermission",
"URL",
"additionalUrls",
"authHeader",
"label",
"showTile",
"isProtected",
],
)
# Parse / organize information to be outputed
apps = sorted(_installed_apps())
apps_base_path = {app: app_setting(app, 'domain') + app_setting(app, 'path')
for app in apps
if app_setting(app, 'domain') and app_setting(app, 'path')}
apps_base_path = {
app: app_setting(app, "domain") + app_setting(app, "path")
for app in apps
if app_setting(app, "domain") and app_setting(app, "path")
}
permissions = {}
for infos in permissions_infos:
name = infos['cn'][0]
name = infos["cn"][0]
if ignore_system_perms and name.split(".")[0] in SYSTEM_PERMS:
continue
app = name.split('.')[0]
app = name.split(".")[0]
perm = {}
perm["allowed"] = [_ldap_path_extract(p, "cn") for p in infos.get('groupPermission', [])]
perm["allowed"] = [
_ldap_path_extract(p, "cn") for p in infos.get("groupPermission", [])
]
if full:
perm["corresponding_users"] = [_ldap_path_extract(p, "uid") for p in infos.get('inheritPermission', [])]
perm["corresponding_users"] = [
_ldap_path_extract(p, "uid") for p in infos.get("inheritPermission", [])
]
perm["auth_header"] = infos.get("authHeader", [False])[0] == "TRUE"
perm["label"] = infos.get("label", [None])[0]
perm["show_tile"] = infos.get("showTile", [False])[0] == "TRUE"
@ -87,19 +107,29 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False, abs
perm["additional_urls"] = infos.get("additionalUrls", [])
if absolute_urls:
app_base_path = apps_base_path[app] if app in apps_base_path else "" # Meh in some situation where the app is currently installed/removed, this function may be called and we still need to act as if the corresponding permission indeed exists ... dunno if that's really the right way to proceed but okay.
app_base_path = (
apps_base_path[app] if app in apps_base_path else ""
) # Meh in some situation where the app is currently installed/removed, this function may be called and we still need to act as if the corresponding permission indeed exists ... dunno if that's really the right way to proceed but okay.
perm["url"] = _get_absolute_url(perm["url"], app_base_path)
perm["additional_urls"] = [_get_absolute_url(url, app_base_path) for url in perm["additional_urls"]]
perm["additional_urls"] = [
_get_absolute_url(url, app_base_path)
for url in perm["additional_urls"]
]
permissions[name] = perm
# Make sure labels for sub-permissions are the form " Applabel (Sublabel) "
if full:
subpermissions = {k: v for k, v in permissions.items() if not k.endswith(".main")}
subpermissions = {
k: v for k, v in permissions.items() if not k.endswith(".main")
}
for name, infos in subpermissions.items():
main_perm_name = name.split(".")[0] + ".main"
if main_perm_name not in permissions:
logger.debug("Uhoh, unknown permission %s ? (Maybe we're in the process or deleting the perm for this app...)" % main_perm_name)
logger.debug(
"Uhoh, unknown permission %s ? (Maybe we're in the process or deleting the perm for this app...)"
% main_perm_name
)
continue
main_perm_label = permissions[main_perm_name]["label"]
infos["sublabel"] = infos["label"]
@ -108,13 +138,21 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False, abs
if short:
permissions = list(permissions.keys())
return {'permissions': permissions}
return {"permissions": permissions}
@is_unit_operation()
def user_permission_update(operation_logger, permission, add=None, remove=None,
label=None, show_tile=None,
protected=None, force=False, sync_perm=True):
def user_permission_update(
operation_logger,
permission,
add=None,
remove=None,
label=None,
show_tile=None,
protected=None,
force=False,
sync_perm=True,
):
"""
Allow or Disallow a user or group to a permission for a specific application
@ -137,43 +175,57 @@ def user_permission_update(operation_logger, permission, add=None, remove=None,
# Refuse to add "visitors" to mail, xmpp ... they require an account to make sense.
if add and "visitors" in add and permission.split(".")[0] in SYSTEM_PERMS:
raise YunohostError('permission_require_account', permission=permission)
raise YunohostError("permission_require_account", permission=permission)
# Refuse to add "visitors" to protected permission
if ((add and "visitors" in add and existing_permission["protected"]) or
(remove and "visitors" in remove and existing_permission["protected"])) and not force:
raise YunohostError('permission_protected', permission=permission)
if (
(add and "visitors" in add and existing_permission["protected"])
or (remove and "visitors" in remove and existing_permission["protected"])
) and not force:
raise YunohostError("permission_protected", permission=permission)
# Fetch currently allowed groups for this permission
current_allowed_groups = existing_permission["allowed"]
operation_logger.related_to.append(('app', permission.split(".")[0]))
operation_logger.related_to.append(("app", permission.split(".")[0]))
# Compute new allowed group list (and make sure what we're doing make sense)
new_allowed_groups = copy.copy(current_allowed_groups)
all_existing_groups = user_group_list()['groups'].keys()
all_existing_groups = user_group_list()["groups"].keys()
if add:
groups_to_add = [add] if not isinstance(add, list) else add
for group in groups_to_add:
if group not in all_existing_groups:
raise YunohostError('group_unknown', group=group)
raise YunohostError("group_unknown", group=group)
if group in current_allowed_groups:
logger.warning(m18n.n('permission_already_allowed', permission=permission, group=group))
logger.warning(
m18n.n(
"permission_already_allowed", permission=permission, group=group
)
)
else:
operation_logger.related_to.append(('group', group))
operation_logger.related_to.append(("group", group))
new_allowed_groups += [group]
if remove:
groups_to_remove = [remove] if not isinstance(remove, list) else remove
for group in groups_to_remove:
if group not in current_allowed_groups:
logger.warning(m18n.n('permission_already_disallowed', permission=permission, group=group))
logger.warning(
m18n.n(
"permission_already_disallowed",
permission=permission,
group=group,
)
)
else:
operation_logger.related_to.append(('group', group))
operation_logger.related_to.append(("group", group))
new_allowed_groups = [g for g in new_allowed_groups if g not in groups_to_remove]
new_allowed_groups = [
g for g in new_allowed_groups if g not in groups_to_remove
]
# If we end up with something like allowed groups is ["all_users", "volunteers"]
# we shall warn the users that they should probably choose between one or
@ -191,17 +243,32 @@ def user_permission_update(operation_logger, permission, add=None, remove=None,
else:
show_tile = False
if existing_permission['url'] and existing_permission['url'].startswith('re:') and show_tile:
logger.warning(m18n.n('regex_incompatible_with_tile', regex=existing_permission['url'], permission=permission))
if (
existing_permission["url"]
and existing_permission["url"].startswith("re:")
and show_tile
):
logger.warning(
m18n.n(
"regex_incompatible_with_tile",
regex=existing_permission["url"],
permission=permission,
)
)
# Commit the new allowed group list
operation_logger.start()
new_permission = _update_ldap_group_permission(permission=permission, allowed=new_allowed_groups,
label=label, show_tile=show_tile,
protected=protected, sync_perm=sync_perm)
new_permission = _update_ldap_group_permission(
permission=permission,
allowed=new_allowed_groups,
label=label,
show_tile=show_tile,
protected=protected,
sync_perm=sync_perm,
)
logger.debug(m18n.n('permission_updated', permission=permission))
logger.debug(m18n.n("permission_updated", permission=permission))
return new_permission
@ -229,12 +296,14 @@ def user_permission_reset(operation_logger, permission, sync_perm=True):
# Update permission with default (all_users)
operation_logger.related_to.append(('app', permission.split(".")[0]))
operation_logger.related_to.append(("app", permission.split(".")[0]))
operation_logger.start()
new_permission = _update_ldap_group_permission(permission=permission, allowed="all_users", sync_perm=sync_perm)
new_permission = _update_ldap_group_permission(
permission=permission, allowed="all_users", sync_perm=sync_perm
)
logger.debug(m18n.n('permission_updated', permission=permission))
logger.debug(m18n.n("permission_updated", permission=permission))
return new_permission
@ -253,9 +322,11 @@ def user_permission_info(permission):
# Fetch existing permission
existing_permission = user_permission_list(full=True)["permissions"].get(permission, None)
existing_permission = user_permission_list(full=True)["permissions"].get(
permission, None
)
if existing_permission is None:
raise YunohostError('permission_not_found', permission=permission)
raise YunohostError("permission_not_found", permission=permission)
return existing_permission
@ -270,10 +341,18 @@ def user_permission_info(permission):
@is_unit_operation()
def permission_create(operation_logger, permission, allowed=None,
url=None, additional_urls=None, auth_header=True,
label=None, show_tile=False,
protected=False, sync_perm=True):
def permission_create(
operation_logger,
permission,
allowed=None,
url=None,
additional_urls=None,
auth_header=True,
label=None,
show_tile=False,
protected=False,
sync_perm=True,
):
"""
Create a new permission for a specific application
@ -301,6 +380,7 @@ def permission_create(operation_logger, permission, allowed=None,
from yunohost.utils.ldap import _get_ldap_interface
from yunohost.user import user_group_list
ldap = _get_ldap_interface()
# By default, manipulate main permission
@ -308,9 +388,10 @@ def permission_create(operation_logger, permission, allowed=None,
permission = permission + ".main"
# Validate uniqueness of permission in LDAP
if ldap.get_conflict({'cn': permission},
base_dn='ou=permission,dc=yunohost,dc=org'):
raise YunohostError('permission_already_exist', permission=permission)
if ldap.get_conflict(
{"cn": permission}, base_dn="ou=permission,dc=yunohost,dc=org"
):
raise YunohostError("permission_already_exist", permission=permission)
# Get random GID
all_gid = {x.gr_gid for x in grp.getgrall()}
@ -323,13 +404,19 @@ def permission_create(operation_logger, permission, allowed=None,
app, subperm = permission.split(".")
attr_dict = {
'objectClass': ['top', 'permissionYnh', 'posixGroup'],
'cn': str(permission),
'gidNumber': gid,
'authHeader': ['TRUE'],
'label': [str(label) if label else (subperm if subperm != "main" else app.title())],
'showTile': ['FALSE'], # Dummy value, it will be fixed when we call '_update_ldap_group_permission'
'isProtected': ['FALSE'] # Dummy value, it will be fixed when we call '_update_ldap_group_permission'
"objectClass": ["top", "permissionYnh", "posixGroup"],
"cn": str(permission),
"gidNumber": gid,
"authHeader": ["TRUE"],
"label": [
str(label) if label else (subperm if subperm != "main" else app.title())
],
"showTile": [
"FALSE"
], # Dummy value, it will be fixed when we call '_update_ldap_group_permission'
"isProtected": [
"FALSE"
], # Dummy value, it will be fixed when we call '_update_ldap_group_permission'
}
if allowed is not None:
@ -337,34 +424,53 @@ def permission_create(operation_logger, permission, allowed=None,
allowed = [allowed]
# Validate that the groups to add actually exist
all_existing_groups = user_group_list()['groups'].keys()
all_existing_groups = user_group_list()["groups"].keys()
for group in allowed or []:
if group not in all_existing_groups:
raise YunohostError('group_unknown', group=group)
raise YunohostError("group_unknown", group=group)
operation_logger.related_to.append(('app', permission.split(".")[0]))
operation_logger.related_to.append(("app", permission.split(".")[0]))
operation_logger.start()
try:
ldap.add('cn=%s,ou=permission' % permission, attr_dict)
ldap.add("cn=%s,ou=permission" % permission, attr_dict)
except Exception as e:
raise YunohostError('permission_creation_failed', permission=permission, error=e)
raise YunohostError(
"permission_creation_failed", permission=permission, error=e
)
permission_url(permission, url=url, add_url=additional_urls, auth_header=auth_header,
sync_perm=False)
permission_url(
permission,
url=url,
add_url=additional_urls,
auth_header=auth_header,
sync_perm=False,
)
new_permission = _update_ldap_group_permission(permission=permission, allowed=allowed,
label=label, show_tile=show_tile,
protected=protected, sync_perm=sync_perm)
new_permission = _update_ldap_group_permission(
permission=permission,
allowed=allowed,
label=label,
show_tile=show_tile,
protected=protected,
sync_perm=sync_perm,
)
logger.debug(m18n.n('permission_created', permission=permission))
logger.debug(m18n.n("permission_created", permission=permission))
return new_permission
@is_unit_operation()
def permission_url(operation_logger, permission,
url=None, add_url=None, remove_url=None, auth_header=None,
clear_urls=False, sync_perm=True):
def permission_url(
operation_logger,
permission,
url=None,
add_url=None,
remove_url=None,
auth_header=None,
clear_urls=False,
sync_perm=True,
):
"""
Update urls related to a permission for a specific application
@ -378,19 +484,20 @@ def permission_url(operation_logger, permission,
"""
from yunohost.app import app_setting
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
# By default, manipulate main permission
if "." not in permission:
permission = permission + ".main"
app = permission.split('.')[0]
app = permission.split(".")[0]
if url or add_url:
domain = app_setting(app, 'domain')
path = app_setting(app, 'path')
domain = app_setting(app, "domain")
path = app_setting(app, "path")
if domain is None or path is None:
raise YunohostError('unknown_main_domain_path', app=app)
raise YunohostError("unknown_main_domain_path", app=app)
else:
app_main_path = domain + path
@ -398,15 +505,17 @@ def permission_url(operation_logger, permission,
existing_permission = user_permission_info(permission)
show_tile = existing_permission['show_tile']
show_tile = existing_permission["show_tile"]
if url is None:
url = existing_permission["url"]
else:
url = _validate_and_sanitize_permission_url(url, app_main_path, app)
if url.startswith('re:') and existing_permission['show_tile']:
logger.warning(m18n.n('regex_incompatible_with_tile', regex=url, permission=permission))
if url.startswith("re:") and existing_permission["show_tile"]:
logger.warning(
m18n.n("regex_incompatible_with_tile", regex=url, permission=permission)
)
show_tile = False
current_additional_urls = existing_permission["additional_urls"]
@ -415,7 +524,11 @@ def permission_url(operation_logger, permission,
if add_url:
for ur in add_url:
if ur in current_additional_urls:
logger.warning(m18n.n('additional_urls_already_added', permission=permission, url=ur))
logger.warning(
m18n.n(
"additional_urls_already_added", permission=permission, url=ur
)
)
else:
ur = _validate_and_sanitize_permission_url(ur, app_main_path, app)
new_additional_urls += [ur]
@ -423,12 +536,16 @@ def permission_url(operation_logger, permission,
if remove_url:
for ur in remove_url:
if ur not in current_additional_urls:
logger.warning(m18n.n('additional_urls_already_removed', permission=permission, url=ur))
logger.warning(
m18n.n(
"additional_urls_already_removed", permission=permission, url=ur
)
)
new_additional_urls = [u for u in new_additional_urls if u not in remove_url]
if auth_header is None:
auth_header = existing_permission['auth_header']
auth_header = existing_permission["auth_header"]
if clear_urls:
url = None
@ -440,21 +557,26 @@ def permission_url(operation_logger, permission,
# Actually commit the change
operation_logger.related_to.append(('app', permission.split(".")[0]))
operation_logger.related_to.append(("app", permission.split(".")[0]))
operation_logger.start()
try:
ldap.update('cn=%s,ou=permission' % permission, {'URL': [url] if url is not None else [],
'additionalUrls': new_additional_urls,
'authHeader': [str(auth_header).upper()],
'showTile': [str(show_tile).upper()], })
ldap.update(
"cn=%s,ou=permission" % permission,
{
"URL": [url] if url is not None else [],
"additionalUrls": new_additional_urls,
"authHeader": [str(auth_header).upper()],
"showTile": [str(show_tile).upper()],
},
)
except Exception as e:
raise YunohostError('permission_update_failed', permission=permission, error=e)
raise YunohostError("permission_update_failed", permission=permission, error=e)
if sync_perm:
permission_sync_to_user()
logger.debug(m18n.n('permission_updated', permission=permission))
logger.debug(m18n.n("permission_updated", permission=permission))
return user_permission_info(permission)
@ -472,9 +594,10 @@ def permission_delete(operation_logger, permission, force=False, sync_perm=True)
permission = permission + ".main"
if permission.endswith(".main") and not force:
raise YunohostError('permission_cannot_remove_main')
raise YunohostError("permission_cannot_remove_main")
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
# Make sure this permission exists
@ -483,17 +606,19 @@ def permission_delete(operation_logger, permission, force=False, sync_perm=True)
# Actually delete the permission
operation_logger.related_to.append(('app', permission.split(".")[0]))
operation_logger.related_to.append(("app", permission.split(".")[0]))
operation_logger.start()
try:
ldap.remove('cn=%s,ou=permission' % permission)
ldap.remove("cn=%s,ou=permission" % permission)
except Exception as e:
raise YunohostError('permission_deletion_failed', permission=permission, error=e)
raise YunohostError(
"permission_deletion_failed", permission=permission, error=e
)
if sync_perm:
permission_sync_to_user()
logger.debug(m18n.n('permission_deleted', permission=permission))
logger.debug(m18n.n("permission_deleted", permission=permission))
def permission_sync_to_user():
@ -505,6 +630,7 @@ def permission_sync_to_user():
from yunohost.app import app_ssowatconf
from yunohost.user import user_group_list
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
groups = user_group_list(full=True)["groups"]
@ -516,7 +642,13 @@ def permission_sync_to_user():
currently_allowed_users = set(permission_infos["corresponding_users"])
# These are the users that should be allowed because they are member of a group that is allowed for this permission ...
should_be_allowed_users = set([user for group in permission_infos["allowed"] for user in groups[group]["members"]])
should_be_allowed_users = set(
[
user
for group in permission_infos["allowed"]
for user in groups[group]["members"]
]
)
# Note that a LDAP operation with the same value that is in LDAP crash SLAP.
# So we need to check before each ldap operation that we really change something in LDAP
@ -524,47 +656,55 @@ def permission_sync_to_user():
# We're all good, this permission is already correctly synchronized !
continue
new_inherited_perms = {'inheritPermission': ["uid=%s,ou=users,dc=yunohost,dc=org" % u for u in should_be_allowed_users],
'memberUid': should_be_allowed_users}
new_inherited_perms = {
"inheritPermission": [
"uid=%s,ou=users,dc=yunohost,dc=org" % u
for u in should_be_allowed_users
],
"memberUid": should_be_allowed_users,
}
# Commit the change with the new inherited stuff
try:
ldap.update('cn=%s,ou=permission' % permission_name, new_inherited_perms)
ldap.update("cn=%s,ou=permission" % permission_name, new_inherited_perms)
except Exception as e:
raise YunohostError('permission_update_failed', permission=permission_name, error=e)
raise YunohostError(
"permission_update_failed", permission=permission_name, error=e
)
logger.debug("The permission database has been resynchronized")
app_ssowatconf()
# Reload unscd, otherwise the group ain't propagated to the LDAP database
os.system('nscd --invalidate=passwd')
os.system('nscd --invalidate=group')
os.system("nscd --invalidate=passwd")
os.system("nscd --invalidate=group")
def _update_ldap_group_permission(permission, allowed,
label=None, show_tile=None,
protected=None, sync_perm=True):
def _update_ldap_group_permission(
permission, allowed, label=None, show_tile=None, protected=None, sync_perm=True
):
"""
Internal function that will rewrite user permission
Internal function that will rewrite user permission
permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors)
allowed -- (optional) A list of group/user to allow for the permission
label -- (optional) Define a name for the permission. This label will be shown on the SSO and in the admin
show_tile -- (optional) Define if a tile will be shown in the SSO
protected -- (optional) Define if the permission can be added/removed to the visitor group
permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors)
allowed -- (optional) A list of group/user to allow for the permission
label -- (optional) Define a name for the permission. This label will be shown on the SSO and in the admin
show_tile -- (optional) Define if a tile will be shown in the SSO
protected -- (optional) Define if the permission can be added/removed to the visitor group
Assumptions made, that should be checked before calling this function:
- the permission does currently exists ...
- the 'allowed' list argument is *different* from the current
permission state ... otherwise ldap will miserably fail in such
case...
- the 'allowed' list contains *existing* groups.
Assumptions made, that should be checked before calling this function:
- the permission does currently exists ...
- the 'allowed' list argument is *different* from the current
permission state ... otherwise ldap will miserably fail in such
case...
- the 'allowed' list contains *existing* groups.
"""
from yunohost.hook import hook_callback
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
existing_permission = user_permission_info(permission)
@ -575,7 +715,9 @@ def _update_ldap_group_permission(permission, allowed,
allowed = [allowed] if not isinstance(allowed, list) else allowed
# Guarantee uniqueness of values in allowed, which would otherwise make ldap.update angry.
allowed = set(allowed)
update['groupPermission'] = ['cn=' + g + ',ou=groups,dc=yunohost,dc=org' for g in allowed]
update["groupPermission"] = [
"cn=" + g + ",ou=groups,dc=yunohost,dc=org" for g in allowed
]
if label is not None:
update["label"] = [str(label)]
@ -586,18 +728,25 @@ def _update_ldap_group_permission(permission, allowed,
if show_tile is not None:
if show_tile is True:
if not existing_permission['url']:
logger.warning(m18n.n('show_tile_cant_be_enabled_for_url_not_defined', permission=permission))
if not existing_permission["url"]:
logger.warning(
m18n.n(
"show_tile_cant_be_enabled_for_url_not_defined",
permission=permission,
)
)
show_tile = False
elif existing_permission['url'].startswith('re:'):
logger.warning(m18n.n('show_tile_cant_be_enabled_for_regex', permission=permission))
elif existing_permission["url"].startswith("re:"):
logger.warning(
m18n.n("show_tile_cant_be_enabled_for_regex", permission=permission)
)
show_tile = False
update["showTile"] = [str(show_tile).upper()]
try:
ldap.update('cn=%s,ou=permission' % permission, update)
ldap.update("cn=%s,ou=permission" % permission, update)
except Exception as e:
raise YunohostError('permission_update_failed', permission=permission, error=e)
raise YunohostError("permission_update_failed", permission=permission, error=e)
# Trigger permission sync if asked
@ -620,13 +769,33 @@ def _update_ldap_group_permission(permission, allowed,
effectively_added_users = new_corresponding_users - old_corresponding_users
effectively_removed_users = old_corresponding_users - new_corresponding_users
effectively_added_group = new_allowed_users - old_allowed_users - effectively_added_users
effectively_removed_group = old_allowed_users - new_allowed_users - effectively_removed_users
effectively_added_group = (
new_allowed_users - old_allowed_users - effectively_added_users
)
effectively_removed_group = (
old_allowed_users - new_allowed_users - effectively_removed_users
)
if effectively_added_users or effectively_added_group:
hook_callback('post_app_addaccess', args=[app, ','.join(effectively_added_users), sub_permission, ','.join(effectively_added_group)])
hook_callback(
"post_app_addaccess",
args=[
app,
",".join(effectively_added_users),
sub_permission,
",".join(effectively_added_group),
],
)
if effectively_removed_users or effectively_removed_group:
hook_callback('post_app_removeaccess', args=[app, ','.join(effectively_removed_users), sub_permission, ','.join(effectively_removed_group)])
hook_callback(
"post_app_removeaccess",
args=[
app,
",".join(effectively_removed_users),
sub_permission,
",".join(effectively_removed_group),
],
)
return new_permission
@ -642,10 +811,10 @@ def _get_absolute_url(url, base_path):
base_path = base_path.rstrip("/")
if url is None:
return None
if url.startswith('/'):
if url.startswith("/"):
return base_path + url.rstrip("/")
if url.startswith('re:/'):
return 're:' + base_path.replace('.', '\\.') + url[3:]
if url.startswith("re:/"):
return "re:" + base_path.replace(".", "\\.") + url[3:]
else:
return url
@ -670,49 +839,51 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app):
re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
We can also have less-trivial regexes like:
re:^\/api\/.*|\/scripts\/api.js$
re:^/api/.*|/scripts/api.js$
"""
from yunohost.domain import domain_list
from yunohost.app import _assert_no_conflicting_apps
domains = domain_list()['domains']
domains = domain_list()["domains"]
#
# Regexes
#
def validate_regex(regex):
if '%' in regex:
logger.warning("/!\\ Packagers! You are probably using a lua regex. You should use a PCRE regex instead.")
if "%" in regex:
logger.warning(
"/!\\ Packagers! You are probably using a lua regex. You should use a PCRE regex instead."
)
return
try:
re.compile(regex)
except Exception:
raise YunohostError('invalid_regex', regex=regex)
raise YunohostError("invalid_regex", regex=regex)
if url.startswith('re:'):
if url.startswith("re:"):
# regex without domain
# we check for the first char after 're:'
if url[3] in ['/', '^', '\\']:
if url[3] in ["/", "^", "\\"]:
validate_regex(url[3:])
return url
# regex with domain
if '/' not in url:
raise YunohostError('regex_with_only_domain')
domain, path = url[3:].split('/', 1)
path = '/' + path
if "/" not in url:
raise YunohostError("regex_with_only_domain")
domain, path = url[3:].split("/", 1)
path = "/" + path
if domain.replace('%', '').replace('\\', '') not in domains:
raise YunohostError('domain_name_unknown', domain=domain)
if domain.replace("%", "").replace("\\", "") not in domains:
raise YunohostError("domain_name_unknown", domain=domain)
validate_regex(path)
return 're:' + domain + path
return "re:" + domain + path
#
# "Regular" URIs
@ -720,13 +891,13 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app):
def split_domain_path(url):
url = url.strip("/")
(domain, path) = url.split('/', 1) if "/" in url else (url, "/")
(domain, path) = url.split("/", 1) if "/" in url else (url, "/")
if path != "/":
path = "/" + path
return (domain, path)
# uris without domain
if url.startswith('/'):
if url.startswith("/"):
# if url is for example /admin/
# we want sanitized_url to be: /admin
# and (domain, path) to be : (domain.tld, /app/admin)
@ -743,7 +914,7 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app):
sanitized_url = domain + path
if domain not in domains:
raise YunohostError('domain_name_unknown', domain=domain)
raise YunohostError("domain_name_unknown", domain=domain)
_assert_no_conflicting_apps(domain, path, ignore_app=app)

View file

@ -35,19 +35,25 @@ from yunohost.utils.error import YunohostError
from yunohost.log import is_unit_operation
from yunohost.hook import hook_callback, hook_list
BASE_CONF_PATH = '/home/yunohost.conf'
BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup')
PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, 'pending')
REGEN_CONF_FILE = '/etc/yunohost/regenconf.yml'
BASE_CONF_PATH = "/home/yunohost.conf"
BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, "backup")
PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, "pending")
REGEN_CONF_FILE = "/etc/yunohost/regenconf.yml"
logger = log.getActionLogger('yunohost.regenconf')
logger = log.getActionLogger("yunohost.regenconf")
# FIXME : those ain't just services anymore ... what are we supposed to do with this ...
# FIXME : check for all reference of 'service' close to operation_logger stuff
@is_unit_operation([('names', 'configuration')])
def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False,
list_pending=False):
@is_unit_operation([("names", "configuration")])
def regen_conf(
operation_logger,
names=[],
with_diff=False,
force=False,
dry_run=False,
list_pending=False,
):
"""
Regenerate the configuration file(s)
@ -73,19 +79,20 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
for system_path, pending_path in conf_files.items():
pending_conf[category][system_path] = {
'pending_conf': pending_path,
'diff': _get_files_diff(
system_path, pending_path, True),
"pending_conf": pending_path,
"diff": _get_files_diff(system_path, pending_path, True),
}
return pending_conf
if not dry_run:
operation_logger.related_to = [('configuration', x) for x in names]
operation_logger.related_to = [("configuration", x) for x in names]
if not names:
operation_logger.name_parameter_override = 'all'
operation_logger.name_parameter_override = "all"
elif len(names) != 1:
operation_logger.name_parameter_override = str(len(operation_logger.related_to)) + '_categories'
operation_logger.name_parameter_override = (
str(len(operation_logger.related_to)) + "_categories"
)
operation_logger.start()
# Clean pending conf directory
@ -94,8 +101,7 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
shutil.rmtree(PENDING_CONF_DIR, ignore_errors=True)
else:
for name in names:
shutil.rmtree(os.path.join(PENDING_CONF_DIR, name),
ignore_errors=True)
shutil.rmtree(os.path.join(PENDING_CONF_DIR, name), ignore_errors=True)
else:
filesystem.mkdir(PENDING_CONF_DIR, 0o755, True)
@ -103,22 +109,25 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
common_args = [1 if force else 0, 1 if dry_run else 0]
# Execute hooks for pre-regen
pre_args = ['pre', ] + common_args
pre_args = [
"pre",
] + common_args
def _pre_call(name, priority, path, args):
# create the pending conf directory for the category
category_pending_path = os.path.join(PENDING_CONF_DIR, name)
filesystem.mkdir(category_pending_path, 0o755, True, uid='root')
filesystem.mkdir(category_pending_path, 0o755, True, uid="root")
# return the arguments to pass to the script
return pre_args + [category_pending_path, ]
return pre_args + [
category_pending_path,
]
ssh_explicitly_specified = isinstance(names, list) and "ssh" in names
# By default, we regen everything
if not names:
names = hook_list('conf_regen', list_by='name',
show_info=False)['hooks']
names = hook_list("conf_regen", list_by="name", show_info=False)["hooks"]
# Dirty hack for legacy code : avoid attempting to regen the conf for
# glances because it got removed ... This is only needed *once*
@ -134,6 +143,7 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
# hooks to avoid having to call "yunohost domain list" so many times which
# ends up in wasted time (about 3~5 seconds per call on a RPi2)
from yunohost.domain import domain_list
env = {}
# Well we can only do domain_list() if postinstall is done ...
# ... but hooks that effectively need the domain list are only
@ -142,18 +152,23 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
if os.path.exists("/etc/yunohost/installed"):
env["YNH_DOMAINS"] = " ".join(domain_list()["domains"])
pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call, env=env)
pre_result = hook_callback("conf_regen", names, pre_callback=_pre_call, env=env)
# Keep only the hook names with at least one success
names = [hook for hook, infos in pre_result.items()
if any(result["state"] == "succeed" for result in infos.values())]
names = [
hook
for hook, infos in pre_result.items()
if any(result["state"] == "succeed" for result in infos.values())
]
# FIXME : what do in case of partial success/failure ...
if not names:
ret_failed = [hook for hook, infos in pre_result.items()
if any(result["state"] == "failed" for result in infos.values())]
raise YunohostError('regenconf_failed',
categories=', '.join(ret_failed))
ret_failed = [
hook
for hook, infos in pre_result.items()
if any(result["state"] == "failed" for result in infos.values())
]
raise YunohostError("regenconf_failed", categories=", ".join(ret_failed))
# Set the processing method
_regen = _process_regen_conf if not dry_run else lambda *a, **k: True
@ -163,12 +178,12 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
# Iterate over categories and process pending conf
for category, conf_files in _get_pending_conf(names).items():
if not dry_run:
operation_logger.related_to.append(('configuration', category))
operation_logger.related_to.append(("configuration", category))
if dry_run:
logger.debug(m18n.n('regenconf_pending_applying', category=category))
logger.debug(m18n.n("regenconf_pending_applying", category=category))
else:
logger.debug(m18n.n('regenconf_dry_pending_applying', category=category))
logger.debug(m18n.n("regenconf_dry_pending_applying", category=category))
conf_hashes = _get_conf_hashes(category)
succeed_regen = {}
@ -184,7 +199,11 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
# hash of the pending configuration ...
# That way, the file will later appear as manually modified.
sshd_config = "/etc/ssh/sshd_config"
if category == "ssh" and sshd_config not in conf_hashes and sshd_config in conf_files:
if (
category == "ssh"
and sshd_config not in conf_hashes
and sshd_config in conf_files
):
conf_hashes[sshd_config] = _calculate_hash(conf_files[sshd_config])
_update_conf_hashes(category, conf_hashes)
@ -227,17 +246,23 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
force_update_hashes_for_this_category = False
for system_path, pending_path in conf_files.items():
logger.debug("processing pending conf '%s' to system conf '%s'",
pending_path, system_path)
logger.debug(
"processing pending conf '%s' to system conf '%s'",
pending_path,
system_path,
)
conf_status = None
regenerated = False
# Get the diff between files
conf_diff = _get_files_diff(
system_path, pending_path, True) if with_diff else None
conf_diff = (
_get_files_diff(system_path, pending_path, True) if with_diff else None
)
# Check if the conf must be removed
to_remove = True if pending_path and os.path.getsize(pending_path) == 0 else False
to_remove = (
True if pending_path and os.path.getsize(pending_path) == 0 else False
)
# Retrieve and calculate hashes
system_hash = _calculate_hash(system_path)
@ -251,7 +276,7 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
if not system_hash:
logger.debug("> forgetting about stale file/hash")
conf_hashes[system_path] = None
conf_status = 'forget-about-it'
conf_status = "forget-about-it"
regenerated = True
# Otherwise there's still a file on the system but it's not managed by
# Yunohost anymore... But if user requested --force we shall
@ -259,13 +284,13 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
elif force:
logger.debug("> force-remove stale file")
regenerated = _regen(system_path)
conf_status = 'force-removed'
conf_status = "force-removed"
# Otherwise, flag the file as manually modified
else:
logger.warning(m18n.n(
'regenconf_file_manually_modified',
conf=system_path))
conf_status = 'modified'
logger.warning(
m18n.n("regenconf_file_manually_modified", conf=system_path)
)
conf_status = "modified"
# -> system conf does not exists
elif not system_hash:
@ -273,56 +298,65 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
logger.debug("> system conf is already removed")
os.remove(pending_path)
conf_hashes[system_path] = None
conf_status = 'forget-about-it'
conf_status = "forget-about-it"
force_update_hashes_for_this_category = True
continue
elif not saved_hash or force:
if force:
logger.debug("> system conf has been manually removed")
conf_status = 'force-created'
conf_status = "force-created"
else:
logger.debug("> system conf does not exist yet")
conf_status = 'created'
regenerated = _regen(
system_path, pending_path, save=False)
conf_status = "created"
regenerated = _regen(system_path, pending_path, save=False)
else:
logger.info(m18n.n(
'regenconf_file_manually_removed',
conf=system_path))
conf_status = 'removed'
logger.info(
m18n.n("regenconf_file_manually_removed", conf=system_path)
)
conf_status = "removed"
# -> system conf is not managed yet
elif not saved_hash:
logger.debug("> system conf is not managed yet")
if system_hash == new_hash:
logger.debug("> no changes to system conf has been made")
conf_status = 'managed'
conf_status = "managed"
regenerated = True
elif not to_remove:
# If the conf exist but is not managed yet, and is not to be removed,
# we assume that it is safe to regen it, since the file is backuped
# anyway (by default in _regen), as long as we warn the user
# appropriately.
logger.info(m18n.n('regenconf_now_managed_by_yunohost',
conf=system_path, category=category))
logger.info(
m18n.n(
"regenconf_now_managed_by_yunohost",
conf=system_path,
category=category,
)
)
regenerated = _regen(system_path, pending_path)
conf_status = 'new'
conf_status = "new"
elif force:
regenerated = _regen(system_path)
conf_status = 'force-removed'
conf_status = "force-removed"
else:
logger.info(m18n.n('regenconf_file_kept_back',
conf=system_path, category=category))
conf_status = 'unmanaged'
logger.info(
m18n.n(
"regenconf_file_kept_back",
conf=system_path,
category=category,
)
)
conf_status = "unmanaged"
# -> system conf has not been manually modified
elif system_hash == saved_hash:
if to_remove:
regenerated = _regen(system_path)
conf_status = 'removed'
conf_status = "removed"
elif system_hash != new_hash:
regenerated = _regen(system_path, pending_path)
conf_status = 'updated'
conf_status = "updated"
else:
logger.debug("> system conf is already up-to-date")
os.remove(pending_path)
@ -332,24 +366,28 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
logger.debug("> system conf has been manually modified")
if system_hash == new_hash:
logger.debug("> new conf is as current system conf")
conf_status = 'managed'
conf_status = "managed"
regenerated = True
elif force and system_path == sshd_config and not ssh_explicitly_specified:
logger.warning(m18n.n('regenconf_need_to_explicitly_specify_ssh'))
conf_status = 'modified'
elif (
force
and system_path == sshd_config
and not ssh_explicitly_specified
):
logger.warning(m18n.n("regenconf_need_to_explicitly_specify_ssh"))
conf_status = "modified"
elif force:
regenerated = _regen(system_path, pending_path)
conf_status = 'force-updated'
conf_status = "force-updated"
else:
logger.warning(m18n.n(
'regenconf_file_manually_modified',
conf=system_path))
conf_status = 'modified'
logger.warning(
m18n.n("regenconf_file_manually_modified", conf=system_path)
)
conf_status = "modified"
# Store the result
conf_result = {'status': conf_status}
conf_result = {"status": conf_status}
if conf_diff is not None:
conf_result['diff'] = conf_diff
conf_result["diff"] = conf_diff
if regenerated:
succeed_regen[system_path] = conf_result
conf_hashes[system_path] = new_hash
@ -360,39 +398,40 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run
# Check for category conf changes
if not succeed_regen and not failed_regen:
logger.debug(m18n.n('regenconf_up_to_date', category=category))
logger.debug(m18n.n("regenconf_up_to_date", category=category))
continue
elif not failed_regen:
if not dry_run:
logger.success(m18n.n('regenconf_updated', category=category))
logger.success(m18n.n("regenconf_updated", category=category))
else:
logger.success(m18n.n('regenconf_would_be_updated', category=category))
logger.success(m18n.n("regenconf_would_be_updated", category=category))
if (succeed_regen or force_update_hashes_for_this_category) and not dry_run:
_update_conf_hashes(category, conf_hashes)
# Append the category results
result[category] = {
'applied': succeed_regen,
'pending': failed_regen
}
result[category] = {"applied": succeed_regen, "pending": failed_regen}
# Return in case of dry run
if dry_run:
return result
# Execute hooks for post-regen
post_args = ['post', ] + common_args
post_args = [
"post",
] + common_args
def _pre_call(name, priority, path, args):
# append coma-separated applied changes for the category
if name in result and result[name]['applied']:
regen_conf_files = ','.join(result[name]['applied'].keys())
if name in result and result[name]["applied"]:
regen_conf_files = ",".join(result[name]["applied"].keys())
else:
regen_conf_files = ''
return post_args + [regen_conf_files, ]
regen_conf_files = ""
return post_args + [
regen_conf_files,
]
hook_callback('conf_regen', names, pre_callback=_pre_call, env=env)
hook_callback("conf_regen", names, pre_callback=_pre_call, env=env)
operation_logger.success()
@ -404,9 +443,9 @@ def _get_regenconf_infos():
Get a dict of regen conf informations
"""
try:
with open(REGEN_CONF_FILE, 'r') as f:
with open(REGEN_CONF_FILE, "r") as f:
return yaml.load(f)
except:
except Exception:
return {}
@ -422,10 +461,12 @@ def _save_regenconf_infos(infos):
del infos["glances"]
try:
with open(REGEN_CONF_FILE, 'w') as f:
with open(REGEN_CONF_FILE, "w") as f:
yaml.safe_dump(infos, f, default_flow_style=False)
except Exception as e:
logger.warning('Error while saving regenconf infos, exception: %s', e, exc_info=1)
logger.warning(
"Error while saving regenconf infos, exception: %s", e, exc_info=1
)
raise
@ -439,13 +480,13 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True):
"""
if orig_file and os.path.exists(orig_file):
with open(orig_file, 'r') as orig_file:
with open(orig_file, "r") as orig_file:
orig_file = orig_file.readlines()
else:
orig_file = []
if new_file and os.path.exists(new_file):
with open(new_file, 'r') as new_file:
with open(new_file, "r") as new_file:
new_file = new_file.readlines()
else:
new_file = []
@ -457,11 +498,11 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True):
try:
next(diff)
next(diff)
except:
except Exception:
pass
if as_string:
return ''.join(diff).rstrip()
return "".join(diff).rstrip()
return diff
@ -475,12 +516,14 @@ def _calculate_hash(path):
hasher = hashlib.md5()
try:
with open(path, 'rb') as f:
with open(path, "rb") as f:
hasher.update(f.read())
return hasher.hexdigest()
except IOError as e:
logger.warning("Error while calculating file '%s' hash: %s", path, e, exc_info=1)
logger.warning(
"Error while calculating file '%s' hash: %s", path, e, exc_info=1
)
return None
@ -535,18 +578,17 @@ def _get_conf_hashes(category):
logger.debug("category %s is not in categories.yml yet.", category)
return {}
elif categories[category] is None or 'conffiles' not in categories[category]:
elif categories[category] is None or "conffiles" not in categories[category]:
logger.debug("No configuration files for category %s.", category)
return {}
else:
return categories[category]['conffiles']
return categories[category]["conffiles"]
def _update_conf_hashes(category, hashes):
"""Update the registered conf hashes for a category"""
logger.debug("updating conf hashes for '%s' with: %s",
category, hashes)
logger.debug("updating conf hashes for '%s' with: %s", category, hashes)
categories = _get_regenconf_infos()
category_conf = categories.get(category, {})
@ -559,9 +601,13 @@ def _update_conf_hashes(category, hashes):
# that path.
# It avoid keeping weird old entries like
# /etc/nginx/conf.d/some.domain.that.got.removed.conf
hashes = {path: hash_ for path, hash_ in hashes.items() if hash_ is not None or os.path.exists(path)}
hashes = {
path: hash_
for path, hash_ in hashes.items()
if hash_ is not None or os.path.exists(path)
}
category_conf['conffiles'] = hashes
category_conf["conffiles"] = hashes
categories[category] = category_conf
_save_regenconf_infos(categories)
@ -571,9 +617,12 @@ def _force_clear_hashes(paths):
categories = _get_regenconf_infos()
for path in paths:
for category in categories.keys():
if path in categories[category]['conffiles']:
logger.debug("force-clearing old conf hash for %s in category %s" % (path, category))
del categories[category]['conffiles'][path]
if path in categories[category]["conffiles"]:
logger.debug(
"force-clearing old conf hash for %s in category %s"
% (path, category)
)
del categories[category]["conffiles"][path]
_save_regenconf_infos(categories)
@ -587,22 +636,26 @@ def _process_regen_conf(system_conf, new_conf=None, save=True):
"""
if save:
backup_path = os.path.join(BACKUP_CONF_DIR, '{0}-{1}'.format(
system_conf.lstrip('/'), datetime.utcnow().strftime("%Y%m%d.%H%M%S")))
backup_path = os.path.join(
BACKUP_CONF_DIR,
"{0}-{1}".format(
system_conf.lstrip("/"), datetime.utcnow().strftime("%Y%m%d.%H%M%S")
),
)
backup_dir = os.path.dirname(backup_path)
if not os.path.isdir(backup_dir):
filesystem.mkdir(backup_dir, 0o755, True)
shutil.copy2(system_conf, backup_path)
logger.debug(m18n.n('regenconf_file_backed_up',
conf=system_conf, backup=backup_path))
logger.debug(
m18n.n("regenconf_file_backed_up", conf=system_conf, backup=backup_path)
)
try:
if not new_conf:
os.remove(system_conf)
logger.debug(m18n.n('regenconf_file_removed',
conf=system_conf))
logger.debug(m18n.n("regenconf_file_removed", conf=system_conf))
else:
system_dir = os.path.dirname(system_conf)
@ -610,14 +663,18 @@ def _process_regen_conf(system_conf, new_conf=None, save=True):
filesystem.mkdir(system_dir, 0o755, True)
shutil.copyfile(new_conf, system_conf)
logger.debug(m18n.n('regenconf_file_updated',
conf=system_conf))
logger.debug(m18n.n("regenconf_file_updated", conf=system_conf))
except Exception as e:
logger.warning("Exception while trying to regenerate conf '%s': %s", system_conf, e, exc_info=1)
logger.warning(
"Exception while trying to regenerate conf '%s': %s",
system_conf,
e,
exc_info=1,
)
if not new_conf and os.path.exists(system_conf):
logger.warning(m18n.n('regenconf_file_remove_failed',
conf=system_conf),
exc_info=1)
logger.warning(
m18n.n("regenconf_file_remove_failed", conf=system_conf), exc_info=1
)
return False
elif new_conf:
@ -626,13 +683,16 @@ def _process_regen_conf(system_conf, new_conf=None, save=True):
# Raise an exception if an os.stat() call on either pathname fails.
# (os.stats returns a series of information from a file like type, size...)
copy_succeed = os.path.samefile(system_conf, new_conf)
except:
except Exception:
copy_succeed = False
finally:
if not copy_succeed:
logger.warning(m18n.n('regenconf_file_copy_failed',
conf=system_conf, new=new_conf),
exc_info=1)
logger.warning(
m18n.n(
"regenconf_file_copy_failed", conf=system_conf, new=new_conf
),
exc_info=1,
)
return False
return True
@ -651,13 +711,17 @@ def manually_modified_files():
return output
def manually_modified_files_compared_to_debian_default(ignore_handled_by_regenconf=False):
def manually_modified_files_compared_to_debian_default(
ignore_handled_by_regenconf=False,
):
# from https://serverfault.com/a/90401
files = check_output("dpkg-query -W -f='${Conffiles}\n' '*' \
files = check_output(
"dpkg-query -W -f='${Conffiles}\n' '*' \
| awk 'OFS=\" \"{print $2,$1}' \
| md5sum -c 2>/dev/null \
| awk -F': ' '$2 !~ /OK/{print $1}'")
| awk -F': ' '$2 !~ /OK/{print $1}'"
)
files = files.strip().split("\n")
if ignore_handled_by_regenconf:

View file

@ -41,10 +41,20 @@ from moulinette.utils.filesystem import read_file, append_to_file, write_to_file
MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock"
logger = getActionLogger('yunohost.service')
logger = getActionLogger("yunohost.service")
def service_add(name, description=None, log=None, log_type=None, test_status=None, test_conf=None, needs_exposed_ports=None, need_lock=False, status=None):
def service_add(
name,
description=None,
log=None,
log_type=None,
test_status=None,
test_conf=None,
needs_exposed_ports=None,
need_lock=False,
status=None,
):
"""
Add a custom service
@ -69,12 +79,14 @@ def service_add(name, description=None, log=None, log_type=None, test_status=Non
# Deprecated log_type stuff
if log_type is not None:
logger.warning("/!\\ Packagers! --log_type is deprecated. You do not need to specify --log_type systemd anymore ... Yunohost now automatically fetch the journalctl of the systemd service by default.")
logger.warning(
"/!\\ Packagers! --log_type is deprecated. You do not need to specify --log_type systemd anymore ... Yunohost now automatically fetch the journalctl of the systemd service by default."
)
# Usually when adding such a service, the service name will be provided so we remove it as it's not a log file path
if name in log:
log.remove(name)
service['log'] = log
service["log"] = log
if not description:
# Try to get the description from systemd service
@ -87,12 +99,14 @@ def service_add(name, description=None, log=None, log_type=None, test_status=Non
description = ""
if description:
service['description'] = description
service["description"] = description
else:
logger.warning("/!\\ Packagers! You added a custom service without specifying a description. Please add a proper Description in the systemd configuration, or use --description to explain what the service does in a similar fashion to existing services.")
logger.warning(
"/!\\ Packagers! You added a custom service without specifying a description. Please add a proper Description in the systemd configuration, or use --description to explain what the service does in a similar fashion to existing services."
)
if need_lock:
service['need_lock'] = True
service["need_lock"] = True
if test_status:
service["test_status"] = test_status
@ -101,7 +115,9 @@ def service_add(name, description=None, log=None, log_type=None, test_status=Non
_, systemd_info = _get_service_information_from_systemd(name)
type_ = systemd_info.get("Type") if systemd_info is not None else ""
if type_ == "oneshot" and name != "postgresql":
logger.warning("/!\\ Packagers! Please provide a --test_status when adding oneshot-type services in Yunohost, such that it has a reliable way to check if the service is running or not.")
logger.warning(
"/!\\ Packagers! Please provide a --test_status when adding oneshot-type services in Yunohost, such that it has a reliable way to check if the service is running or not."
)
if test_conf:
service["test_conf"] = test_conf
@ -113,9 +129,9 @@ def service_add(name, description=None, log=None, log_type=None, test_status=Non
_save_services(services)
except Exception:
# we'll get a logger.warning with more details in _save_services
raise YunohostError('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))
def service_remove(name):
@ -129,16 +145,16 @@ def service_remove(name):
services = _get_services()
if name not in services:
raise YunohostError('service_unknown', service=name)
raise YunohostError("service_unknown", service=name)
del services[name]
try:
_save_services(services)
except Exception:
# we'll get a logger.warning with more details in _save_services
raise YunohostError('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))
def service_start(names):
@ -153,12 +169,16 @@ def service_start(names):
names = [names]
for name in names:
if _run_service_command('start', name):
logger.success(m18n.n('service_started', service=name))
if _run_service_command("start", name):
logger.success(m18n.n("service_started", service=name))
else:
if service_status(name)['status'] != 'running':
raise YunohostError('service_start_failed', service=name, logs=_get_journalctl_logs(name))
logger.debug(m18n.n('service_already_started', service=name))
if service_status(name)["status"] != "running":
raise YunohostError(
"service_start_failed",
service=name,
logs=_get_journalctl_logs(name),
)
logger.debug(m18n.n("service_already_started", service=name))
def service_stop(names):
@ -172,12 +192,14 @@ def service_stop(names):
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('stop', name):
logger.success(m18n.n('service_stopped', service=name))
if _run_service_command("stop", name):
logger.success(m18n.n("service_stopped", service=name))
else:
if service_status(name)['status'] != 'inactive':
raise YunohostError('service_stop_failed', service=name, logs=_get_journalctl_logs(name))
logger.debug(m18n.n('service_already_stopped', service=name))
if service_status(name)["status"] != "inactive":
raise YunohostError(
"service_stop_failed", service=name, logs=_get_journalctl_logs(name)
)
logger.debug(m18n.n("service_already_stopped", service=name))
def service_reload(names):
@ -191,11 +213,15 @@ def service_reload(names):
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('reload', name):
logger.success(m18n.n('service_reloaded', service=name))
if _run_service_command("reload", name):
logger.success(m18n.n("service_reloaded", service=name))
else:
if service_status(name)['status'] != 'inactive':
raise YunohostError('service_reload_failed', service=name, logs=_get_journalctl_logs(name))
if service_status(name)["status"] != "inactive":
raise YunohostError(
"service_reload_failed",
service=name,
logs=_get_journalctl_logs(name),
)
def service_restart(names):
@ -209,11 +235,15 @@ def service_restart(names):
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('restart', name):
logger.success(m18n.n('service_restarted', service=name))
if _run_service_command("restart", name):
logger.success(m18n.n("service_restarted", service=name))
else:
if service_status(name)['status'] != 'inactive':
raise YunohostError('service_restart_failed', service=name, logs=_get_journalctl_logs(name))
if service_status(name)["status"] != "inactive":
raise YunohostError(
"service_restart_failed",
service=name,
logs=_get_journalctl_logs(name),
)
def service_reload_or_restart(names):
@ -227,11 +257,15 @@ def service_reload_or_restart(names):
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('reload-or-restart', name):
logger.success(m18n.n('service_reloaded_or_restarted', service=name))
if _run_service_command("reload-or-restart", name):
logger.success(m18n.n("service_reloaded_or_restarted", service=name))
else:
if service_status(name)['status'] != 'inactive':
raise YunohostError('service_reload_or_restart_failed', service=name, logs=_get_journalctl_logs(name))
if service_status(name)["status"] != "inactive":
raise YunohostError(
"service_reload_or_restart_failed",
service=name,
logs=_get_journalctl_logs(name),
)
def service_enable(names):
@ -245,10 +279,12 @@ def service_enable(names):
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('enable', name):
logger.success(m18n.n('service_enabled', service=name))
if _run_service_command("enable", name):
logger.success(m18n.n("service_enabled", service=name))
else:
raise YunohostError('service_enable_failed', service=name, logs=_get_journalctl_logs(name))
raise YunohostError(
"service_enable_failed", service=name, logs=_get_journalctl_logs(name)
)
def service_disable(names):
@ -262,10 +298,12 @@ def service_disable(names):
if isinstance(names, str):
names = [names]
for name in names:
if _run_service_command('disable', name):
logger.success(m18n.n('service_disabled', service=name))
if _run_service_command("disable", name):
logger.success(m18n.n("service_disabled", service=name))
else:
raise YunohostError('service_disable_failed', service=name, logs=_get_journalctl_logs(name))
raise YunohostError(
"service_disable_failed", service=name, logs=_get_journalctl_logs(name)
)
def service_status(names=[]):
@ -287,7 +325,7 @@ def service_status(names=[]):
# Validate service names requested
for name in names:
if name not in services.keys():
raise YunohostError('service_unknown', service=name)
raise YunohostError("service_unknown", service=name)
# Filter only requested servivces
services = {k: v for k, v in services.items() if k in names}
@ -300,7 +338,9 @@ def service_status(names=[]):
# the hack was to add fake services...
services = {k: v for k, v in services.items() if v.get("status", "") is not None}
output = {s: _get_and_format_service_status(s, infos) for s, infos in services.items()}
output = {
s: _get_and_format_service_status(s, infos) for s, infos in services.items()
}
if len(names) == 1:
return output[names[0]]
@ -313,17 +353,19 @@ def _get_service_information_from_systemd(service):
d = dbus.SystemBus()
systemd = d.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager')
systemd = d.get_object("org.freedesktop.systemd1", "/org/freedesktop/systemd1")
manager = dbus.Interface(systemd, "org.freedesktop.systemd1.Manager")
# c.f. https://zignar.net/2014/09/08/getting-started-with-dbus-python-systemd/
# Very interface, much intuitive, wow
service_unit = manager.LoadUnit(service + '.service')
service_proxy = d.get_object('org.freedesktop.systemd1', str(service_unit))
properties_interface = dbus.Interface(service_proxy, 'org.freedesktop.DBus.Properties')
service_unit = manager.LoadUnit(service + ".service")
service_proxy = d.get_object("org.freedesktop.systemd1", str(service_unit))
properties_interface = dbus.Interface(
service_proxy, "org.freedesktop.DBus.Properties"
)
unit = properties_interface.GetAll('org.freedesktop.systemd1.Unit')
service = properties_interface.GetAll('org.freedesktop.systemd1.Service')
unit = properties_interface.GetAll("org.freedesktop.systemd1.Unit")
service = properties_interface.GetAll("org.freedesktop.systemd1.Service")
if unit.get("LoadState", "not-found") == "not-found":
# Service doesn't really exist
@ -338,13 +380,16 @@ def _get_and_format_service_status(service, infos):
raw_status, raw_service = _get_service_information_from_systemd(systemd_service)
if raw_status is None:
logger.error("Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')." % systemd_service)
logger.error(
"Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')."
% systemd_service
)
return {
'status': "unknown",
'start_on_boot': "unknown",
'last_state_change': "unknown",
'description': "Error: failed to get information for this service, it doesn't exists for systemd",
'configuration': "unknown",
"status": "unknown",
"start_on_boot": "unknown",
"last_state_change": "unknown",
"description": "Error: failed to get information for this service, it doesn't exists for systemd",
"configuration": "unknown",
}
# Try to get description directly from services.yml
@ -360,35 +405,46 @@ def _get_and_format_service_status(service, infos):
description = str(raw_status.get("Description", ""))
output = {
'status': str(raw_status.get("SubState", "unknown")),
'start_on_boot': str(raw_status.get("UnitFileState", "unknown")),
'last_state_change': "unknown",
'description': description,
'configuration': "unknown",
"status": str(raw_status.get("SubState", "unknown")),
"start_on_boot": str(raw_status.get("UnitFileState", "unknown")),
"last_state_change": "unknown",
"description": description,
"configuration": "unknown",
}
# Fun stuff™ : to obtain the enabled/disabled status for sysv services,
# gotta do this ... cf code of /lib/systemd/systemd-sysv-install
if output["start_on_boot"] == "generated":
output["start_on_boot"] = "enabled" if glob("/etc/rc[S5].d/S??" + service) else "disabled"
elif os.path.exists("/etc/systemd/system/multi-user.target.wants/%s.service" % service):
output["start_on_boot"] = (
"enabled" if glob("/etc/rc[S5].d/S??" + service) else "disabled"
)
elif os.path.exists(
"/etc/systemd/system/multi-user.target.wants/%s.service" % service
):
output["start_on_boot"] = "enabled"
if "StateChangeTimestamp" in raw_status:
output['last_state_change'] = datetime.utcfromtimestamp(raw_status["StateChangeTimestamp"] / 1000000)
output["last_state_change"] = datetime.utcfromtimestamp(
raw_status["StateChangeTimestamp"] / 1000000
)
# 'test_status' is an optional field to test the status of the service using a custom command
if "test_status" in infos:
p = subprocess.Popen(infos["test_status"],
shell=True,
executable='/bin/bash',
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
p = subprocess.Popen(
infos["test_status"],
shell=True,
executable="/bin/bash",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
p.communicate()
output["status"] = "running" if p.returncode == 0 else "failed"
elif raw_service.get("Type", "").lower() == "oneshot" and output["status"] == "exited":
elif (
raw_service.get("Type", "").lower() == "oneshot"
and output["status"] == "exited"
):
# These are services like yunohost-firewall, hotspot, vpnclient,
# ... they will be "exited" why doesn't provide any info about
# the real state of the service (unless they did provide a
@ -397,11 +453,13 @@ def _get_and_format_service_status(service, infos):
# 'test_status' is an optional field to test the status of the service using a custom command
if "test_conf" in infos:
p = subprocess.Popen(infos["test_conf"],
shell=True,
executable='/bin/bash',
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
p = subprocess.Popen(
infos["test_conf"],
shell=True,
executable="/bin/bash",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
out, _ = p.communicate()
if p.returncode == 0:
@ -426,9 +484,9 @@ def service_log(name, number=50):
number = int(number)
if name not in services.keys():
raise YunohostError('service_unknown', service=name)
raise YunohostError("service_unknown", service=name)
log_list = services[name].get('log', [])
log_list = services[name].get("log", [])
if not isinstance(log_list, list):
log_list = [log_list]
@ -469,13 +527,16 @@ def service_log(name, number=50):
if not log_file.endswith(".log"):
continue
result[log_file_path] = _tail(log_file_path, number) if os.path.exists(log_file_path) else []
result[log_file_path] = (
_tail(log_file_path, number) if os.path.exists(log_file_path) else []
)
return result
def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False,
list_pending=False):
def service_regen_conf(
names=[], with_diff=False, force=False, dry_run=False, list_pending=False
):
services = _get_services()
@ -484,7 +545,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False,
for name in names:
if name not in services.keys():
raise YunohostError('service_unknown', service=name)
raise YunohostError("service_unknown", service=name)
if names is []:
names = list(services.keys())
@ -492,6 +553,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False,
logger.warning(m18n.n("service_regen_conf_is_deprecated"))
from yunohost.regenconf import regen_conf
return regen_conf(names, with_diff, force, dry_run, list_pending)
@ -506,16 +568,32 @@ def _run_service_command(action, service):
"""
services = _get_services()
if service not in services.keys():
raise YunohostError('service_unknown', service=service)
raise YunohostError("service_unknown", service=service)
possible_actions = ['start', 'stop', 'restart', 'reload', 'reload-or-restart', 'enable', 'disable']
possible_actions = [
"start",
"stop",
"restart",
"reload",
"reload-or-restart",
"enable",
"disable",
]
if action not in possible_actions:
raise ValueError("Unknown action '%s', available actions are: %s" % (action, ", ".join(possible_actions)))
raise ValueError(
"Unknown action '%s', available actions are: %s"
% (action, ", ".join(possible_actions))
)
cmd = 'systemctl %s %s' % (action, service)
cmd = "systemctl %s %s" % (action, service)
need_lock = services[service].get('need_lock', False) \
and action in ['start', 'stop', 'restart', 'reload', 'reload-or-restart']
need_lock = services[service].get("need_lock", False) and action in [
"start",
"stop",
"restart",
"reload",
"reload-or-restart",
]
if action in ["enable", "disable"]:
cmd += " --quiet"
@ -532,7 +610,7 @@ def _run_service_command(action, service):
p.communicate()
if p.returncode != 0:
logger.warning(m18n.n('service_cmd_exec_failed', command=cmd))
logger.warning(m18n.n("service_cmd_exec_failed", command=cmd))
return False
except Exception as e:
@ -568,8 +646,9 @@ def _give_lock(action, service, p):
# If we found a PID
if son_PID != 0:
# Append the PID to the lock file
logger.debug("Giving a lock to PID %s for service %s !"
% (str(son_PID), service))
logger.debug(
"Giving a lock to PID %s for service %s !" % (str(son_PID), service)
)
append_to_file(MOULINETTE_LOCK, "\n%s" % str(son_PID))
return son_PID
@ -580,7 +659,7 @@ def _remove_lock(PID_to_remove):
PIDs = read_file(MOULINETTE_LOCK).split("\n")
PIDs_to_keep = [PID for PID in PIDs if int(PID) != PID_to_remove]
write_to_file(MOULINETTE_LOCK, '\n'.join(PIDs_to_keep))
write_to_file(MOULINETTE_LOCK, "\n".join(PIDs_to_keep))
def _get_services():
@ -589,9 +668,9 @@ def _get_services():
"""
try:
with open('/etc/yunohost/services.yml', 'r') as f:
with open("/etc/yunohost/services.yml", "r") as f:
services = yaml.load(f) or {}
except:
except Exception:
return {}
# some services are marked as None to remove them from YunoHost
@ -601,7 +680,9 @@ def _get_services():
del services[key]
# Dirty hack to automatically find custom SSH port ...
ssh_port_line = re.findall(r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config"))
ssh_port_line = re.findall(
r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config")
)
if len(ssh_port_line) == 1:
services["ssh"]["needs_exposed_ports"] = [int(ssh_port_line[0])]
@ -633,10 +714,10 @@ def _save_services(services):
"""
try:
with open('/etc/yunohost/services.yml', 'w') as f:
with open("/etc/yunohost/services.yml", "w") as f:
yaml.safe_dump(services, f, default_flow_style=False)
except Exception as e:
logger.warning('Error while saving services, exception: %s', e, exc_info=1)
logger.warning("Error while saving services, exception: %s", e, exc_info=1)
raise
@ -654,6 +735,7 @@ def _tail(file, n):
try:
if file.endswith(".gz"):
import gzip
f = gzip.open(file)
lines = f.read().splitlines()
else:
@ -694,15 +776,15 @@ def _find_previous_log_file(file):
Find the previous log file
"""
splitext = os.path.splitext(file)
if splitext[1] == '.gz':
if splitext[1] == ".gz":
file = splitext[0]
splitext = os.path.splitext(file)
ext = splitext[1]
i = re.findall(r'\.(\d+)', ext)
i = re.findall(r"\.(\d+)", ext)
i = int(i[0]) + 1 if len(i) > 0 else 1
previous_file = file if i == 1 else splitext[0]
previous_file = previous_file + '.%d' % (i)
previous_file = previous_file + ".%d" % (i)
if os.path.exists(previous_file):
return previous_file
@ -717,7 +799,15 @@ def _get_journalctl_logs(service, number="all"):
services = _get_services()
systemd_service = services.get(service, {}).get("actual_systemd_service", service)
try:
return check_output("journalctl --no-hostname --no-pager -u {0} -n{1}".format(systemd_service, number))
except:
return check_output(
"journalctl --no-hostname --no-pager -u {0} -n{1}".format(
systemd_service, number
)
)
except Exception:
import traceback
return "error while get services logs from journalctl:\n%s" % traceback.format_exc()
return (
"error while get services logs from journalctl:\n%s"
% traceback.format_exc()
)

View file

@ -10,7 +10,7 @@ from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from yunohost.regenconf import regen_conf
logger = getActionLogger('yunohost.settings')
logger = getActionLogger("yunohost.settings")
SETTINGS_PATH = "/etc/yunohost/settings.json"
SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"
@ -30,8 +30,8 @@ def is_boolean(value):
if isinstance(value, bool):
return True, value
elif isinstance(value, str):
if str(value).lower() in ['true', 'on', 'yes', 'false', 'off', 'no']:
return True, str(value).lower() in ['true', 'on', 'yes']
if str(value).lower() in ["true", "on", "yes", "false", "off", "no"]:
return True, str(value).lower() in ["true", "on", "yes"]
else:
return False, None
else:
@ -53,28 +53,49 @@ def is_boolean(value):
# * string
# * enum (in the form of a python list)
DEFAULTS = OrderedDict([
# Password Validation
# -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest
("security.password.admin.strength", {"type": "int", "default": 1}),
("security.password.user.strength", {"type": "int", "default": 1}),
("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", "default": False}),
("security.ssh.compatibility", {"type": "enum", "default": "modern",
"choices": ["intermediate", "modern"]}),
("security.nginx.compatibility", {"type": "enum", "default": "intermediate",
"choices": ["intermediate", "modern"]}),
("security.postfix.compatibility", {"type": "enum", "default": "intermediate",
"choices": ["intermediate", "modern"]}),
("pop3.enabled", {"type": "bool", "default": False}),
("smtp.allow_ipv6", {"type": "bool", "default": True}),
("smtp.relay.host", {"type": "string", "default": ""}),
("smtp.relay.port", {"type": "int", "default": 587}),
("smtp.relay.user", {"type": "string", "default": ""}),
("smtp.relay.password", {"type": "string", "default": ""}),
("backup.compress_tar_archives", {"type": "bool", "default": False}),
])
DEFAULTS = OrderedDict(
[
# Password Validation
# -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest
("security.password.admin.strength", {"type": "int", "default": 1}),
("security.password.user.strength", {"type": "int", "default": 1}),
(
"service.ssh.allow_deprecated_dsa_hostkey",
{"type": "bool", "default": False},
),
(
"security.ssh.compatibility",
{
"type": "enum",
"default": "modern",
"choices": ["intermediate", "modern"],
},
),
(
"security.nginx.compatibility",
{
"type": "enum",
"default": "intermediate",
"choices": ["intermediate", "modern"],
},
),
(
"security.postfix.compatibility",
{
"type": "enum",
"default": "intermediate",
"choices": ["intermediate", "modern"],
},
),
("pop3.enabled", {"type": "bool", "default": False}),
("smtp.allow_ipv6", {"type": "bool", "default": True}),
("smtp.relay.host", {"type": "string", "default": ""}),
("smtp.relay.port", {"type": "int", "default": 587}),
("smtp.relay.user", {"type": "string", "default": ""}),
("smtp.relay.password", {"type": "string", "default": ""}),
("backup.compress_tar_archives", {"type": "bool", "default": False}),
]
)
def settings_get(key, full=False):
@ -88,12 +109,12 @@ def settings_get(key, full=False):
settings = _get_settings()
if key not in settings:
raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)
raise YunohostError("global_settings_key_doesnt_exists", settings_key=key)
if full:
return settings[key]
return settings[key]['value']
return settings[key]["value"]
def settings_list():
@ -116,7 +137,7 @@ def settings_set(key, value):
settings = _get_settings()
if key not in settings:
raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)
raise YunohostError("global_settings_key_doesnt_exists", settings_key=key)
key_type = settings[key]["type"]
@ -125,33 +146,51 @@ def settings_set(key, value):
if boolean_value[0]:
value = boolean_value[1]
else:
raise YunohostError('global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type)
raise YunohostError(
"global_settings_bad_type_for_setting",
setting=key,
received_type=type(value).__name__,
expected_type=key_type,
)
elif key_type == "int":
if not isinstance(value, int) or isinstance(value, bool):
if isinstance(value, str):
try:
value = int(value)
except:
raise YunohostError('global_settings_bad_type_for_setting',
setting=key,
received_type=type(value).__name__,
expected_type=key_type)
except Exception:
raise YunohostError(
"global_settings_bad_type_for_setting",
setting=key,
received_type=type(value).__name__,
expected_type=key_type,
)
else:
raise YunohostError('global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type)
raise YunohostError(
"global_settings_bad_type_for_setting",
setting=key,
received_type=type(value).__name__,
expected_type=key_type,
)
elif key_type == "string":
if not isinstance(value, str):
raise YunohostError('global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type)
raise YunohostError(
"global_settings_bad_type_for_setting",
setting=key,
received_type=type(value).__name__,
expected_type=key_type,
)
elif key_type == "enum":
if value not in settings[key]["choices"]:
raise YunohostError('global_settings_bad_choice_for_enum', setting=key,
choice=str(value),
available_choices=", ".join(settings[key]["choices"]))
raise YunohostError(
"global_settings_bad_choice_for_enum",
setting=key,
choice=str(value),
available_choices=", ".join(settings[key]["choices"]),
)
else:
raise YunohostError('global_settings_unknown_type', setting=key,
unknown_type=key_type)
raise YunohostError(
"global_settings_unknown_type", setting=key, unknown_type=key_type
)
old_value = settings[key].get("value")
settings[key]["value"] = value
@ -175,7 +214,7 @@ def settings_reset(key):
settings = _get_settings()
if key not in settings:
raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)
raise YunohostError("global_settings_key_doesnt_exists", settings_key=key)
settings[key]["value"] = settings[key]["default"]
_save_settings(settings)
@ -196,7 +235,9 @@ def settings_reset_all():
# addition but we'll see if this is a common need.
# Another solution would be to use etckeeper and integrate those
# modification inside of it and take advantage of its git history
old_settings_backup_path = SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X")
old_settings_backup_path = (
SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X")
)
_save_settings(settings, location=old_settings_backup_path)
for value in settings.values():
@ -206,12 +247,13 @@ def settings_reset_all():
return {
"old_settings_backup_path": old_settings_backup_path,
"message": m18n.n("global_settings_reset_success", path=old_settings_backup_path)
"message": m18n.n(
"global_settings_reset_success", path=old_settings_backup_path
),
}
def _get_settings():
def get_setting_description(key):
if key.startswith("example"):
# (This is for dummy stuff used during unit tests)
@ -254,18 +296,24 @@ def _get_settings():
settings[key] = value
settings[key]["description"] = get_setting_description(key)
else:
logger.warning(m18n.n('global_settings_unknown_setting_from_settings_file',
setting_key=key))
logger.warning(
m18n.n(
"global_settings_unknown_setting_from_settings_file",
setting_key=key,
)
)
unknown_settings[key] = value
except Exception as e:
raise YunohostError('global_settings_cant_open_settings', reason=e)
raise YunohostError("global_settings_cant_open_settings", reason=e)
if unknown_settings:
try:
_save_settings(unknown_settings, location=unknown_settings_path)
_save_settings(settings)
except Exception as e:
logger.warning("Failed to save unknown settings (because %s), aborting." % e)
logger.warning(
"Failed to save unknown settings (because %s), aborting." % e
)
return settings
@ -280,13 +328,13 @@ def _save_settings(settings, location=SETTINGS_PATH):
try:
result = json.dumps(settings_without_description, indent=4)
except Exception as e:
raise YunohostError('global_settings_cant_serialize_settings', reason=e)
raise YunohostError("global_settings_cant_serialize_settings", reason=e)
try:
with open(location, "w") as settings_fd:
settings_fd.write(result)
except Exception as e:
raise YunohostError('global_settings_cant_write_settings', reason=e)
raise YunohostError("global_settings_cant_write_settings", reason=e)
# Meant to be a dict of setting_name -> function to call
@ -295,10 +343,16 @@ post_change_hooks = {}
def post_change_hook(setting_name):
def decorator(func):
assert setting_name in DEFAULTS.keys(), "The setting %s does not exists" % setting_name
assert setting_name not in post_change_hooks, "You can only register one post change hook per setting (in particular for %s)" % setting_name
assert setting_name in DEFAULTS.keys(), (
"The setting %s does not exists" % setting_name
)
assert setting_name not in post_change_hooks, (
"You can only register one post change hook per setting (in particular for %s)"
% setting_name
)
post_change_hooks[setting_name] = func
return func
return decorator
@ -322,16 +376,17 @@ def trigger_post_change_hook(setting_name, old_value, new_value):
#
# ===========================================
@post_change_hook("security.nginx.compatibility")
def reconfigure_nginx(setting_name, old_value, new_value):
if old_value != new_value:
regen_conf(names=['nginx'])
regen_conf(names=["nginx"])
@post_change_hook("security.ssh.compatibility")
def reconfigure_ssh(setting_name, old_value, new_value):
if old_value != new_value:
regen_conf(names=['ssh'])
regen_conf(names=["ssh"])
@post_change_hook("smtp.allow_ipv6")
@ -342,31 +397,31 @@ def reconfigure_ssh(setting_name, old_value, new_value):
@post_change_hook("security.postfix.compatibility")
def reconfigure_postfix(setting_name, old_value, new_value):
if old_value != new_value:
regen_conf(names=['postfix'])
regen_conf(names=["postfix"])
@post_change_hook("pop3.enabled")
def reconfigure_dovecot(setting_name, old_value, new_value):
dovecot_package = 'dovecot-pop3d'
dovecot_package = "dovecot-pop3d"
environment = os.environ.copy()
environment.update({'DEBIAN_FRONTEND': 'noninteractive'})
environment.update({"DEBIAN_FRONTEND": "noninteractive"})
if new_value == "True":
command = [
'apt-get',
'-y',
'--no-remove',
'-o Dpkg::Options::=--force-confdef',
'-o Dpkg::Options::=--force-confold',
'install',
"apt-get",
"-y",
"--no-remove",
"-o Dpkg::Options::=--force-confdef",
"-o Dpkg::Options::=--force-confold",
"install",
dovecot_package,
]
subprocess.call(command, env=environment)
if old_value != new_value:
regen_conf(names=['dovecot'])
regen_conf(names=["dovecot"])
else:
if old_value != new_value:
regen_conf(names=['dovecot'])
command = ['apt-get', '-y', 'remove', dovecot_package]
regen_conf(names=["dovecot"])
command = ["apt-get", "-y", "remove", dovecot_package]
subprocess.call(command, env=environment)

View file

@ -21,15 +21,16 @@ def user_ssh_allow(username):
# TODO it would be good to support different kind of shells
if not _get_user_for_ssh(username):
raise YunohostError('user_unknown', user=username)
raise YunohostError("user_unknown", user=username)
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
ldap.update('uid=%s,ou=users' % username, {'loginShell': ['/bin/bash']})
ldap.update("uid=%s,ou=users" % username, {"loginShell": ["/bin/bash"]})
# Somehow this is needed otherwise the PAM thing doesn't forget about the
# old loginShell value ?
subprocess.call(['nscd', '-i', 'passwd'])
subprocess.call(["nscd", "-i", "passwd"])
def user_ssh_disallow(username):
@ -42,15 +43,16 @@ def user_ssh_disallow(username):
# TODO it would be good to support different kind of shells
if not _get_user_for_ssh(username):
raise YunohostError('user_unknown', user=username)
raise YunohostError("user_unknown", user=username)
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
ldap.update('uid=%s,ou=users' % username, {'loginShell': ['/bin/false']})
ldap.update("uid=%s,ou=users" % username, {"loginShell": ["/bin/false"]})
# Somehow this is needed otherwise the PAM thing doesn't forget about the
# old loginShell value ?
subprocess.call(['nscd', '-i', 'passwd'])
subprocess.call(["nscd", "-i", "passwd"])
def user_ssh_list_keys(username):
@ -58,7 +60,9 @@ def user_ssh_list_keys(username):
if not user:
raise Exception("User with username '%s' doesn't exists" % username)
authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys")
authorized_keys_file = os.path.join(
user["homeDirectory"][0], ".ssh", "authorized_keys"
)
if not os.path.exists(authorized_keys_file):
return {"keys": []}
@ -76,10 +80,12 @@ def user_ssh_list_keys(username):
# assuming a key per non empty line
key = line.strip()
keys.append({
"key": key,
"name": last_comment,
})
keys.append(
{
"key": key,
"name": last_comment,
}
)
last_comment = ""
@ -91,12 +97,18 @@ def user_ssh_add_key(username, key, comment):
if not user:
raise Exception("User with username '%s' doesn't exists" % username)
authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys")
authorized_keys_file = os.path.join(
user["homeDirectory"][0], ".ssh", "authorized_keys"
)
if not os.path.exists(authorized_keys_file):
# ensure ".ssh" exists
mkdir(os.path.join(user["homeDirectory"][0], ".ssh"),
force=True, parents=True, uid=user["uid"][0])
mkdir(
os.path.join(user["homeDirectory"][0], ".ssh"),
force=True,
parents=True,
uid=user["uid"][0],
)
# create empty file to set good permissions
write_to_file(authorized_keys_file, "")
@ -125,10 +137,14 @@ def user_ssh_remove_key(username, key):
if not user:
raise Exception("User with username '%s' doesn't exists" % username)
authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys")
authorized_keys_file = os.path.join(
user["homeDirectory"][0], ".ssh", "authorized_keys"
)
if not os.path.exists(authorized_keys_file):
raise Exception("this key doesn't exists ({} dosesn't exists)".format(authorized_keys_file))
raise Exception(
"this key doesn't exists ({} dosesn't exists)".format(authorized_keys_file)
)
authorized_keys_content = read_file(authorized_keys_file)
@ -147,6 +163,7 @@ def user_ssh_remove_key(username, key):
write_to_file(authorized_keys_file, authorized_keys_content)
#
# Helpers
#
@ -164,8 +181,11 @@ def _get_user_for_ssh(username, attrs=None):
# default is “yes”.
sshd_config_content = read_file(SSHD_CONFIG_PATH)
if re.search("^ *PermitRootLogin +(no|forced-commands-only) *$",
sshd_config_content, re.MULTILINE):
if re.search(
"^ *PermitRootLogin +(no|forced-commands-only) *$",
sshd_config_content,
re.MULTILINE,
):
return {"PermitRootLogin": False}
return {"PermitRootLogin": True}
@ -173,31 +193,34 @@ def _get_user_for_ssh(username, attrs=None):
if username == "root":
root_unix = pwd.getpwnam("root")
return {
'username': 'root',
'fullname': '',
'mail': '',
'ssh_allowed': ssh_root_login_status()["PermitRootLogin"],
'shell': root_unix.pw_shell,
'home_path': root_unix.pw_dir,
"username": "root",
"fullname": "",
"mail": "",
"ssh_allowed": ssh_root_login_status()["PermitRootLogin"],
"shell": root_unix.pw_shell,
"home_path": root_unix.pw_dir,
}
if username == "admin":
admin_unix = pwd.getpwnam("admin")
return {
'username': 'admin',
'fullname': '',
'mail': '',
'ssh_allowed': admin_unix.pw_shell.strip() != "/bin/false",
'shell': admin_unix.pw_shell,
'home_path': admin_unix.pw_dir,
"username": "admin",
"fullname": "",
"mail": "",
"ssh_allowed": admin_unix.pw_shell.strip() != "/bin/false",
"shell": admin_unix.pw_shell,
"home_path": admin_unix.pw_dir,
}
# TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
user = ldap.search('ou=users,dc=yunohost,dc=org',
'(&(objectclass=person)(uid=%s))' % username,
attrs)
user = ldap.search(
"ou=users,dc=yunohost,dc=org",
"(&(objectclass=person)(uid=%s))" % username,
attrs,
)
assert len(user) in (0, 1)

View file

@ -6,6 +6,7 @@ import moulinette
from moulinette import m18n, msettings
from yunohost.utils.error import YunohostError
from contextlib import contextmanager
sys.path.append("..")
@ -43,6 +44,7 @@ def raiseYunohostError(mocker, key, **kwargs):
def pytest_addoption(parser):
parser.addoption("--yunodebug", action="store_true", default=False)
#
# Tweak translator to raise exceptions if string keys are not defined #
#
@ -77,5 +79,6 @@ def pytest_cmdline_main(config):
sys.path.insert(0, "/usr/lib/moulinette/")
import yunohost
yunohost.init(debug=config.option.yunodebug)
msettings["interface"] = "test"

View file

@ -159,7 +159,9 @@ def install_legacy_app(domain, path, public=True):
def install_full_domain_app(domain):
app_install(
os.path.join(get_test_apps_dir(), "full_domain_app_ynh"), args="domain=%s" % domain, force=True
os.path.join(get_test_apps_dir(), "full_domain_app_ynh"),
args="domain=%s" % domain,
force=True,
)
@ -376,7 +378,10 @@ def test_systemfuckedup_during_app_upgrade(mocker, secondary_domain):
with pytest.raises(YunohostError):
with message(mocker, "app_action_broke_system"):
app_upgrade("break_yo_system", file=os.path.join(get_test_apps_dir(), "break_yo_system_ynh"))
app_upgrade(
"break_yo_system",
file=os.path.join(get_test_apps_dir(), "break_yo_system_ynh"),
)
def test_failed_multiple_app_upgrade(mocker, secondary_domain):
@ -389,7 +394,9 @@ def test_failed_multiple_app_upgrade(mocker, secondary_domain):
app_upgrade(
["break_yo_system", "legacy_app"],
file={
"break_yo_system": os.path.join(get_test_apps_dir(), "break_yo_system_ynh"),
"break_yo_system": os.path.join(
get_test_apps_dir(), "break_yo_system_ynh"
),
"legacy": os.path.join(get_test_apps_dir(), "legacy_app_ynh"),
},
)

File diff suppressed because it is too large Load diff

View file

@ -9,18 +9,20 @@ from moulinette import m18n
from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml
from yunohost.utils.error import YunohostError
from yunohost.app import (_initialize_apps_catalog_system,
_read_apps_catalog_list,
_update_apps_catalog,
_actual_apps_catalog_api_url,
_load_apps_catalog,
app_catalog,
logger,
APPS_CATALOG_CACHE,
APPS_CATALOG_CONF,
APPS_CATALOG_CRON_PATH,
APPS_CATALOG_API_VERSION,
APPS_CATALOG_DEFAULT_URL)
from yunohost.app import (
_initialize_apps_catalog_system,
_read_apps_catalog_list,
_update_apps_catalog,
_actual_apps_catalog_api_url,
_load_apps_catalog,
app_catalog,
logger,
APPS_CATALOG_CACHE,
APPS_CATALOG_CONF,
APPS_CATALOG_CRON_PATH,
APPS_CATALOG_API_VERSION,
APPS_CATALOG_DEFAULT_URL,
)
APPS_CATALOG_DEFAULT_URL_FULL = _actual_apps_catalog_api_url(APPS_CATALOG_DEFAULT_URL)
CRON_FOLDER, CRON_NAME = APPS_CATALOG_CRON_PATH.rsplit("/", 1)
@ -69,6 +71,7 @@ def cron_job_is_there():
r = os.system("run-parts -v --test %s | grep %s" % (CRON_FOLDER, CRON_NAME))
return r == 0
#
# ################################################
#
@ -86,7 +89,7 @@ def test_apps_catalog_init(mocker):
# Initialize ...
mocker.spy(m18n, "n")
_initialize_apps_catalog_system()
m18n.n.assert_any_call('apps_catalog_init_success')
m18n.n.assert_any_call("apps_catalog_init_success")
# Then there's a cron enabled
assert cron_job_is_there()
@ -159,8 +162,7 @@ def test_apps_catalog_update_404(mocker):
with requests_mock.Mocker() as m:
# 404 error
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL,
status_code=404)
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, status_code=404)
with pytest.raises(YunohostError):
mocker.spy(m18n, "n")
@ -176,8 +178,9 @@ def test_apps_catalog_update_timeout(mocker):
with requests_mock.Mocker() as m:
# Timeout
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL,
exc=requests.exceptions.ConnectTimeout)
m.register_uri(
"GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.ConnectTimeout
)
with pytest.raises(YunohostError):
mocker.spy(m18n, "n")
@ -193,8 +196,9 @@ def test_apps_catalog_update_sslerror(mocker):
with requests_mock.Mocker() as m:
# SSL error
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL,
exc=requests.exceptions.SSLError)
m.register_uri(
"GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.SSLError
)
with pytest.raises(YunohostError):
mocker.spy(m18n, "n")
@ -210,8 +214,9 @@ def test_apps_catalog_update_corrupted(mocker):
with requests_mock.Mocker() as m:
# Corrupted json
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL,
text=DUMMY_APP_CATALOG[:-2])
m.register_uri(
"GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG[:-2]
)
with pytest.raises(YunohostError):
mocker.spy(m18n, "n")
@ -252,8 +257,13 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker):
# Initialize ...
_initialize_apps_catalog_system()
conf = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL},
{"id": "default2", "url": APPS_CATALOG_DEFAULT_URL.replace("yunohost.org", "yolohost.org")}]
conf = [
{"id": "default", "url": APPS_CATALOG_DEFAULT_URL},
{
"id": "default2",
"url": APPS_CATALOG_DEFAULT_URL.replace("yunohost.org", "yolohost.org"),
},
]
write_to_yaml(APPS_CATALOG_CONF, conf)
@ -263,7 +273,11 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker):
# Mock the server response with a dummy apps catalog
# + the same apps catalog for the second list
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL.replace("yunohost.org", "yolohost.org"), text=DUMMY_APP_CATALOG)
m.register_uri(
"GET",
APPS_CATALOG_DEFAULT_URL_FULL.replace("yunohost.org", "yolohost.org"),
text=DUMMY_APP_CATALOG,
)
# Try to load the apps catalog
# This should implicitly trigger an update in the background

View file

@ -16,7 +16,7 @@ def setup_function(function):
try:
app_remove("register_url_app")
except:
except Exception:
pass
@ -24,15 +24,24 @@ def teardown_function(function):
try:
app_remove("register_url_app")
except:
except Exception:
pass
def test_normalize_domain_path():
assert _normalize_domain_path("https://yolo.swag/", "macnuggets") == ("yolo.swag", "/macnuggets")
assert _normalize_domain_path("http://yolo.swag", "/macnuggets/") == ("yolo.swag", "/macnuggets")
assert _normalize_domain_path("yolo.swag/", "macnuggets/") == ("yolo.swag", "/macnuggets")
assert _normalize_domain_path("https://yolo.swag/", "macnuggets") == (
"yolo.swag",
"/macnuggets",
)
assert _normalize_domain_path("http://yolo.swag", "/macnuggets/") == (
"yolo.swag",
"/macnuggets",
)
assert _normalize_domain_path("yolo.swag/", "macnuggets/") == (
"yolo.swag",
"/macnuggets",
)
def test_urlavailable():
@ -47,70 +56,152 @@ def test_urlavailable():
def test_registerurl():
app_install(os.path.join(get_test_apps_dir(), "register_url_app_ynh"),
args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"), force=True)
app_install(
os.path.join(get_test_apps_dir(), "register_url_app_ynh"),
args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"),
force=True,
)
assert not domain_url_available(maindomain, "/urlregisterapp")
# Try installing at same location
with pytest.raises(YunohostError):
app_install(os.path.join(get_test_apps_dir(), "register_url_app_ynh"),
args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"), force=True)
app_install(
os.path.join(get_test_apps_dir(), "register_url_app_ynh"),
args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"),
force=True,
)
def test_registerurl_baddomain():
with pytest.raises(YunohostError):
app_install(os.path.join(get_test_apps_dir(), "register_url_app_ynh"),
args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp"), force=True)
app_install(
os.path.join(get_test_apps_dir(), "register_url_app_ynh"),
args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp"),
force=True,
)
def test_normalize_permission_path():
# Relative path
assert _validate_and_sanitize_permission_url("/wiki/", maindomain + '/path', 'test_permission') == "/wiki"
assert _validate_and_sanitize_permission_url("/", maindomain + '/path', 'test_permission') == "/"
assert _validate_and_sanitize_permission_url("//salut/", maindomain + '/path', 'test_permission') == "/salut"
assert (
_validate_and_sanitize_permission_url(
"/wiki/", maindomain + "/path", "test_permission"
)
== "/wiki"
)
assert (
_validate_and_sanitize_permission_url(
"/", maindomain + "/path", "test_permission"
)
== "/"
)
assert (
_validate_and_sanitize_permission_url(
"//salut/", maindomain + "/path", "test_permission"
)
== "/salut"
)
# Full path
assert _validate_and_sanitize_permission_url(maindomain + "/hey/", maindomain + '/path', 'test_permission') == maindomain + "/hey"
assert _validate_and_sanitize_permission_url(maindomain + "//", maindomain + '/path', 'test_permission') == maindomain + "/"
assert _validate_and_sanitize_permission_url(maindomain + "/", maindomain + '/path', 'test_permission') == maindomain + "/"
assert (
_validate_and_sanitize_permission_url(
maindomain + "/hey/", maindomain + "/path", "test_permission"
)
== maindomain + "/hey"
)
assert (
_validate_and_sanitize_permission_url(
maindomain + "//", maindomain + "/path", "test_permission"
)
== maindomain + "/"
)
assert (
_validate_and_sanitize_permission_url(
maindomain + "/", maindomain + "/path", "test_permission"
)
== maindomain + "/"
)
# Relative Regex
assert _validate_and_sanitize_permission_url("re:/yolo.*/", maindomain + '/path', 'test_permission') == "re:/yolo.*/"
assert _validate_and_sanitize_permission_url("re:/y.*o(o+)[a-z]*/bo\1y", maindomain + '/path', 'test_permission') == "re:/y.*o(o+)[a-z]*/bo\1y"
assert (
_validate_and_sanitize_permission_url(
"re:/yolo.*/", maindomain + "/path", "test_permission"
)
== "re:/yolo.*/"
)
assert (
_validate_and_sanitize_permission_url(
"re:/y.*o(o+)[a-z]*/bo\1y", maindomain + "/path", "test_permission"
)
== "re:/y.*o(o+)[a-z]*/bo\1y"
)
# Full Regex
assert _validate_and_sanitize_permission_url("re:" + maindomain + "/yolo.*/", maindomain + '/path', 'test_permission') == "re:" + maindomain + "/yolo.*/"
assert _validate_and_sanitize_permission_url("re:" + maindomain + "/y.*o(o+)[a-z]*/bo\1y", maindomain + '/path', 'test_permission') == "re:" + maindomain + "/y.*o(o+)[a-z]*/bo\1y"
assert (
_validate_and_sanitize_permission_url(
"re:" + maindomain + "/yolo.*/", maindomain + "/path", "test_permission"
)
== "re:" + maindomain + "/yolo.*/"
)
assert (
_validate_and_sanitize_permission_url(
"re:" + maindomain + "/y.*o(o+)[a-z]*/bo\1y",
maindomain + "/path",
"test_permission",
)
== "re:" + maindomain + "/y.*o(o+)[a-z]*/bo\1y"
)
def test_normalize_permission_path_with_bad_regex():
# Relative Regex
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url("re:/yolo.*[1-7]^?/", maindomain + '/path', 'test_permission')
_validate_and_sanitize_permission_url(
"re:/yolo.*[1-7]^?/", maindomain + "/path", "test_permission"
)
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url("re:/yolo.*[1-7](]/", maindomain + '/path', 'test_permission')
_validate_and_sanitize_permission_url(
"re:/yolo.*[1-7](]/", maindomain + "/path", "test_permission"
)
# Full Regex
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url("re:" + maindomain + "/yolo?+/", maindomain + '/path', 'test_permission')
_validate_and_sanitize_permission_url(
"re:" + maindomain + "/yolo?+/", maindomain + "/path", "test_permission"
)
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url("re:" + maindomain + "/yolo[1-9]**/", maindomain + '/path', 'test_permission')
_validate_and_sanitize_permission_url(
"re:" + maindomain + "/yolo[1-9]**/",
maindomain + "/path",
"test_permission",
)
def test_normalize_permission_path_with_unknown_domain():
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url("shouldntexist.tld/hey", maindomain + '/path', 'test_permission')
_validate_and_sanitize_permission_url(
"shouldntexist.tld/hey", maindomain + "/path", "test_permission"
)
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url("re:shouldntexist.tld/hey.*", maindomain + '/path', 'test_permission')
_validate_and_sanitize_permission_url(
"re:shouldntexist.tld/hey.*", maindomain + "/path", "test_permission"
)
def test_normalize_permission_path_conflicting_path():
app_install(os.path.join(get_test_apps_dir(), "register_url_app_ynh"),
args="domain=%s&path=%s" % (maindomain, "/url/registerapp"), force=True)
app_install(
os.path.join(get_test_apps_dir(), "register_url_app_ynh"),
args="domain=%s&path=%s" % (maindomain, "/url/registerapp"),
force=True,
)
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url("/registerapp", maindomain + '/url', 'test_permission')
_validate_and_sanitize_permission_url(
"/registerapp", maindomain + "/url", "test_permission"
)
with pytest.raises(YunohostError):
_validate_and_sanitize_permission_url(maindomain + "/url/registerapp", maindomain + '/path', 'test_permission')
_validate_and_sanitize_permission_url(
maindomain + "/url/registerapp", maindomain + "/path", "test_permission"
)

View file

@ -7,11 +7,21 @@ from .conftest import message, raiseYunohostError, get_test_apps_dir
from yunohost.app import app_install, app_remove, app_ssowatconf
from yunohost.app import _is_installed
from yunohost.backup import backup_create, backup_restore, backup_list, backup_info, backup_delete, _recursive_umount
from yunohost.backup import (
backup_create,
backup_restore,
backup_list,
backup_info,
backup_delete,
_recursive_umount,
)
from yunohost.domain import _get_maindomain, domain_list, domain_add, domain_remove
from yunohost.user import user_create, user_list, user_delete
from yunohost.permission import user_permission_list
from yunohost.tests.test_permission import check_LDAP_db_integrity, check_permission_for_apps
from yunohost.tests.test_permission import (
check_LDAP_db_integrity,
check_permission_for_apps,
)
from yunohost.hook import CUSTOM_HOOK_FOLDER
# Get main domain
@ -32,7 +42,10 @@ def setup_function(function):
assert len(backup_list()["archives"]) == 0
markers = {m.name: {'args': m.args, 'kwargs': m.kwargs} for m in function.__dict__.get("pytestmark", [])}
markers = {
m.name: {"args": m.args, "kwargs": m.kwargs}
for m in function.__dict__.get("pytestmark", [])
}
if "with_wordpress_archive_from_2p4" in markers:
add_archive_wordpress_from_2p4()
@ -45,14 +58,16 @@ def setup_function(function):
if "with_backup_recommended_app_installed" in markers:
assert not app_is_installed("backup_recommended_app")
install_app("backup_recommended_app_ynh", "/yolo",
"&helper_to_test=ynh_restore_file")
install_app(
"backup_recommended_app_ynh", "/yolo", "&helper_to_test=ynh_restore_file"
)
assert app_is_installed("backup_recommended_app")
if "with_backup_recommended_app_installed_with_ynh_restore" in markers:
assert not app_is_installed("backup_recommended_app")
install_app("backup_recommended_app_ynh", "/yolo",
"&helper_to_test=ynh_restore")
install_app(
"backup_recommended_app_ynh", "/yolo", "&helper_to_test=ynh_restore"
)
assert app_is_installed("backup_recommended_app")
if "with_system_archive_from_2p4" in markers:
@ -62,13 +77,12 @@ def setup_function(function):
if "with_permission_app_installed" in markers:
assert not app_is_installed("permissions_app")
user_create("alice", "Alice", "White", maindomain, "test123Ynh")
install_app("permissions_app_ynh", "/urlpermissionapp"
"&admin=alice")
install_app("permissions_app_ynh", "/urlpermissionapp" "&admin=alice")
assert app_is_installed("permissions_app")
if "with_custom_domain" in markers:
domain = markers['with_custom_domain']['args'][0]
if domain not in domain_list()['domains']:
domain = markers["with_custom_domain"]["args"][0]
if domain not in domain_list()["domains"]:
domain_add(domain)
@ -80,7 +94,10 @@ def teardown_function(function):
delete_all_backups()
uninstall_test_apps_if_needed()
markers = {m.name: {'args': m.args, 'kwargs': m.kwargs} for m in function.__dict__.get("pytestmark", [])}
markers = {
m.name: {"args": m.args, "kwargs": m.kwargs}
for m in function.__dict__.get("pytestmark", [])
}
if "clean_opt_dir" in markers:
shutil.rmtree("/opt/test_backup_output_directory")
@ -89,7 +106,7 @@ def teardown_function(function):
user_delete("alice")
if "with_custom_domain" in markers:
domain = markers['with_custom_domain']['args'][0]
domain = markers["with_custom_domain"]["args"][0]
domain_remove(domain)
@ -106,6 +123,7 @@ def check_permission_for_apps_call():
yield
check_permission_for_apps()
#
# Helpers #
#
@ -128,9 +146,13 @@ def app_is_installed(app):
def backup_test_dependencies_are_met():
# Dummy test apps (or backup archives)
assert os.path.exists(os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4"))
assert os.path.exists(
os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4")
)
assert os.path.exists(os.path.join(get_test_apps_dir(), "legacy_app_ynh"))
assert os.path.exists(os.path.join(get_test_apps_dir(), "backup_recommended_app_ynh"))
assert os.path.exists(
os.path.join(get_test_apps_dir(), "backup_recommended_app_ynh")
)
return True
@ -140,7 +162,7 @@ def tmp_backup_directory_is_empty():
if not os.path.exists("/home/yunohost.backup/tmp/"):
return True
else:
return len(os.listdir('/home/yunohost.backup/tmp/')) == 0
return len(os.listdir("/home/yunohost.backup/tmp/")) == 0
def clean_tmp_backup_directory():
@ -150,15 +172,16 @@ def clean_tmp_backup_directory():
mount_lines = subprocess.check_output("mount").decode().split("\n")
points_to_umount = [line.split(" ")[2]
for line in mount_lines
if len(line) >= 3
and line.split(" ")[2].startswith("/home/yunohost.backup/tmp")]
points_to_umount = [
line.split(" ")[2]
for line in mount_lines
if len(line) >= 3 and line.split(" ")[2].startswith("/home/yunohost.backup/tmp")
]
for point in reversed(points_to_umount):
os.system("umount %s" % point)
for f in os.listdir('/home/yunohost.backup/tmp/'):
for f in os.listdir("/home/yunohost.backup/tmp/"):
shutil.rmtree("/home/yunohost.backup/tmp/%s" % f)
shutil.rmtree("/home/yunohost.backup/tmp/")
@ -186,31 +209,48 @@ def uninstall_test_apps_if_needed():
def install_app(app, path, additionnal_args=""):
app_install(os.path.join(get_test_apps_dir(), app),
args="domain=%s&path=%s%s" % (maindomain, path,
additionnal_args), force=True)
app_install(
os.path.join(get_test_apps_dir(), app),
args="domain=%s&path=%s%s" % (maindomain, path, additionnal_args),
force=True,
)
def add_archive_wordpress_from_2p4():
os.system("mkdir -p /home/yunohost.backup/archives")
os.system("cp " + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.info.json")
+ " /home/yunohost.backup/archives/backup_wordpress_from_2p4.info.json")
os.system(
"cp "
+ os.path.join(
get_test_apps_dir(), "backup_wordpress_from_2p4/backup.info.json"
)
+ " /home/yunohost.backup/archives/backup_wordpress_from_2p4.info.json"
)
os.system("cp " + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.tar.gz")
+ " /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz")
os.system(
"cp "
+ os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.tar.gz")
+ " /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz"
)
def add_archive_system_from_2p4():
os.system("mkdir -p /home/yunohost.backup/archives")
os.system("cp " + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.info.json")
+ " /home/yunohost.backup/archives/backup_system_from_2p4.info.json")
os.system(
"cp "
+ os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.info.json")
+ " /home/yunohost.backup/archives/backup_system_from_2p4.info.json"
)
os.system(
"cp "
+ os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.tar.gz")
+ " /home/yunohost.backup/archives/backup_system_from_2p4.tar.gz"
)
os.system("cp " + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.tar.gz")
+ " /home/yunohost.backup/archives/backup_system_from_2p4.tar.gz")
#
# System backup #
@ -235,7 +275,7 @@ def test_backup_only_ldap(mocker):
def test_backup_system_part_that_does_not_exists(mocker):
# Create the backup
with message(mocker, 'backup_hook_unknown', hook="doesnt_exist"):
with message(mocker, "backup_hook_unknown", hook="doesnt_exist"):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=["doesnt_exist"], apps=None)
@ -256,8 +296,9 @@ def test_backup_and_restore_all_sys(mocker):
archives_info = backup_info(archives[0], with_details=True)
assert archives_info["apps"] == {}
assert (len(archives_info["system"].keys()) ==
len(os.listdir("/usr/share/yunohost/hooks/backup/")))
assert len(archives_info["system"].keys()) == len(
os.listdir("/usr/share/yunohost/hooks/backup/")
)
# Remove ssowat conf
assert os.path.exists("/etc/ssowat/conf.json")
@ -266,8 +307,7 @@ def test_backup_and_restore_all_sys(mocker):
# Restore the backup
with message(mocker, "restore_complete"):
backup_restore(name=archives[0], force=True,
system=[], apps=None)
backup_restore(name=archives[0], force=True, system=[], apps=None)
# Check ssowat conf is back
assert os.path.exists("/etc/ssowat/conf.json")
@ -277,6 +317,7 @@ def test_backup_and_restore_all_sys(mocker):
# System restore from 2.4 #
#
@pytest.mark.with_system_archive_from_2p4
def test_restore_system_from_Ynh2p4(monkeypatch, mocker):
@ -289,16 +330,15 @@ def test_restore_system_from_Ynh2p4(monkeypatch, mocker):
# Restore system archive from 2.4
try:
with message(mocker, "restore_complete"):
backup_restore(name=backup_list()["archives"][1],
system=[],
apps=None,
force=True)
backup_restore(
name=backup_list()["archives"][1], system=[], apps=None, force=True
)
finally:
# Restore system as it was
backup_restore(name=backup_list()["archives"][0],
system=[],
apps=None,
force=True)
backup_restore(
name=backup_list()["archives"][0], system=[], apps=None, force=True
)
#
# App backup #
@ -307,7 +347,6 @@ def test_restore_system_from_Ynh2p4(monkeypatch, mocker):
@pytest.mark.with_backup_recommended_app_installed
def test_backup_script_failure_handling(monkeypatch, mocker):
def custom_hook_exec(name, *args, **kwargs):
if os.path.basename(name).startswith("backup_"):
@ -320,14 +359,13 @@ def test_backup_script_failure_handling(monkeypatch, mocker):
# with the expected error message key
monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec)
with message(mocker, 'backup_app_failed', app='backup_recommended_app'):
with raiseYunohostError(mocker, 'backup_nothings_done'):
with message(mocker, "backup_app_failed", app="backup_recommended_app"):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=None, apps=["backup_recommended_app"])
@pytest.mark.with_backup_recommended_app_installed
def test_backup_not_enough_free_space(monkeypatch, mocker):
def custom_disk_usage(path):
return 99999999999999999
@ -335,10 +373,11 @@ def test_backup_not_enough_free_space(monkeypatch, mocker):
return 0
monkeypatch.setattr("yunohost.backup.disk_usage", custom_disk_usage)
monkeypatch.setattr("yunohost.backup.free_space_in_directory",
custom_free_space_in_directory)
monkeypatch.setattr(
"yunohost.backup.free_space_in_directory", custom_free_space_in_directory
)
with raiseYunohostError(mocker, 'not_enough_disk_space'):
with raiseYunohostError(mocker, "not_enough_disk_space"):
backup_create(system=None, apps=["backup_recommended_app"])
@ -347,7 +386,7 @@ def test_backup_app_not_installed(mocker):
assert not _is_installed("wordpress")
with message(mocker, "unbackup_app", app="wordpress"):
with raiseYunohostError(mocker, 'backup_nothings_done'):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=None, apps=["wordpress"])
@ -358,8 +397,10 @@ def test_backup_app_with_no_backup_script(mocker):
os.system("rm %s" % backup_script)
assert not os.path.exists(backup_script)
with message(mocker, "backup_with_no_backup_script_for_app", app="backup_recommended_app"):
with raiseYunohostError(mocker, 'backup_nothings_done'):
with message(
mocker, "backup_with_no_backup_script_for_app", app="backup_recommended_app"
):
with raiseYunohostError(mocker, "backup_nothings_done"):
backup_create(system=None, apps=["backup_recommended_app"])
@ -373,7 +414,9 @@ def test_backup_app_with_no_restore_script(mocker):
# Backuping an app with no restore script will only display a warning to the
# user...
with message(mocker, "backup_with_no_restore_script_for_app", app="backup_recommended_app"):
with message(
mocker, "backup_with_no_restore_script_for_app", app="backup_recommended_app"
):
backup_create(system=None, apps=["backup_recommended_app"])
@ -382,9 +425,12 @@ def test_backup_with_different_output_directory(mocker):
# Create the backup
with message(mocker, "backup_created"):
backup_create(system=["conf_ssh"], apps=None,
output_directory="/opt/test_backup_output_directory",
name="backup")
backup_create(
system=["conf_ssh"],
apps=None,
output_directory="/opt/test_backup_output_directory",
name="backup",
)
assert os.path.exists("/opt/test_backup_output_directory/backup.tar")
@ -402,10 +448,13 @@ def test_backup_using_copy_method(mocker):
# Create the backup
with message(mocker, "backup_created"):
backup_create(system=["conf_nginx"], apps=None,
output_directory="/opt/test_backup_output_directory",
methods=["copy"],
name="backup")
backup_create(
system=["conf_nginx"],
apps=None,
output_directory="/opt/test_backup_output_directory",
methods=["copy"],
name="backup",
)
assert os.path.exists("/opt/test_backup_output_directory/info.json")
@ -414,19 +463,20 @@ def test_backup_using_copy_method(mocker):
# App restore #
#
@pytest.mark.with_wordpress_archive_from_2p4
@pytest.mark.with_custom_domain("yolo.test")
def test_restore_app_wordpress_from_Ynh2p4(mocker):
with message(mocker, "restore_complete"):
backup_restore(system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
@pytest.mark.with_wordpress_archive_from_2p4
@pytest.mark.with_custom_domain("yolo.test")
def test_restore_app_script_failure_handling(monkeypatch, mocker):
def custom_hook_exec(name, *args, **kwargs):
if os.path.basename(name).startswith("restore"):
monkeypatch.undo()
@ -436,28 +486,30 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker):
assert not _is_installed("wordpress")
with message(mocker, 'restore_app_failed', app='wordpress'):
with raiseYunohostError(mocker, 'restore_nothings_done'):
backup_restore(system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
with message(mocker, "restore_app_failed", app="wordpress"):
with raiseYunohostError(mocker, "restore_nothings_done"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
assert not _is_installed("wordpress")
@pytest.mark.with_wordpress_archive_from_2p4
def test_restore_app_not_enough_free_space(monkeypatch, mocker):
def custom_free_space_in_directory(dirpath):
return 0
monkeypatch.setattr("yunohost.backup.free_space_in_directory",
custom_free_space_in_directory)
monkeypatch.setattr(
"yunohost.backup.free_space_in_directory", custom_free_space_in_directory
)
assert not _is_installed("wordpress")
with raiseYunohostError(mocker, 'restore_not_enough_disk_space'):
backup_restore(system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
with raiseYunohostError(mocker, "restore_not_enough_disk_space"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
assert not _is_installed("wordpress")
@ -468,10 +520,11 @@ def test_restore_app_not_in_backup(mocker):
assert not _is_installed("wordpress")
assert not _is_installed("yoloswag")
with message(mocker, 'backup_archive_app_not_found', app="yoloswag"):
with raiseYunohostError(mocker, 'restore_nothings_done'):
backup_restore(system=None, name=backup_list()["archives"][0],
apps=["yoloswag"])
with message(mocker, "backup_archive_app_not_found", app="yoloswag"):
with raiseYunohostError(mocker, "restore_nothings_done"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["yoloswag"]
)
assert not _is_installed("wordpress")
assert not _is_installed("yoloswag")
@ -484,14 +537,16 @@ def test_restore_app_already_installed(mocker):
assert not _is_installed("wordpress")
with message(mocker, "restore_complete"):
backup_restore(system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
assert _is_installed("wordpress")
with raiseYunohostError(mocker, 'restore_already_installed_apps'):
backup_restore(system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
with raiseYunohostError(mocker, "restore_already_installed_apps"):
backup_restore(
system=None, name=backup_list()["archives"][0], apps=["wordpress"]
)
assert _is_installed("wordpress")
@ -517,33 +572,33 @@ def test_backup_and_restore_with_ynh_restore(mocker):
@pytest.mark.with_permission_app_installed
def test_backup_and_restore_permission_app(mocker):
res = user_permission_list(full=True)['permissions']
res = user_permission_list(full=True)["permissions"]
assert "permissions_app.main" in res
assert "permissions_app.admin" in res
assert "permissions_app.dev" in res
assert res['permissions_app.main']['url'] == "/"
assert res['permissions_app.admin']['url'] == "/admin"
assert res['permissions_app.dev']['url'] == "/dev"
assert res["permissions_app.main"]["url"] == "/"
assert res["permissions_app.admin"]["url"] == "/admin"
assert res["permissions_app.dev"]["url"] == "/dev"
assert "visitors" in res['permissions_app.main']['allowed']
assert "all_users" in res['permissions_app.main']['allowed']
assert res['permissions_app.admin']['allowed'] == ["alice"]
assert res['permissions_app.dev']['allowed'] == []
assert "visitors" in res["permissions_app.main"]["allowed"]
assert "all_users" in res["permissions_app.main"]["allowed"]
assert res["permissions_app.admin"]["allowed"] == ["alice"]
assert res["permissions_app.dev"]["allowed"] == []
_test_backup_and_restore_app(mocker, "permissions_app")
res = user_permission_list(full=True)['permissions']
res = user_permission_list(full=True)["permissions"]
assert "permissions_app.main" in res
assert "permissions_app.admin" in res
assert "permissions_app.dev" in res
assert res['permissions_app.main']['url'] == "/"
assert res['permissions_app.admin']['url'] == "/admin"
assert res['permissions_app.dev']['url'] == "/dev"
assert res["permissions_app.main"]["url"] == "/"
assert res["permissions_app.admin"]["url"] == "/admin"
assert res["permissions_app.dev"]["url"] == "/dev"
assert "visitors" in res['permissions_app.main']['allowed']
assert "all_users" in res['permissions_app.main']['allowed']
assert res['permissions_app.admin']['allowed'] == ["alice"]
assert res['permissions_app.dev']['allowed'] == []
assert "visitors" in res["permissions_app.main"]["allowed"]
assert "all_users" in res["permissions_app.main"]["allowed"]
assert res["permissions_app.admin"]["allowed"] == ["alice"]
assert res["permissions_app.dev"]["allowed"] == []
def _test_backup_and_restore_app(mocker, app):
@ -563,19 +618,19 @@ def _test_backup_and_restore_app(mocker, app):
# Uninstall the app
app_remove(app)
assert not app_is_installed(app)
assert app + ".main" not in user_permission_list()['permissions']
assert app + ".main" not in user_permission_list()["permissions"]
# Restore the app
with message(mocker, "restore_complete"):
backup_restore(system=None, name=archives[0],
apps=[app])
backup_restore(system=None, name=archives[0], apps=[app])
assert app_is_installed(app)
# Check permission
per_list = user_permission_list()['permissions']
per_list = user_permission_list()["permissions"]
assert app + ".main" in per_list
#
# Some edge cases #
#
@ -589,7 +644,7 @@ def test_restore_archive_with_no_json(mocker):
assert "badbackup" in backup_list()["archives"]
with raiseYunohostError(mocker, 'backup_archive_cant_retrieve_info_json'):
with raiseYunohostError(mocker, "backup_archive_cant_retrieve_info_json"):
backup_restore(name="badbackup", force=True)
@ -597,11 +652,13 @@ def test_restore_archive_with_no_json(mocker):
def test_restore_archive_with_bad_archive(mocker):
# Break the archive
os.system("head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz > /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz")
os.system(
"head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz > /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz"
)
assert "backup_wordpress_from_2p4" in backup_list()["archives"]
with raiseYunohostError(mocker, 'backup_archive_open_failed'):
with raiseYunohostError(mocker, "backup_archive_open_failed"):
backup_restore(name="backup_wordpress_from_2p4", force=True)
clean_tmp_backup_directory()
@ -609,7 +666,7 @@ def test_restore_archive_with_bad_archive(mocker):
def test_restore_archive_with_custom_hook(mocker):
custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, 'restore')
custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore")
os.system("touch %s/99-yolo" % custom_restore_hook_folder)
# Backup with custom hook system
@ -620,22 +677,23 @@ def test_restore_archive_with_custom_hook(mocker):
# Restore system with custom hook
with message(mocker, "restore_complete"):
backup_restore(name=backup_list()["archives"][0],
system=[],
apps=None,
force=True)
backup_restore(
name=backup_list()["archives"][0], system=[], apps=None, force=True
)
os.system("rm %s/99-yolo" % custom_restore_hook_folder)
def test_backup_binds_are_readonly(mocker, monkeypatch):
def custom_mount_and_backup(self):
self._organize_files()
confssh = os.path.join(self.work_dir, "conf/ssh")
output = subprocess.check_output("touch %s/test 2>&1 || true" % confssh,
shell=True, env={'LANG': 'en_US.UTF-8'})
output = subprocess.check_output(
"touch %s/test 2>&1 || true" % confssh,
shell=True,
env={"LANG": "en_US.UTF-8"},
)
output = output.decode()
assert "Read-only file system" in output
@ -645,8 +703,9 @@ def test_backup_binds_are_readonly(mocker, monkeypatch):
self.clean()
monkeypatch.setattr("yunohost.backup.BackupMethod.mount_and_backup",
custom_mount_and_backup)
monkeypatch.setattr(
"yunohost.backup.BackupMethod.mount_and_backup", custom_mount_and_backup
)
# Create the backup
with message(mocker, "backup_created"):

View file

@ -24,8 +24,11 @@ def teardown_function(function):
def install_changeurl_app(path):
app_install(os.path.join(get_test_apps_dir(), "change_url_app_ynh"),
args="domain=%s&path=%s" % (maindomain, path), force=True)
app_install(
os.path.join(get_test_apps_dir(), "change_url_app_ynh"),
args="domain=%s&path=%s" % (maindomain, path),
force=True,
)
def check_changeurl_app(path):
@ -35,7 +38,9 @@ def check_changeurl_app(path):
assert appmap[maindomain][path]["id"] == "change_url_app"
r = requests.get("https://127.0.0.1%s/" % path, headers={"domain": maindomain}, verify=False)
r = requests.get(
"https://127.0.0.1%s/" % path, headers={"domain": maindomain}, verify=False
)
assert r.status_code == 200

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,12 @@ import os
from .conftest import message
from yunohost.domain import domain_add, domain_remove, domain_list
from yunohost.regenconf import regen_conf, manually_modified_files, _get_conf_hashes, _force_clear_hashes
from yunohost.regenconf import (
regen_conf,
manually_modified_files,
_get_conf_hashes,
_force_clear_hashes,
)
TEST_DOMAIN = "secondarydomain.test"
TEST_DOMAIN_NGINX_CONFIG = "/etc/nginx/conf.d/%s.conf" % TEST_DOMAIN
@ -39,7 +44,7 @@ def clean():
assert TEST_DOMAIN_NGINX_CONFIG not in _get_conf_hashes("nginx")
assert TEST_DOMAIN_NGINX_CONFIG not in manually_modified_files()
regen_conf(['ssh'], force=True)
regen_conf(["ssh"], force=True)
def test_add_domain():
@ -107,7 +112,7 @@ def test_ssh_conf_unmanaged_and_manually_modified(mocker):
assert SSHD_CONFIG in _get_conf_hashes("ssh")
assert SSHD_CONFIG in manually_modified_files()
regen_conf(['ssh'], force=True)
regen_conf(["ssh"], force=True)
assert SSHD_CONFIG in _get_conf_hashes("ssh")
assert SSHD_CONFIG not in manually_modified_files()
@ -158,6 +163,7 @@ def test_stale_hashes_if_file_manually_deleted():
assert not os.path.exists(TEST_DOMAIN_DNSMASQ_CONFIG)
assert TEST_DOMAIN_DNSMASQ_CONFIG not in _get_conf_hashes("dnsmasq")
# This test only works if you comment the part at the end of the regen-conf in
# dnsmasq that auto-flag /etc/dnsmasq.d/foo.bar as "to be removed" (using touch)
# ... But we want to keep it because they also possibly flag files that were

View file

@ -2,7 +2,14 @@ import os
from .conftest import raiseYunohostError
from yunohost.service import _get_services, _save_services, service_status, service_add, service_remove, service_log
from yunohost.service import (
_get_services,
_save_services,
service_status,
service_add,
service_remove,
service_log,
)
def setup_function(function):
@ -55,7 +62,7 @@ def test_service_log():
def test_service_status_unknown_service(mocker):
with raiseYunohostError(mocker, 'service_unknown'):
with raiseYunohostError(mocker, "service_unknown"):
service_status(["ssh", "doesnotexists"])
@ -83,7 +90,7 @@ def test_service_remove_service_that_doesnt_exists(mocker):
assert "dummyservice" not in service_status().keys()
with raiseYunohostError(mocker, 'service_unknown'):
with raiseYunohostError(mocker, "service_unknown"):
service_remove("dummyservice")
assert "dummyservice" not in service_status().keys()

View file

@ -4,9 +4,17 @@ import pytest
from yunohost.utils.error import YunohostError
from yunohost.settings import settings_get, settings_list, _get_settings, \
settings_set, settings_reset, settings_reset_all, \
SETTINGS_PATH_OTHER_LOCATION, SETTINGS_PATH, DEFAULTS
from yunohost.settings import (
settings_get,
settings_list,
_get_settings,
settings_set,
settings_reset,
settings_reset_all,
SETTINGS_PATH_OTHER_LOCATION,
SETTINGS_PATH,
DEFAULTS,
)
DEFAULTS["example.bool"] = {"type": "bool", "default": True}
DEFAULTS["example.int"] = {"type": "int", "default": 42}
@ -27,7 +35,12 @@ def test_settings_get_bool():
def test_settings_get_full_bool():
assert settings_get("example.bool", True) == {"type": "bool", "value": True, "default": True, "description": "Dummy bool setting"}
assert settings_get("example.bool", True) == {
"type": "bool",
"value": True,
"default": True,
"description": "Dummy bool setting",
}
def test_settings_get_int():
@ -35,7 +48,12 @@ def test_settings_get_int():
def test_settings_get_full_int():
assert settings_get("example.int", True) == {"type": "int", "value": 42, "default": 42, "description": "Dummy int setting"}
assert settings_get("example.int", True) == {
"type": "int",
"value": 42,
"default": 42,
"description": "Dummy int setting",
}
def test_settings_get_string():
@ -43,7 +61,12 @@ def test_settings_get_string():
def test_settings_get_full_string():
assert settings_get("example.string", True) == {"type": "string", "value": "yolo swag", "default": "yolo swag", "description": "Dummy string setting"}
assert settings_get("example.string", True) == {
"type": "string",
"value": "yolo swag",
"default": "yolo swag",
"description": "Dummy string setting",
}
def test_settings_get_enum():
@ -51,7 +74,13 @@ def test_settings_get_enum():
def test_settings_get_full_enum():
assert settings_get("example.enum", True) == {"type": "enum", "value": "a", "default": "a", "description": "Dummy enum setting", "choices": ["a", "b", "c"]}
assert settings_get("example.enum", True) == {
"type": "enum",
"value": "a",
"default": "a",
"description": "Dummy enum setting",
"choices": ["a", "b", "c"],
}
def test_settings_get_doesnt_exists():
@ -120,7 +149,12 @@ def test_settings_set_bad_value_enum():
def test_settings_list_modified():
settings_set("example.int", 21)
assert settings_list()["example.int"] == {'default': 42, 'description': 'Dummy int setting', 'type': 'int', 'value': 21}
assert settings_list()["example.int"] == {
"default": 42,
"description": "Dummy int setting",
"type": "int",
"value": 21,
}
def test_reset():

View file

@ -2,8 +2,17 @@ import pytest
from .conftest import message, raiseYunohostError
from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \
user_group_list, user_group_create, user_group_delete, user_group_update
from yunohost.user import (
user_list,
user_info,
user_create,
user_delete,
user_update,
user_group_list,
user_group_create,
user_group_delete,
user_group_update,
)
from yunohost.domain import _get_maindomain
from yunohost.tests.test_permission import check_LDAP_db_integrity
@ -12,10 +21,10 @@ maindomain = ""
def clean_user_groups():
for u in user_list()['users']:
for u in user_list()["users"]:
user_delete(u)
for g in user_group_list()['groups']:
for g in user_group_list()["groups"]:
if g not in ["all_users", "visitors"]:
user_group_delete(g)
@ -46,13 +55,14 @@ def check_LDAP_db_integrity_call():
yield
check_LDAP_db_integrity()
#
# List functions
#
def test_list_users():
res = user_list()['users']
res = user_list()["users"]
assert "alice" in res
assert "bob" in res
@ -60,7 +70,7 @@ def test_list_users():
def test_list_groups():
res = user_group_list()['groups']
res = user_group_list()["groups"]
assert "all_users" in res
assert "alice" in res
@ -68,8 +78,9 @@ def test_list_groups():
assert "jack" in res
for u in ["alice", "bob", "jack"]:
assert u in res
assert u in res[u]['members']
assert u in res["all_users"]['members']
assert u in res[u]["members"]
assert u in res["all_users"]["members"]
#
# Create - Remove functions
@ -81,11 +92,11 @@ def test_create_user(mocker):
with message(mocker, "user_created"):
user_create("albert", "Albert", "Good", maindomain, "test123Ynh")
group_res = user_group_list()['groups']
assert "albert" in user_list()['users']
group_res = user_group_list()["groups"]
assert "albert" in user_list()["users"]
assert "albert" in group_res
assert "albert" in group_res['albert']['members']
assert "albert" in group_res['all_users']['members']
assert "albert" in group_res["albert"]["members"]
assert "albert" in group_res["all_users"]["members"]
def test_del_user(mocker):
@ -93,10 +104,10 @@ def test_del_user(mocker):
with message(mocker, "user_deleted"):
user_delete("alice")
group_res = user_group_list()['groups']
group_res = user_group_list()["groups"]
assert "alice" not in user_list()
assert "alice" not in group_res
assert "alice" not in group_res['all_users']['members']
assert "alice" not in group_res["all_users"]["members"]
def test_create_group(mocker):
@ -104,9 +115,9 @@ def test_create_group(mocker):
with message(mocker, "group_created", group="adminsys"):
user_group_create("adminsys")
group_res = user_group_list()['groups']
group_res = user_group_list()["groups"]
assert "adminsys" in group_res
assert "members" in group_res['adminsys'].keys()
assert "members" in group_res["adminsys"].keys()
assert group_res["adminsys"]["members"] == []
@ -115,9 +126,10 @@ def test_del_group(mocker):
with message(mocker, "group_deleted", group="dev"):
user_group_delete("dev")
group_res = user_group_list()['groups']
group_res = user_group_list()["groups"]
assert "dev" not in group_res
#
# Error on create / remove function
#
@ -174,6 +186,7 @@ def test_del_group_that_does_not_exist(mocker):
with raiseYunohostError(mocker, "group_unknown"):
user_group_delete("doesnt_exist")
#
# Update function
#
@ -184,40 +197,41 @@ def test_update_user(mocker):
user_update("alice", firstname="NewName", lastname="NewLast")
info = user_info("alice")
assert info['firstname'] == "NewName"
assert info['lastname'] == "NewLast"
assert info["firstname"] == "NewName"
assert info["lastname"] == "NewLast"
def test_update_group_add_user(mocker):
with message(mocker, "group_updated", group="dev"):
user_group_update("dev", add=["bob"])
group_res = user_group_list()['groups']
assert set(group_res['dev']['members']) == set(["alice", "bob"])
group_res = user_group_list()["groups"]
assert set(group_res["dev"]["members"]) == set(["alice", "bob"])
def test_update_group_add_user_already_in(mocker):
with message(mocker, "group_user_already_in_group", user="bob", group="apps"):
user_group_update("apps", add=["bob"])
group_res = user_group_list()['groups']
assert group_res['apps']['members'] == ["bob"]
group_res = user_group_list()["groups"]
assert group_res["apps"]["members"] == ["bob"]
def test_update_group_remove_user(mocker):
with message(mocker, "group_updated", group="apps"):
user_group_update("apps", remove=["bob"])
group_res = user_group_list()['groups']
assert group_res['apps']['members'] == []
group_res = user_group_list()["groups"]
assert group_res["apps"]["members"] == []
def test_update_group_remove_user_not_already_in(mocker):
with message(mocker, "group_user_not_in_group", user="jack", group="apps"):
user_group_update("apps", remove=["jack"])
group_res = user_group_list()['groups']
assert group_res['apps']['members'] == ["bob"]
group_res = user_group_list()["groups"]
assert group_res["apps"]["members"] == ["bob"]
#
# Error on update functions

File diff suppressed because it is too large Load diff

View file

@ -41,7 +41,7 @@ from yunohost.utils.error import YunohostError
from yunohost.service import service_status
from yunohost.log import is_unit_operation
logger = getActionLogger('yunohost.user')
logger = getActionLogger("yunohost.user")
def user_list(fields=None):
@ -49,16 +49,16 @@ def user_list(fields=None):
from yunohost.utils.ldap import _get_ldap_interface
user_attrs = {
'uid': 'username',
'cn': 'fullname',
'mail': 'mail',
'maildrop': 'mail-forward',
'loginShell': 'shell',
'homeDirectory': 'home_path',
'mailuserquota': 'mailbox-quota'
"uid": "username",
"cn": "fullname",
"mail": "mail",
"maildrop": "mail-forward",
"loginShell": "shell",
"homeDirectory": "home_path",
"mailuserquota": "mailbox-quota",
}
attrs = ['uid']
attrs = ["uid"]
users = {}
if fields:
@ -67,14 +67,16 @@ def user_list(fields=None):
if attr in keys:
attrs.append(attr)
else:
raise YunohostError('field_invalid', attr)
raise YunohostError("field_invalid", attr)
else:
attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell']
attrs = ["uid", "cn", "mail", "mailuserquota", "loginShell"]
ldap = _get_ldap_interface()
result = ldap.search('ou=users,dc=yunohost,dc=org',
'(&(objectclass=person)(!(uid=root))(!(uid=nobody)))',
attrs)
result = ldap.search(
"ou=users,dc=yunohost,dc=org",
"(&(objectclass=person)(!(uid=root))(!(uid=nobody)))",
attrs,
)
for user in result:
entry = {}
@ -88,15 +90,23 @@ def user_list(fields=None):
entry[user_attrs[attr]] = values[0]
uid = entry[user_attrs['uid']]
uid = entry[user_attrs["uid"]]
users[uid] = entry
return {'users': users}
return {"users": users}
@is_unit_operation([('username', 'user')])
def user_create(operation_logger, username, firstname, lastname, domain, password,
mailbox_quota="0", mail=None):
@is_unit_operation([("username", "user")])
def user_create(
operation_logger,
username,
firstname,
lastname,
domain,
password,
mailbox_quota="0",
mail=None,
):
from yunohost.domain import domain_list, _get_maindomain
from yunohost.hook import hook_callback
@ -107,29 +117,33 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor
assert_password_is_strong_enough("user", password)
if mail is not None:
logger.warning("Packagers ! Using --mail in 'yunohost user create' is deprecated ... please use --domain instead.")
logger.warning(
"Packagers ! Using --mail in 'yunohost user create' is deprecated ... please use --domain instead."
)
domain = mail.split("@")[-1]
# Validate domain used for email address/xmpp account
if domain is None:
if msettings.get('interface') == 'api':
raise YunohostError('Invalide usage, specify domain argument')
if msettings.get("interface") == "api":
raise YunohostError("Invalide usage, specify domain argument")
else:
# On affiche les differents domaines possibles
msignals.display(m18n.n('domains_available'))
for domain in domain_list()['domains']:
msignals.display(m18n.n("domains_available"))
for domain in domain_list()["domains"]:
msignals.display("- {}".format(domain))
maindomain = _get_maindomain()
domain = msignals.prompt(m18n.n('ask_user_domain') + ' (default: %s)' % maindomain)
domain = msignals.prompt(
m18n.n("ask_user_domain") + " (default: %s)" % maindomain
)
if not domain:
domain = maindomain
# Check that the domain exists
if domain not in domain_list()['domains']:
raise YunohostError('domain_name_unknown', domain=domain)
if domain not in domain_list()["domains"]:
raise YunohostError("domain_name_unknown", domain=domain)
mail = username + '@' + domain
mail = username + "@" + domain
ldap = _get_ldap_interface()
if username in user_list()["users"]:
@ -137,30 +151,26 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor
# Validate uniqueness of username and mail in LDAP
try:
ldap.validate_uniqueness({
'uid': username,
'mail': mail,
'cn': username
})
ldap.validate_uniqueness({"uid": username, "mail": mail, "cn": username})
except Exception as e:
raise YunohostError('user_creation_failed', user=username, error=e)
raise YunohostError("user_creation_failed", user=username, error=e)
# Validate uniqueness of username in system users
all_existing_usernames = {x.pw_name for x in pwd.getpwall()}
if username in all_existing_usernames:
raise YunohostError('system_username_exists')
raise YunohostError("system_username_exists")
main_domain = _get_maindomain()
aliases = [
'root@' + main_domain,
'admin@' + main_domain,
'webmaster@' + main_domain,
'postmaster@' + main_domain,
'abuse@' + main_domain,
"root@" + main_domain,
"admin@" + main_domain,
"webmaster@" + main_domain,
"postmaster@" + main_domain,
"abuse@" + main_domain,
]
if mail in aliases:
raise YunohostError('mail_unavailable')
raise YunohostError("mail_unavailable")
operation_logger.start()
@ -175,49 +185,53 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor
uid_guid_found = uid not in all_uid and uid not in all_gid
# Adapt values for LDAP
fullname = '%s %s' % (firstname, lastname)
fullname = "%s %s" % (firstname, lastname)
attr_dict = {
'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh'],
'givenName': [firstname],
'sn': [lastname],
'displayName': [fullname],
'cn': [fullname],
'uid': [username],
'mail': mail, # NOTE: this one seems to be already a list
'maildrop': [username],
'mailuserquota': [mailbox_quota],
'userPassword': [_hash_user_password(password)],
'gidNumber': [uid],
'uidNumber': [uid],
'homeDirectory': ['/home/' + username],
'loginShell': ['/bin/false']
"objectClass": [
"mailAccount",
"inetOrgPerson",
"posixAccount",
"userPermissionYnh",
],
"givenName": [firstname],
"sn": [lastname],
"displayName": [fullname],
"cn": [fullname],
"uid": [username],
"mail": mail, # NOTE: this one seems to be already a list
"maildrop": [username],
"mailuserquota": [mailbox_quota],
"userPassword": [_hash_user_password(password)],
"gidNumber": [uid],
"uidNumber": [uid],
"homeDirectory": ["/home/" + username],
"loginShell": ["/bin/false"],
}
# If it is the first user, add some aliases
if not ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'):
attr_dict['mail'] = [attr_dict['mail']] + aliases
if not ldap.search(base="ou=users,dc=yunohost,dc=org", filter="uid=*"):
attr_dict["mail"] = [attr_dict["mail"]] + aliases
try:
ldap.add('uid=%s,ou=users' % username, attr_dict)
ldap.add("uid=%s,ou=users" % username, attr_dict)
except Exception as e:
raise YunohostError('user_creation_failed', user=username, error=e)
raise YunohostError("user_creation_failed", user=username, error=e)
# Invalidate passwd and group to take user and group creation into account
subprocess.call(['nscd', '-i', 'passwd'])
subprocess.call(['nscd', '-i', 'group'])
subprocess.call(["nscd", "-i", "passwd"])
subprocess.call(["nscd", "-i", "group"])
try:
# Attempt to create user home folder
subprocess.check_call(["mkhomedir_helper", username])
except subprocess.CalledProcessError:
if not os.path.isdir('/home/{0}'.format(username)):
logger.warning(m18n.n('user_home_creation_failed'),
exc_info=1)
if not os.path.isdir("/home/{0}".format(username)):
logger.warning(m18n.n("user_home_creation_failed"), exc_info=1)
# Create group for user and add to group 'all_users'
user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False)
user_group_update(groupname='all_users', add=username, force=True, sync_perm=True)
user_group_update(groupname="all_users", add=username, force=True, sync_perm=True)
# Trigger post_user_create hooks
env_dict = {
@ -225,18 +239,18 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor
"YNH_USER_MAIL": mail,
"YNH_USER_PASSWORD": password,
"YNH_USER_FIRSTNAME": firstname,
"YNH_USER_LASTNAME": lastname
"YNH_USER_LASTNAME": lastname,
}
hook_callback('post_user_create', args=[username, mail], env=env_dict)
hook_callback("post_user_create", args=[username, mail], env=env_dict)
# TODO: Send a welcome mail to user
logger.success(m18n.n('user_created'))
logger.success(m18n.n("user_created"))
return {'fullname': fullname, 'username': username, 'mail': mail}
return {"fullname": fullname, "username": username, "mail": mail}
@is_unit_operation([('username', 'user')])
@is_unit_operation([("username", "user")])
def user_delete(operation_logger, username, purge=False):
"""
Delete user
@ -250,7 +264,7 @@ def user_delete(operation_logger, username, purge=False):
from yunohost.utils.ldap import _get_ldap_interface
if username not in user_list()["users"]:
raise YunohostError('user_unknown', user=username)
raise YunohostError("user_unknown", user=username)
operation_logger.start()
@ -266,31 +280,41 @@ def user_delete(operation_logger, username, purge=False):
# Delete primary group if it exists (why wouldnt it exists ? because some
# epic bug happened somewhere else and only a partial removal was
# performed...)
if username in user_group_list()['groups'].keys():
if username in user_group_list()["groups"].keys():
user_group_delete(username, force=True, sync_perm=True)
ldap = _get_ldap_interface()
try:
ldap.remove('uid=%s,ou=users' % username)
ldap.remove("uid=%s,ou=users" % username)
except Exception as e:
raise YunohostError('user_deletion_failed', user=username, error=e)
raise YunohostError("user_deletion_failed", user=username, error=e)
# Invalidate passwd to take user deletion into account
subprocess.call(['nscd', '-i', 'passwd'])
subprocess.call(["nscd", "-i", "passwd"])
if purge:
subprocess.call(['rm', '-rf', '/home/{0}'.format(username)])
subprocess.call(['rm', '-rf', '/var/mail/{0}'.format(username)])
subprocess.call(["rm", "-rf", "/home/{0}".format(username)])
subprocess.call(["rm", "-rf", "/var/mail/{0}".format(username)])
hook_callback('post_user_delete', args=[username, purge])
hook_callback("post_user_delete", args=[username, purge])
logger.success(m18n.n('user_deleted'))
logger.success(m18n.n("user_deleted"))
@is_unit_operation([('username', 'user')], exclude=['change_password'])
def user_update(operation_logger, username, firstname=None, lastname=None, mail=None,
change_password=None, add_mailforward=None, remove_mailforward=None,
add_mailalias=None, remove_mailalias=None, mailbox_quota=None):
@is_unit_operation([("username", "user")], exclude=["change_password"])
def user_update(
operation_logger,
username,
firstname=None,
lastname=None,
mail=None,
change_password=None,
add_mailforward=None,
remove_mailforward=None,
add_mailalias=None,
remove_mailalias=None,
mailbox_quota=None,
):
"""
Update user informations
@ -312,130 +336,142 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail=
from yunohost.utils.ldap import _get_ldap_interface
from yunohost.hook import hook_callback
domains = domain_list()['domains']
domains = domain_list()["domains"]
# Populate user informations
ldap = _get_ldap_interface()
attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop']
result = ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch)
attrs_to_fetch = ["givenName", "sn", "mail", "maildrop"]
result = ldap.search(
base="ou=users,dc=yunohost,dc=org",
filter="uid=" + username,
attrs=attrs_to_fetch,
)
if not result:
raise YunohostError('user_unknown', user=username)
raise YunohostError("user_unknown", user=username)
user = result[0]
env_dict = {
"YNH_USER_USERNAME": username
}
env_dict = {"YNH_USER_USERNAME": username}
# Get modifications from arguments
new_attr_dict = {}
if firstname:
new_attr_dict['givenName'] = [firstname] # TODO: Validate
new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + user['sn'][0]]
new_attr_dict["givenName"] = [firstname] # TODO: Validate
new_attr_dict["cn"] = new_attr_dict["displayName"] = [
firstname + " " + user["sn"][0]
]
env_dict["YNH_USER_FIRSTNAME"] = firstname
if lastname:
new_attr_dict['sn'] = [lastname] # TODO: Validate
new_attr_dict['cn'] = new_attr_dict['displayName'] = [user['givenName'][0] + ' ' + lastname]
new_attr_dict["sn"] = [lastname] # TODO: Validate
new_attr_dict["cn"] = new_attr_dict["displayName"] = [
user["givenName"][0] + " " + lastname
]
env_dict["YNH_USER_LASTNAME"] = lastname
if lastname and firstname:
new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + lastname]
new_attr_dict["cn"] = new_attr_dict["displayName"] = [
firstname + " " + lastname
]
# change_password is None if user_update is not called to change the password
if change_password is not None:
# when in the cli interface if the option to change the password is called
# without a specified value, change_password will be set to the const 0.
# In this case we prompt for the new password.
if msettings.get('interface') == 'cli' and not change_password:
if msettings.get("interface") == "cli" and not change_password:
change_password = msignals.prompt(m18n.n("ask_password"), True, True)
# Ensure sufficiently complex password
assert_password_is_strong_enough("user", change_password)
new_attr_dict['userPassword'] = [_hash_user_password(change_password)]
new_attr_dict["userPassword"] = [_hash_user_password(change_password)]
env_dict["YNH_USER_PASSWORD"] = change_password
if mail:
main_domain = _get_maindomain()
aliases = [
'root@' + main_domain,
'admin@' + main_domain,
'webmaster@' + main_domain,
'postmaster@' + main_domain,
"root@" + main_domain,
"admin@" + main_domain,
"webmaster@" + main_domain,
"postmaster@" + main_domain,
]
try:
ldap.validate_uniqueness({'mail': mail})
ldap.validate_uniqueness({"mail": mail})
except Exception as e:
raise YunohostError('user_update_failed', user=username, error=e)
if mail[mail.find('@') + 1:] not in domains:
raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:])
raise YunohostError("user_update_failed", user=username, error=e)
if mail[mail.find("@") + 1 :] not in domains:
raise YunohostError(
"mail_domain_unknown", domain=mail[mail.find("@") + 1 :]
)
if mail in aliases:
raise YunohostError('mail_unavailable')
raise YunohostError("mail_unavailable")
del user['mail'][0]
new_attr_dict['mail'] = [mail] + user['mail']
del user["mail"][0]
new_attr_dict["mail"] = [mail] + user["mail"]
if add_mailalias:
if not isinstance(add_mailalias, list):
add_mailalias = [add_mailalias]
for mail in add_mailalias:
try:
ldap.validate_uniqueness({'mail': mail})
ldap.validate_uniqueness({"mail": mail})
except Exception as e:
raise YunohostError('user_update_failed', user=username, error=e)
if mail[mail.find('@') + 1:] not in domains:
raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:])
user['mail'].append(mail)
new_attr_dict['mail'] = user['mail']
raise YunohostError("user_update_failed", user=username, error=e)
if mail[mail.find("@") + 1 :] not in domains:
raise YunohostError(
"mail_domain_unknown", domain=mail[mail.find("@") + 1 :]
)
user["mail"].append(mail)
new_attr_dict["mail"] = user["mail"]
if remove_mailalias:
if not isinstance(remove_mailalias, list):
remove_mailalias = [remove_mailalias]
for mail in remove_mailalias:
if len(user['mail']) > 1 and mail in user['mail'][1:]:
user['mail'].remove(mail)
if len(user["mail"]) > 1 and mail in user["mail"][1:]:
user["mail"].remove(mail)
else:
raise YunohostError('mail_alias_remove_failed', mail=mail)
new_attr_dict['mail'] = user['mail']
raise YunohostError("mail_alias_remove_failed", mail=mail)
new_attr_dict["mail"] = user["mail"]
if 'mail' in new_attr_dict:
env_dict["YNH_USER_MAILS"] = ','.join(new_attr_dict['mail'])
if "mail" in new_attr_dict:
env_dict["YNH_USER_MAILS"] = ",".join(new_attr_dict["mail"])
if add_mailforward:
if not isinstance(add_mailforward, list):
add_mailforward = [add_mailforward]
for mail in add_mailforward:
if mail in user['maildrop'][1:]:
if mail in user["maildrop"][1:]:
continue
user['maildrop'].append(mail)
new_attr_dict['maildrop'] = user['maildrop']
user["maildrop"].append(mail)
new_attr_dict["maildrop"] = user["maildrop"]
if remove_mailforward:
if not isinstance(remove_mailforward, list):
remove_mailforward = [remove_mailforward]
for mail in remove_mailforward:
if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]:
user['maildrop'].remove(mail)
if len(user["maildrop"]) > 1 and mail in user["maildrop"][1:]:
user["maildrop"].remove(mail)
else:
raise YunohostError('mail_forward_remove_failed', mail=mail)
new_attr_dict['maildrop'] = user['maildrop']
raise YunohostError("mail_forward_remove_failed", mail=mail)
new_attr_dict["maildrop"] = user["maildrop"]
if 'maildrop' in new_attr_dict:
env_dict["YNH_USER_MAILFORWARDS"] = ','.join(new_attr_dict['maildrop'])
if "maildrop" in new_attr_dict:
env_dict["YNH_USER_MAILFORWARDS"] = ",".join(new_attr_dict["maildrop"])
if mailbox_quota is not None:
new_attr_dict['mailuserquota'] = [mailbox_quota]
new_attr_dict["mailuserquota"] = [mailbox_quota]
env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota
operation_logger.start()
try:
ldap.update('uid=%s,ou=users' % username, new_attr_dict)
ldap.update("uid=%s,ou=users" % username, new_attr_dict)
except Exception as e:
raise YunohostError('user_update_failed', user=username, error=e)
raise YunohostError("user_update_failed", user=username, error=e)
# Trigger post_user_update hooks
hook_callback('post_user_update', env=env_dict)
hook_callback("post_user_update", env=env_dict)
logger.success(m18n.n('user_updated'))
logger.success(m18n.n("user_updated"))
app_ssowatconf()
return user_info(username)
@ -452,53 +488,51 @@ def user_info(username):
ldap = _get_ldap_interface()
user_attrs = [
'cn', 'mail', 'uid', 'maildrop', 'givenName', 'sn', 'mailuserquota'
]
user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"]
if len(username.split('@')) == 2:
filter = 'mail=' + username
if len(username.split("@")) == 2:
filter = "mail=" + username
else:
filter = 'uid=' + username
filter = "uid=" + username
result = ldap.search('ou=users,dc=yunohost,dc=org', filter, user_attrs)
result = ldap.search("ou=users,dc=yunohost,dc=org", filter, user_attrs)
if result:
user = result[0]
else:
raise YunohostError('user_unknown', user=username)
raise YunohostError("user_unknown", user=username)
result_dict = {
'username': user['uid'][0],
'fullname': user['cn'][0],
'firstname': user['givenName'][0],
'lastname': user['sn'][0],
'mail': user['mail'][0]
"username": user["uid"][0],
"fullname": user["cn"][0],
"firstname": user["givenName"][0],
"lastname": user["sn"][0],
"mail": user["mail"][0],
}
if len(user['mail']) > 1:
result_dict['mail-aliases'] = user['mail'][1:]
if len(user["mail"]) > 1:
result_dict["mail-aliases"] = user["mail"][1:]
if len(user['maildrop']) > 1:
result_dict['mail-forward'] = user['maildrop'][1:]
if len(user["maildrop"]) > 1:
result_dict["mail-forward"] = user["maildrop"][1:]
if 'mailuserquota' in user:
userquota = user['mailuserquota'][0]
if "mailuserquota" in user:
userquota = user["mailuserquota"][0]
if isinstance(userquota, int):
userquota = str(userquota)
# Test if userquota is '0' or '0M' ( quota pattern is ^(\d+[bkMGT])|0$ )
is_limited = not re.match('0[bkMGT]?', userquota)
storage_use = '?'
is_limited = not re.match("0[bkMGT]?", userquota)
storage_use = "?"
if service_status("dovecot")["status"] != "running":
logger.warning(m18n.n('mailbox_used_space_dovecot_down'))
logger.warning(m18n.n("mailbox_used_space_dovecot_down"))
elif username not in user_permission_info("mail.main")["corresponding_users"]:
logger.warning(m18n.n('mailbox_disabled', user=username))
logger.warning(m18n.n("mailbox_disabled", user=username))
else:
try:
cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0]
cmd = "doveadm -f flow quota get -u %s" % user["uid"][0]
cmd_result = check_output(cmd)
except Exception as e:
cmd_result = ""
@ -507,22 +541,22 @@ def user_info(username):
# Exemple of return value for cmd:
# """Quota name=User quota Type=STORAGE Value=0 Limit=- %=0
# Quota name=User quota Type=MESSAGE Value=0 Limit=- %=0"""
has_value = re.search(r'Value=(\d+)', cmd_result)
has_value = re.search(r"Value=(\d+)", cmd_result)
if has_value:
storage_use = int(has_value.group(1))
storage_use = _convertSize(storage_use)
if is_limited:
has_percent = re.search(r'%=(\d+)', cmd_result)
has_percent = re.search(r"%=(\d+)", cmd_result)
if has_percent:
percentage = int(has_percent.group(1))
storage_use += ' (%s%%)' % percentage
storage_use += " (%s%%)" % percentage
result_dict['mailbox-quota'] = {
'limit': userquota if is_limited else m18n.n('unlimit'),
'use': storage_use
result_dict["mailbox-quota"] = {
"limit": userquota if is_limited else m18n.n("unlimit"),
"use": storage_use,
}
return result_dict
@ -547,10 +581,13 @@ def user_group_list(short=False, full=False, include_primary_groups=True):
# Fetch relevant informations
from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract
ldap = _get_ldap_interface()
groups_infos = ldap.search('ou=groups,dc=yunohost,dc=org',
'(objectclass=groupOfNamesYnh)',
["cn", "member", "permission"])
groups_infos = ldap.search(
"ou=groups,dc=yunohost,dc=org",
"(objectclass=groupOfNamesYnh)",
["cn", "member", "permission"],
)
# Parse / organize information to be outputed
@ -565,19 +602,25 @@ def user_group_list(short=False, full=False, include_primary_groups=True):
groups[name] = {}
groups[name]["members"] = [_ldap_path_extract(p, "uid") for p in infos.get("member", [])]
groups[name]["members"] = [
_ldap_path_extract(p, "uid") for p in infos.get("member", [])
]
if full:
groups[name]["permissions"] = [_ldap_path_extract(p, "cn") for p in infos.get("permission", [])]
groups[name]["permissions"] = [
_ldap_path_extract(p, "cn") for p in infos.get("permission", [])
]
if short:
groups = list(groups.keys())
return {'groups': groups}
return {"groups": groups}
@is_unit_operation([('groupname', 'group')])
def user_group_create(operation_logger, groupname, gid=None, primary_group=False, sync_perm=True):
@is_unit_operation([("groupname", "group")])
def user_group_create(
operation_logger, groupname, gid=None, primary_group=False, sync_perm=True
):
"""
Create group
@ -591,20 +634,24 @@ def user_group_create(operation_logger, groupname, gid=None, primary_group=False
ldap = _get_ldap_interface()
# Validate uniqueness of groupname in LDAP
conflict = ldap.get_conflict({
'cn': groupname
}, base_dn='ou=groups,dc=yunohost,dc=org')
conflict = ldap.get_conflict(
{"cn": groupname}, base_dn="ou=groups,dc=yunohost,dc=org"
)
if conflict:
raise YunohostError('group_already_exist', group=groupname)
raise YunohostError("group_already_exist", group=groupname)
# Validate uniqueness of groupname in system group
all_existing_groupnames = {x.gr_name for x in grp.getgrall()}
if groupname in all_existing_groupnames:
if primary_group:
logger.warning(m18n.n('group_already_exist_on_system_but_removing_it', group=groupname))
subprocess.check_call("sed --in-place '/^%s:/d' /etc/group" % groupname, shell=True)
logger.warning(
m18n.n("group_already_exist_on_system_but_removing_it", group=groupname)
)
subprocess.check_call(
"sed --in-place '/^%s:/d' /etc/group" % groupname, shell=True
)
else:
raise YunohostError('group_already_exist_on_system', group=groupname)
raise YunohostError("group_already_exist_on_system", group=groupname)
if not gid:
# Get random GID
@ -616,9 +663,9 @@ def user_group_create(operation_logger, groupname, gid=None, primary_group=False
uid_guid_found = gid not in all_gid
attr_dict = {
'objectClass': ['top', 'groupOfNamesYnh', 'posixGroup'],
'cn': groupname,
'gidNumber': gid,
"objectClass": ["top", "groupOfNamesYnh", "posixGroup"],
"cn": groupname,
"gidNumber": gid,
}
# Here we handle the creation of a primary group
@ -629,22 +676,22 @@ def user_group_create(operation_logger, groupname, gid=None, primary_group=False
operation_logger.start()
try:
ldap.add('cn=%s,ou=groups' % groupname, attr_dict)
ldap.add("cn=%s,ou=groups" % groupname, attr_dict)
except Exception as e:
raise YunohostError('group_creation_failed', group=groupname, error=e)
raise YunohostError("group_creation_failed", group=groupname, error=e)
if sync_perm:
permission_sync_to_user()
if not primary_group:
logger.success(m18n.n('group_created', group=groupname))
logger.success(m18n.n("group_created", group=groupname))
else:
logger.debug(m18n.n('group_created', group=groupname))
logger.debug(m18n.n("group_created", group=groupname))
return {'name': groupname}
return {"name": groupname}
@is_unit_operation([('groupname', 'group')])
@is_unit_operation([("groupname", "group")])
def user_group_delete(operation_logger, groupname, force=False, sync_perm=True):
"""
Delete user
@ -656,37 +703,39 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True):
from yunohost.permission import permission_sync_to_user
from yunohost.utils.ldap import _get_ldap_interface
existing_groups = list(user_group_list()['groups'].keys())
existing_groups = list(user_group_list()["groups"].keys())
if groupname not in existing_groups:
raise YunohostError('group_unknown', group=groupname)
raise YunohostError("group_unknown", group=groupname)
# Refuse to delete primary groups of a user (e.g. group 'sam' related to user 'sam')
# without the force option...
#
# We also can't delete "all_users" because that's a special group...
existing_users = list(user_list()['users'].keys())
existing_users = list(user_list()["users"].keys())
undeletable_groups = existing_users + ["all_users", "visitors"]
if groupname in undeletable_groups and not force:
raise YunohostError('group_cannot_be_deleted', group=groupname)
raise YunohostError("group_cannot_be_deleted", group=groupname)
operation_logger.start()
ldap = _get_ldap_interface()
try:
ldap.remove('cn=%s,ou=groups' % groupname)
ldap.remove("cn=%s,ou=groups" % groupname)
except Exception as e:
raise YunohostError('group_deletion_failed', group=groupname, error=e)
raise YunohostError("group_deletion_failed", group=groupname, error=e)
if sync_perm:
permission_sync_to_user()
if groupname not in existing_users:
logger.success(m18n.n('group_deleted', group=groupname))
logger.success(m18n.n("group_deleted", group=groupname))
else:
logger.debug(m18n.n('group_deleted', group=groupname))
logger.debug(m18n.n("group_deleted", group=groupname))
@is_unit_operation([('groupname', 'group')])
def user_group_update(operation_logger, groupname, add=None, remove=None, force=False, sync_perm=True):
@is_unit_operation([("groupname", "group")])
def user_group_update(
operation_logger, groupname, add=None, remove=None, force=False, sync_perm=True
):
"""
Update user informations
@ -700,18 +749,18 @@ def user_group_update(operation_logger, groupname, add=None, remove=None, force=
from yunohost.permission import permission_sync_to_user
from yunohost.utils.ldap import _get_ldap_interface
existing_users = list(user_list()['users'].keys())
existing_users = list(user_list()["users"].keys())
# Refuse to edit a primary group of a user (e.g. group 'sam' related to user 'sam')
# Those kind of group should only ever contain the user (e.g. sam) and only this one.
# We also can't edit "all_users" without the force option because that's a special group...
if not force:
if groupname == "all_users":
raise YunohostError('group_cannot_edit_all_users')
raise YunohostError("group_cannot_edit_all_users")
elif groupname == "visitors":
raise YunohostError('group_cannot_edit_visitors')
raise YunohostError("group_cannot_edit_visitors")
elif groupname in existing_users:
raise YunohostError('group_cannot_edit_primary_group', group=groupname)
raise YunohostError("group_cannot_edit_primary_group", group=groupname)
# We extract the uid for each member of the group to keep a simple flat list of members
current_group = user_group_info(groupname)["members"]
@ -722,12 +771,14 @@ def user_group_update(operation_logger, groupname, add=None, remove=None, force=
for user in users_to_add:
if user not in existing_users:
raise YunohostError('user_unknown', user=user)
raise YunohostError("user_unknown", user=user)
if user in current_group:
logger.warning(m18n.n('group_user_already_in_group', user=user, group=groupname))
logger.warning(
m18n.n("group_user_already_in_group", user=user, group=groupname)
)
else:
operation_logger.related_to.append(('user', user))
operation_logger.related_to.append(("user", user))
new_group += users_to_add
@ -736,28 +787,35 @@ def user_group_update(operation_logger, groupname, add=None, remove=None, force=
for user in users_to_remove:
if user not in current_group:
logger.warning(m18n.n('group_user_not_in_group', user=user, group=groupname))
logger.warning(
m18n.n("group_user_not_in_group", user=user, group=groupname)
)
else:
operation_logger.related_to.append(('user', user))
operation_logger.related_to.append(("user", user))
# Remove users_to_remove from new_group
# Kinda like a new_group -= users_to_remove
new_group = [u for u in new_group if u not in users_to_remove]
new_group_dns = ["uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group]
new_group_dns = [
"uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group
]
if set(new_group) != set(current_group):
operation_logger.start()
ldap = _get_ldap_interface()
try:
ldap.update('cn=%s,ou=groups' % groupname, {"member": set(new_group_dns), "memberUid": set(new_group)})
ldap.update(
"cn=%s,ou=groups" % groupname,
{"member": set(new_group_dns), "memberUid": set(new_group)},
)
except Exception as e:
raise YunohostError('group_update_failed', group=groupname, error=e)
raise YunohostError("group_update_failed", group=groupname, error=e)
if groupname != "all_users":
logger.success(m18n.n('group_updated', group=groupname))
logger.success(m18n.n("group_updated", group=groupname))
else:
logger.debug(m18n.n('group_updated', group=groupname))
logger.debug(m18n.n("group_updated", group=groupname))
if sync_perm:
permission_sync_to_user()
@ -774,23 +832,28 @@ def user_group_info(groupname):
"""
from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract
ldap = _get_ldap_interface()
# Fetch info for this group
result = ldap.search('ou=groups,dc=yunohost,dc=org',
"cn=" + groupname,
["cn", "member", "permission"])
result = ldap.search(
"ou=groups,dc=yunohost,dc=org",
"cn=" + groupname,
["cn", "member", "permission"],
)
if not result:
raise YunohostError('group_unknown', group=groupname)
raise YunohostError("group_unknown", group=groupname)
infos = result[0]
# Format data
return {
'members': [_ldap_path_extract(p, "uid") for p in infos.get("member", [])],
'permissions': [_ldap_path_extract(p, "cn") for p in infos.get("permission", [])]
"members": [_ldap_path_extract(p, "uid") for p in infos.get("member", [])],
"permissions": [
_ldap_path_extract(p, "cn") for p in infos.get("permission", [])
],
}
@ -798,27 +861,37 @@ def user_group_info(groupname):
# Permission subcategory
#
def user_permission_list(short=False, full=False):
import yunohost.permission
return yunohost.permission.user_permission_list(short, full, absolute_urls=True)
def user_permission_update(permission, add=None, remove=None, label=None, show_tile=None, sync_perm=True):
def user_permission_update(
permission, add=None, remove=None, label=None, show_tile=None, sync_perm=True
):
import yunohost.permission
return yunohost.permission.user_permission_update(permission,
add=add, remove=remove,
label=label, show_tile=show_tile,
sync_perm=sync_perm)
return yunohost.permission.user_permission_update(
permission,
add=add,
remove=remove,
label=label,
show_tile=show_tile,
sync_perm=sync_perm,
)
def user_permission_reset(permission, sync_perm=True):
import yunohost.permission
return yunohost.permission.user_permission_reset(permission,
sync_perm=sync_perm)
return yunohost.permission.user_permission_reset(permission, sync_perm=sync_perm)
def user_permission_info(permission):
import yunohost.permission
return yunohost.permission.user_permission_info(permission)
@ -847,17 +920,18 @@ def user_ssh_add_key(username, key, comment):
def user_ssh_remove_key(username, key):
return yunohost.ssh.user_ssh_remove_key(username, key)
#
# End SSH subcategory
#
def _convertSize(num, suffix=''):
for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']:
def _convertSize(num, suffix=""):
for unit in ["K", "M", "G", "T", "P", "E", "Z"]:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)
return "%.1f%s%s" % (num, "Yi", suffix)
def _hash_user_password(password):
@ -883,7 +957,7 @@ def _hash_user_password(password):
"""
char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./"
salt = ''.join([random.SystemRandom().choice(char_set) for x in range(16)])
salt = "".join([random.SystemRandom().choice(char_set) for x in range(16)])
salt = '$6$' + salt + '$'
return '{CRYPT}' + crypt.crypt(str(password), salt)
salt = "$6$" + salt + "$"
return "{CRYPT}" + crypt.crypt(str(password), salt)

View file

@ -48,7 +48,4 @@ class YunohostError(MoulinetteError):
if not self.log_ref:
return super(YunohostError, self).content()
else:
return {
"error": self.strerror,
"log_ref": self.log_ref
}
return {"error": self.strerror, "log_ref": self.log_ref}

View file

@ -36,18 +36,23 @@ def _get_ldap_interface():
if _ldap_interface is None:
conf = {"vendor": "ldap",
"name": "as-root",
"parameters": {'uri': 'ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi',
'base_dn': 'dc=yunohost,dc=org',
'user_rdn': 'gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth'},
"extra": {}
}
conf = {
"vendor": "ldap",
"name": "as-root",
"parameters": {
"uri": "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi",
"base_dn": "dc=yunohost,dc=org",
"user_rdn": "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth",
},
"extra": {},
}
try:
_ldap_interface = ldap.Authenticator(**conf)
except MoulinetteLdapIsDownError:
raise YunohostError("Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'")
raise YunohostError(
"Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'"
)
assert_slapd_is_running()
@ -58,7 +63,9 @@ def assert_slapd_is_running():
# Assert slapd is running...
if not os.system("pgrep slapd >/dev/null") == 0:
raise YunohostError("Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'")
raise YunohostError(
"Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'"
)
# We regularly want to extract stuff like 'bar' in ldap path like
@ -68,10 +75,11 @@ def assert_slapd_is_running():
# e.g. using _ldap_path_extract(path, "foo") on the previous example will
# return bar
def _ldap_path_extract(path, info):
for element in path.split(","):
if element.startswith(info + "="):
return element[len(info + "="):]
return element[len(info + "=") :]
# Add this to properly close / delete the ldap interface / authenticator

View file

@ -5,18 +5,27 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import write_to_json, read_yaml
from yunohost.user import user_list, user_group_create, user_group_update
from yunohost.app import app_setting, _installed_apps, _get_app_settings, _set_app_settings
from yunohost.permission import permission_create, user_permission_list, user_permission_update, permission_sync_to_user
from yunohost.app import (
app_setting,
_installed_apps,
_get_app_settings,
_set_app_settings,
)
from yunohost.permission import (
permission_create,
user_permission_update,
permission_sync_to_user,
)
logger = getActionLogger('yunohost.legacy')
logger = getActionLogger("yunohost.legacy")
class SetupGroupPermissions():
class SetupGroupPermissions:
@staticmethod
def remove_if_exists(target):
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
try:
@ -34,7 +43,9 @@ class SetupGroupPermissions():
try:
ldap.remove(dn)
except Exception as e:
raise YunohostError("migration_0011_failed_to_remove_stale_object", dn=dn, error=e)
raise YunohostError(
"migration_0011_failed_to_remove_stale_object", dn=dn, error=e
)
@staticmethod
def migrate_LDAP_db():
@ -42,27 +53,30 @@ class SetupGroupPermissions():
logger.info(m18n.n("migration_0011_update_LDAP_database"))
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
ldap_map = read_yaml('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml')
ldap_map = read_yaml(
"/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml"
)
try:
SetupGroupPermissions.remove_if_exists("ou=permission")
SetupGroupPermissions.remove_if_exists('ou=groups')
SetupGroupPermissions.remove_if_exists("ou=groups")
attr_dict = ldap_map['parents']['ou=permission']
ldap.add('ou=permission', attr_dict)
attr_dict = ldap_map["parents"]["ou=permission"]
ldap.add("ou=permission", attr_dict)
attr_dict = ldap_map['parents']['ou=groups']
ldap.add('ou=groups', attr_dict)
attr_dict = ldap_map["parents"]["ou=groups"]
ldap.add("ou=groups", attr_dict)
attr_dict = ldap_map['children']['cn=all_users,ou=groups']
ldap.add('cn=all_users,ou=groups', attr_dict)
attr_dict = ldap_map["children"]["cn=all_users,ou=groups"]
ldap.add("cn=all_users,ou=groups", attr_dict)
attr_dict = ldap_map['children']['cn=visitors,ou=groups']
ldap.add('cn=visitors,ou=groups', attr_dict)
attr_dict = ldap_map["children"]["cn=visitors,ou=groups"]
ldap.add("cn=visitors,ou=groups", attr_dict)
for rdn, attr_dict in ldap_map['depends_children'].items():
for rdn, attr_dict in ldap_map["depends_children"].items():
ldap.add(rdn, attr_dict)
except Exception as e:
raise YunohostError("migration_0011_LDAP_update_failed", error=e)
@ -70,15 +84,33 @@ class SetupGroupPermissions():
logger.info(m18n.n("migration_0011_create_group"))
# Create a group for each yunohost user
user_list = ldap.search('ou=users,dc=yunohost,dc=org',
'(&(objectclass=person)(!(uid=root))(!(uid=nobody)))',
['uid', 'uidNumber'])
user_list = ldap.search(
"ou=users,dc=yunohost,dc=org",
"(&(objectclass=person)(!(uid=root))(!(uid=nobody)))",
["uid", "uidNumber"],
)
for user_info in user_list:
username = user_info['uid'][0]
ldap.update('uid=%s,ou=users' % username,
{'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh']})
user_group_create(username, gid=user_info['uidNumber'][0], primary_group=True, sync_perm=False)
user_group_update(groupname='all_users', add=username, force=True, sync_perm=False)
username = user_info["uid"][0]
ldap.update(
"uid=%s,ou=users" % username,
{
"objectClass": [
"mailAccount",
"inetOrgPerson",
"posixAccount",
"userPermissionYnh",
]
},
)
user_group_create(
username,
gid=user_info["uidNumber"][0],
primary_group=True,
sync_perm=False,
)
user_group_update(
groupname="all_users", add=username, force=True, sync_perm=False
)
@staticmethod
def migrate_app_permission(app=None):
@ -88,64 +120,99 @@ class SetupGroupPermissions():
if app:
if app not in apps:
logger.error("Can't migrate permission for app %s because it ain't installed..." % app)
logger.error(
"Can't migrate permission for app %s because it ain't installed..."
% app
)
apps = []
else:
apps = [app]
for app in apps:
permission = app_setting(app, 'allowed_users')
path = app_setting(app, 'path')
domain = app_setting(app, 'domain')
permission = app_setting(app, "allowed_users")
path = app_setting(app, "path")
domain = app_setting(app, "domain")
url = "/" if domain and path else None
if permission:
known_users = list(user_list()["users"].keys())
allowed = [user for user in permission.split(',') if user in known_users]
allowed = [
user for user in permission.split(",") if user in known_users
]
else:
allowed = ["all_users"]
permission_create(app + ".main", url=url, allowed=allowed, show_tile=True, protected=False, sync_perm=False)
permission_create(
app + ".main",
url=url,
allowed=allowed,
show_tile=True,
protected=False,
sync_perm=False,
)
app_setting(app, 'allowed_users', delete=True)
app_setting(app, "allowed_users", delete=True)
# Migrate classic public app still using the legacy unprotected_uris
if app_setting(app, "unprotected_uris") == "/" or app_setting(app, "skipped_uris") == "/":
if (
app_setting(app, "unprotected_uris") == "/"
or app_setting(app, "skipped_uris") == "/"
):
user_permission_update(app + ".main", add="visitors", sync_perm=False)
permission_sync_to_user()
LEGACY_PERMISSION_LABEL = {
("nextcloud", "skipped"): "api", # .well-known
("libreto", "skipped"): "pad access", # /[^/]+
("leed", "skipped"): "api", # /action.php, for cron task ...
("mailman", "protected"): "admin", # /admin
("prettynoemiecms", "protected"): "admin", # /admin
("etherpad_mypads", "skipped"): "admin", # /admin
("baikal", "protected"): "admin", # /admin/
("couchpotato", "unprotected"): "api", # /api
("freshrss", "skipped"): "api", # /api/,
("portainer", "skipped"): "api", # /api/webhooks/
("jeedom", "unprotected"): "api", # /core/api/jeeApi.php
("bozon", "protected"): "user interface", # /index.php
("limesurvey", "protected"): "admin", # /index.php?r=admin,/index.php?r=plugins,/scripts
("kanboard", "unprotected"): "api", # /jsonrpc.php
("seafile", "unprotected"): "medias", # /media
("ttrss", "skipped"): "api", # /public.php,/api,/opml.php?op=publish
("libreerp", "protected"): "admin", # /web/database/manager
("z-push", "skipped"): "api", # $domain/[Aa]uto[Dd]iscover/.*
("radicale", "skipped"): "?", # $domain$path_url
("jirafeau", "protected"): "user interface", # $domain$path_url/$","$domain$path_url/admin.php.*$
("opensondage", "protected"): "admin", # $domain$path_url/admin/
("lstu", "protected"): "user interface", # $domain$path_url/login$","$domain$path_url/logout$","$domain$path_url/api$","$domain$path_url/extensions$","$domain$path_url/stats$","$domain$path_url/d/.*$","$domain$path_url/a$","$domain$path_url/$
("lutim", "protected"): "user interface", # $domain$path_url/stats/?$","$domain$path_url/manifest.webapp/?$","$domain$path_url/?$","$domain$path_url/[d-m]/.*$
("lufi", "protected"): "user interface", # $domain$path_url/stats$","$domain$path_url/manifest.webapp$","$domain$path_url/$","$domain$path_url/d/.*$","$domain$path_url/m/.*$
("gogs", "skipped"): "api", # $excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-receive%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-upload%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/info/refs
("nextcloud", "skipped"): "api", # .well-known
("libreto", "skipped"): "pad access", # /[^/]+
("leed", "skipped"): "api", # /action.php, for cron task ...
("mailman", "protected"): "admin", # /admin
("prettynoemiecms", "protected"): "admin", # /admin
("etherpad_mypads", "skipped"): "admin", # /admin
("baikal", "protected"): "admin", # /admin/
("couchpotato", "unprotected"): "api", # /api
("freshrss", "skipped"): "api", # /api/,
("portainer", "skipped"): "api", # /api/webhooks/
("jeedom", "unprotected"): "api", # /core/api/jeeApi.php
("bozon", "protected"): "user interface", # /index.php
(
"limesurvey",
"protected",
): "admin", # /index.php?r=admin,/index.php?r=plugins,/scripts
("kanboard", "unprotected"): "api", # /jsonrpc.php
("seafile", "unprotected"): "medias", # /media
("ttrss", "skipped"): "api", # /public.php,/api,/opml.php?op=publish
("libreerp", "protected"): "admin", # /web/database/manager
("z-push", "skipped"): "api", # $domain/[Aa]uto[Dd]iscover/.*
("radicale", "skipped"): "?", # $domain$path_url
(
"jirafeau",
"protected",
): "user interface", # $domain$path_url/$","$domain$path_url/admin.php.*$
("opensondage", "protected"): "admin", # $domain$path_url/admin/
(
"lstu",
"protected",
): "user interface", # $domain$path_url/login$","$domain$path_url/logout$","$domain$path_url/api$","$domain$path_url/extensions$","$domain$path_url/stats$","$domain$path_url/d/.*$","$domain$path_url/a$","$domain$path_url/$
(
"lutim",
"protected",
): "user interface", # $domain$path_url/stats/?$","$domain$path_url/manifest.webapp/?$","$domain$path_url/?$","$domain$path_url/[d-m]/.*$
(
"lufi",
"protected",
): "user interface", # $domain$path_url/stats$","$domain$path_url/manifest.webapp$","$domain$path_url/$","$domain$path_url/d/.*$","$domain$path_url/m/.*$
(
"gogs",
"skipped",
): "api", # $excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-receive%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-upload%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/info/refs
}
def legacy_permission_label(app, permission_type):
return LEGACY_PERMISSION_LABEL.get((app, permission_type), "Legacy %s urls" % permission_type)
return LEGACY_PERMISSION_LABEL.get(
(app, permission_type), "Legacy %s urls" % permission_type
)
def migrate_legacy_permission_settings(app=None):
@ -155,7 +222,10 @@ def migrate_legacy_permission_settings(app=None):
if app:
if app not in apps:
logger.error("Can't migrate permission for app %s because it ain't installed..." % app)
logger.error(
"Can't migrate permission for app %s because it ain't installed..."
% app
)
apps = []
else:
apps = [app]
@ -164,33 +234,55 @@ def migrate_legacy_permission_settings(app=None):
settings = _get_app_settings(app) or {}
if settings.get("label"):
user_permission_update(app + ".main", label=settings["label"], sync_perm=False)
user_permission_update(
app + ".main", label=settings["label"], sync_perm=False
)
del settings["label"]
def _setting(name):
s = settings.get(name)
return s.split(',') if s else []
return s.split(",") if s else []
skipped_urls = [uri for uri in _setting('skipped_uris') if uri != '/']
skipped_urls += ['re:' + regex for regex in _setting('skipped_regex')]
unprotected_urls = [uri for uri in _setting('unprotected_uris') if uri != '/']
unprotected_urls += ['re:' + regex for regex in _setting('unprotected_regex')]
protected_urls = [uri for uri in _setting('protected_uris') if uri != '/']
protected_urls += ['re:' + regex for regex in _setting('protected_regex')]
skipped_urls = [uri for uri in _setting("skipped_uris") if uri != "/"]
skipped_urls += ["re:" + regex for regex in _setting("skipped_regex")]
unprotected_urls = [uri for uri in _setting("unprotected_uris") if uri != "/"]
unprotected_urls += ["re:" + regex for regex in _setting("unprotected_regex")]
protected_urls = [uri for uri in _setting("protected_uris") if uri != "/"]
protected_urls += ["re:" + regex for regex in _setting("protected_regex")]
if skipped_urls != []:
permission_create(app + ".legacy_skipped_uris", additional_urls=skipped_urls,
auth_header=False, label=legacy_permission_label(app, "skipped"),
show_tile=False, allowed='visitors', protected=True, sync_perm=False)
permission_create(
app + ".legacy_skipped_uris",
additional_urls=skipped_urls,
auth_header=False,
label=legacy_permission_label(app, "skipped"),
show_tile=False,
allowed="visitors",
protected=True,
sync_perm=False,
)
if unprotected_urls != []:
permission_create(app + ".legacy_unprotected_uris", additional_urls=unprotected_urls,
auth_header=True, label=legacy_permission_label(app, "unprotected"),
show_tile=False, allowed='visitors', protected=True, sync_perm=False)
permission_create(
app + ".legacy_unprotected_uris",
additional_urls=unprotected_urls,
auth_header=True,
label=legacy_permission_label(app, "unprotected"),
show_tile=False,
allowed="visitors",
protected=True,
sync_perm=False,
)
if protected_urls != []:
permission_create(app + ".legacy_protected_uris", additional_urls=protected_urls,
auth_header=True, label=legacy_permission_label(app, "protected"),
show_tile=False, allowed=user_permission_list()['permissions'][app + ".main"]['allowed'],
protected=True, sync_perm=False)
permission_create(
app + ".legacy_protected_uris",
additional_urls=protected_urls,
auth_header=True,
label=legacy_permission_label(app, "protected"),
show_tile=False,
allowed=[],
protected=True,
sync_perm=False,
)
legacy_permission_settings = [
"skipped_uris",
@ -198,7 +290,7 @@ def migrate_legacy_permission_settings(app=None):
"protected_uris",
"skipped_regex",
"unprotected_regex",
"protected_regex"
"protected_regex",
]
for key in legacy_permission_settings:
if key in settings:
@ -215,6 +307,9 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent():
if not os.path.exists(persistent_file_name):
return
# Ugly hack because for some reason so many people have tabs in their conf.json.persistent ...
os.system(r"sed -i 's/\t/ /g' /etc/ssowat/conf.json.persistent")
# Ugly hack to try not to misarably fail migration
persistent = read_yaml(persistent_file_name)
@ -224,7 +319,7 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent():
"protected_urls",
"skipped_regex",
"unprotected_regex",
"protected_regex"
"protected_regex",
]
if not any(legacy_rule in persistent for legacy_rule in legacy_rules):
@ -233,9 +328,15 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent():
if not isinstance(persistent.get("permissions"), dict):
persistent["permissions"] = {}
skipped_urls = persistent.get("skipped_urls", []) + ["re:" + r for r in persistent.get("skipped_regex", [])]
protected_urls = persistent.get("protected_urls", []) + ["re:" + r for r in persistent.get("protected_regex", [])]
unprotected_urls = persistent.get("unprotected_urls", []) + ["re:" + r for r in persistent.get("unprotected_regex", [])]
skipped_urls = persistent.get("skipped_urls", []) + [
"re:" + r for r in persistent.get("skipped_regex", [])
]
protected_urls = persistent.get("protected_urls", []) + [
"re:" + r for r in persistent.get("protected_regex", [])
]
unprotected_urls = persistent.get("unprotected_urls", []) + [
"re:" + r for r in persistent.get("unprotected_regex", [])
]
known_users = list(user_list()["users"].keys())
@ -244,35 +345,40 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent():
del persistent[legacy_rule]
if skipped_urls:
persistent["permissions"]['custom_skipped'] = {
persistent["permissions"]["custom_skipped"] = {
"users": [],
"label": "Custom permissions - skipped",
"show_tile": False,
"auth_header": False,
"public": True,
"uris": skipped_urls + persistent["permissions"].get("custom_skipped", {}).get("uris", []),
"uris": skipped_urls
+ persistent["permissions"].get("custom_skipped", {}).get("uris", []),
}
if unprotected_urls:
persistent["permissions"]['custom_unprotected'] = {
persistent["permissions"]["custom_unprotected"] = {
"users": [],
"label": "Custom permissions - unprotected",
"show_tile": False,
"auth_header": True,
"public": True,
"uris": unprotected_urls + persistent["permissions"].get("custom_unprotected", {}).get("uris", []),
"uris": unprotected_urls
+ persistent["permissions"].get("custom_unprotected", {}).get("uris", []),
}
if protected_urls:
persistent["permissions"]['custom_protected'] = {
persistent["permissions"]["custom_protected"] = {
"users": known_users,
"label": "Custom permissions - protected",
"show_tile": False,
"auth_header": True,
"public": False,
"uris": protected_urls + persistent["permissions"].get("custom_protected", {}).get("uris", []),
"uris": protected_urls
+ persistent["permissions"].get("custom_protected", {}).get("uris", []),
}
write_to_json(persistent_file_name, persistent, sort_keys=True, indent=4)
logger.warning("Yunohost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system")
logger.warning(
"Yunohost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system"
)

View file

@ -28,16 +28,21 @@ from moulinette.utils.filesystem import read_file, write_to_file
from moulinette.utils.network import download_text
from moulinette.utils.process import check_output
logger = logging.getLogger('yunohost.utils.network')
logger = logging.getLogger("yunohost.utils.network")
def get_public_ip(protocol=4):
assert protocol in [4, 6], "Invalid protocol version for get_public_ip: %s, expected 4 or 6" % protocol
assert protocol in [4, 6], (
"Invalid protocol version for get_public_ip: %s, expected 4 or 6" % protocol
)
cache_file = "/var/cache/yunohost/ipv%s" % protocol
cache_duration = 120 # 2 min
if os.path.exists(cache_file) and abs(os.path.getctime(cache_file) - time.time()) < cache_duration:
if (
os.path.exists(cache_file)
and abs(os.path.getctime(cache_file) - time.time()) < cache_duration
):
ip = read_file(cache_file).strip()
ip = ip if ip else None # Empty file (empty string) means there's no IP
logger.debug("Reusing IPv%s from cache: %s" % (protocol, ip))
@ -53,7 +58,9 @@ def get_public_ip_from_remote_server(protocol=4):
# We can know that ipv6 is not available directly if this file does not exists
if protocol == 6 and not os.path.exists("/proc/net/if_inet6"):
logger.debug("IPv6 appears not at all available on the system, so assuming there's no IP address for that version")
logger.debug(
"IPv6 appears not at all available on the system, so assuming there's no IP address for that version"
)
return None
# If we are indeed connected in ipv4 or ipv6, we should find a default route
@ -64,12 +71,18 @@ def get_public_ip_from_remote_server(protocol=4):
# But of course IPv6 is more complex ... e.g. on internet cube there's
# no default route but a /3 which acts as a default-like route...
# e.g. 2000:/3 dev tun0 ...
return r.startswith("default") or (":" in r and re.match(r".*/[0-3]$", r.split()[0]))
return r.startswith("default") or (
":" in r and re.match(r".*/[0-3]$", r.split()[0])
)
if not any(is_default_route(r) for r in routes):
logger.debug("No default route for IPv%s, so assuming there's no IP address for that version" % protocol)
logger.debug(
"No default route for IPv%s, so assuming there's no IP address for that version"
% protocol
)
return None
url = 'https://ip%s.yunohost.org' % (protocol if protocol != 4 else '')
url = "https://ip%s.yunohost.org" % (protocol if protocol != 4 else "")
logger.debug("Fetching IP from %s " % url)
try:
@ -83,23 +96,27 @@ def get_network_interfaces():
# Get network devices and their addresses (raw infos from 'ip addr')
devices_raw = {}
output = check_output('ip addr show')
for d in re.split(r'^(?:[0-9]+: )', output, flags=re.MULTILINE):
output = check_output("ip addr show")
for d in re.split(r"^(?:[0-9]+: )", output, flags=re.MULTILINE):
# Extract device name (1) and its addresses (2)
m = re.match(r'([^\s@]+)(?:@[\S]+)?: (.*)', d, flags=re.DOTALL)
m = re.match(r"([^\s@]+)(?:@[\S]+)?: (.*)", d, flags=re.DOTALL)
if m:
devices_raw[m.group(1)] = m.group(2)
# Parse relevant informations for each of them
devices = {name: _extract_inet(addrs) for name, addrs in devices_raw.items() if name != "lo"}
devices = {
name: _extract_inet(addrs)
for name, addrs in devices_raw.items()
if name != "lo"
}
return devices
def get_gateway():
output = check_output('ip route show')
m = re.search(r'default via (.*) dev ([a-z]+[0-9]?)', output)
output = check_output("ip route show")
m = re.search(r"default via (.*) dev ([a-z]+[0-9]?)", output)
if not m:
return None
@ -118,7 +135,9 @@ def external_resolvers():
if not external_resolvers_:
resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n")
external_resolvers_ = [r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver")]
external_resolvers_ = [
r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver")
]
# We keep only ipv4 resolvers, otherwise on IPv4-only instances, IPv6
# will be tried anyway resulting in super-slow dig requests that'll wait
# until timeout...
@ -127,7 +146,9 @@ def external_resolvers():
return external_resolvers_
def dig(qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False):
def dig(
qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False
):
"""
Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf
"""
@ -151,10 +172,12 @@ def dig(qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_an
resolver.timeout = timeout
try:
answers = resolver.query(qname, rdtype)
except (dns.resolver.NXDOMAIN,
dns.resolver.NoNameservers,
dns.resolver.NoAnswer,
dns.exception.Timeout) as e:
except (
dns.resolver.NXDOMAIN,
dns.resolver.NoNameservers,
dns.resolver.NoAnswer,
dns.exception.Timeout,
) as e:
return ("nok", (e.__class__.__name__, e))
if not full_answers:
@ -178,28 +201,30 @@ def _extract_inet(string, skip_netmask=False, skip_loopback=True):
A dict of {protocol: address} with protocol one of 'ipv4' or 'ipv6'
"""
ip4_pattern = r'((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
ip6_pattern = r'(((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)'
ip4_pattern += r'/[0-9]{1,2})' if not skip_netmask else ')'
ip6_pattern += r'/[0-9]{1,3})' if not skip_netmask else ')'
ip4_pattern = (
r"((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}"
)
ip6_pattern = r"(((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)"
ip4_pattern += r"/[0-9]{1,2})" if not skip_netmask else ")"
ip6_pattern += r"/[0-9]{1,3})" if not skip_netmask else ")"
result = {}
for m in re.finditer(ip4_pattern, string):
addr = m.group(1)
if skip_loopback and addr.startswith('127.'):
if skip_loopback and addr.startswith("127."):
continue
# Limit to only one result
result['ipv4'] = addr
result["ipv4"] = addr
break
for m in re.finditer(ip6_pattern, string):
addr = m.group(1)
if skip_loopback and addr == '::1':
if skip_loopback and addr == "::1":
continue
# Limit to only one result
result['ipv6'] = addr
result["ipv6"] = addr
break
return result

View file

@ -25,9 +25,9 @@ import logging
from moulinette.utils.process import check_output
from packaging import version
logger = logging.getLogger('yunohost.utils.packages')
logger = logging.getLogger("yunohost.utils.packages")
YUNOHOST_PACKAGES = ['yunohost', 'yunohost-admin', 'moulinette', 'ssowat']
YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat"]
def get_ynh_package_version(package):
@ -45,8 +45,7 @@ def get_ynh_package_version(package):
return {"version": "?", "repo": "?"}
out = check_output(cmd).split()
# Output looks like : "yunohost (1.2.3) testing; urgency=medium"
return {"version": out[1].strip("()"),
"repo": out[2].strip(";")}
return {"version": out[1].strip("()"), "repo": out[2].strip(";")}
def meets_version_specifier(pkg_name, specifier):
@ -63,11 +62,11 @@ def meets_version_specifier(pkg_name, specifier):
# context
assert pkg_name in YUNOHOST_PACKAGES
pkg_version = get_ynh_package_version(pkg_name)["version"]
pkg_version = re.split(r'\~|\+|\-', pkg_version)[0]
pkg_version = re.split(r"\~|\+|\-", pkg_version)[0]
pkg_version = version.parse(pkg_version)
# Extract operator and version specifier
op, req_version = re.search(r'(<<|<=|=|>=|>>) *([\d\.]+)', specifier).groups()
op, req_version = re.search(r"(<<|<=|=|>=|>>) *([\d\.]+)", specifier).groups()
req_version = version.parse(req_version)
# Python2 had a builtin that returns (-1, 0, 1) depending on comparison
@ -80,7 +79,7 @@ def meets_version_specifier(pkg_name, specifier):
"<=": lambda v1, v2: cmp(v1, v2) in [-1, 0],
"=": lambda v1, v2: cmp(v1, v2) in [0],
">=": lambda v1, v2: cmp(v1, v2) in [0, 1],
">>": lambda v1, v2: cmp(v1, v2) in [1]
">>": lambda v1, v2: cmp(v1, v2) in [1],
}
return deb_operators[op](pkg_version, req_version)
@ -92,6 +91,7 @@ def ynh_packages_version(*args, **kwargs):
# they don't seem to serve any purpose
"""Return the version of each YunoHost package"""
from collections import OrderedDict
packages = OrderedDict()
for package in YUNOHOST_PACKAGES:
packages[package] = get_ynh_package_version(package)
@ -106,8 +106,7 @@ def dpkg_is_broken():
# ref: https://sources.debian.org/src/apt/1.4.9/apt-pkg/deb/debsystem.cc/#L141-L174
if not os.path.isdir("/var/lib/dpkg/updates/"):
return False
return any(re.match("^[0-9]+$", f)
for f in os.listdir("/var/lib/dpkg/updates/"))
return any(re.match("^[0-9]+$", f) for f in os.listdir("/var/lib/dpkg/updates/"))
def dpkg_lock_available():
@ -121,7 +120,9 @@ def _list_upgradable_apt_packages():
upgradable_raw = check_output("LC_ALL=C apt list --upgradable")
# Dirty parsing of the output
upgradable_raw = [l.strip() for l in upgradable_raw.split("\n") if l.strip()]
upgradable_raw = [
line.strip() for line in upgradable_raw.split("\n") if line.strip()
]
for line in upgradable_raw:
# Remove stupid warning and verbose messages >.>
@ -132,7 +133,7 @@ def _list_upgradable_apt_packages():
# yunohost/stable 3.5.0.2+201903211853 all [upgradable from: 3.4.2.4+201903080053]
line = line.split()
if len(line) != 6:
logger.warning("Failed to parse this line : %s" % ' '.join(line))
logger.warning("Failed to parse this line : %s" % " ".join(line))
continue
yield {

View file

@ -25,10 +25,18 @@ import json
import string
import subprocess
SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin",
"root", "test", "rpi"]
SMALL_PWD_LIST = [
"yunohost",
"olinuxino",
"olinux",
"raspberry",
"admin",
"root",
"test",
"rpi",
]
MOST_USED_PASSWORDS = '/usr/share/yunohost/other/password/100000-most-used.txt'
MOST_USED_PASSWORDS = "/usr/share/yunohost/other/password/100000-most-used.txt"
# Length, digits, lowers, uppers, others
STRENGTH_LEVELS = [
@ -44,7 +52,6 @@ def assert_password_is_strong_enough(profile, password):
class PasswordValidator(object):
def __init__(self, profile):
"""
Initialize a password validator.
@ -60,7 +67,7 @@ class PasswordValidator(object):
# from settings.py because this file is also meant to be
# use as a script by ssowat.
# (or at least that's my understanding -- Alex)
settings = json.load(open('/etc/yunohost/settings.json', "r"))
settings = json.load(open("/etc/yunohost/settings.json", "r"))
setting_key = "security.password." + profile + ".strength"
self.validation_strength = int(settings[setting_key]["value"])
except Exception:
@ -173,20 +180,21 @@ class PasswordValidator(object):
# stdin to avoid it being shown in ps -ef --forest...
command = "grep -q -F -f - %s" % MOST_USED_PASSWORDS
p = subprocess.Popen(command.split(), stdin=subprocess.PIPE)
p.communicate(input=password.encode('utf-8'))
p.communicate(input=password.encode("utf-8"))
return not bool(p.returncode)
# This file is also meant to be used as an executable by
# SSOwat to validate password from the portal when an user
# change its password.
if __name__ == '__main__':
if __name__ == "__main__":
if len(sys.argv) < 2:
import getpass
pwd = getpass.getpass("")
# print("usage: password.py PASSWORD")
else:
pwd = sys.argv[1]
status, msg = PasswordValidator('user').validation_summary(pwd)
status, msg = PasswordValidator("user").validation_summary(pwd)
print(msg)
sys.exit(0)

View file

@ -8,7 +8,7 @@ from yunohost.domain import _get_maindomain, domain_list
from yunohost.utils.network import get_public_ip
from yunohost.utils.error import YunohostError
logger = logging.getLogger('yunohost.utils.yunopaste')
logger = logging.getLogger("yunohost.utils.yunopaste")
def yunopaste(data):
@ -18,28 +18,41 @@ def yunopaste(data):
try:
data = anonymize(data)
except Exception as e:
logger.warning("For some reason, YunoHost was not able to anonymize the pasted data. Sorry about that. Be careful about sharing the link, as it may contain somewhat private infos like domain names or IP addresses. Error: %s" % e)
logger.warning(
"For some reason, YunoHost was not able to anonymize the pasted data. Sorry about that. Be careful about sharing the link, as it may contain somewhat private infos like domain names or IP addresses. Error: %s"
% e
)
data = data.encode()
try:
r = requests.post("%s/documents" % paste_server, data=data, timeout=30)
except Exception as e:
raise YunohostError("Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e), raw_msg=True)
raise YunohostError(
"Something wrong happened while trying to paste data on paste.yunohost.org : %s"
% str(e),
raw_msg=True,
)
if r.status_code != 200:
raise YunohostError("Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text), raw_msg=True)
raise YunohostError(
"Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s"
% (r.status_code, r.text),
raw_msg=True,
)
try:
url = json.loads(r.text)["key"]
except:
raise YunohostError("Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text, raw_msg=True)
except Exception:
raise YunohostError(
"Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text,
raw_msg=True,
)
return "%s/raw/%s" % (paste_server, url)
def anonymize(data):
def anonymize_domain(data, domain, redact):
data = data.replace(domain, redact)
# This stuff appears sometimes because some folder in

View file

@ -1,28 +1,41 @@
#!/usr/bin/env python
# Copyright Daniel Roesler, under MIT license, see LICENSE at github.com/diafygi/acme-tiny
import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging
try:
from urllib.request import urlopen, Request # Python 3
except ImportError:
from urllib2 import urlopen, Request # Python 2
DEFAULT_CA = "https://acme-v02.api.letsencrypt.org" # DEPRECATED! USE DEFAULT_DIRECTORY_URL INSTEAD
try:
from urllib.request import urlopen, Request # Python 3
except ImportError:
from urllib2 import urlopen, Request # Python 2
DEFAULT_CA = "https://acme-v02.api.letsencrypt.org" # DEPRECATED! USE DEFAULT_DIRECTORY_URL INSTEAD
DEFAULT_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory"
LOGGER = logging.getLogger(__name__)
LOGGER.addHandler(logging.StreamHandler())
LOGGER.setLevel(logging.INFO)
def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check=False, directory_url=DEFAULT_DIRECTORY_URL, contact=None):
directory, acct_headers, alg, jwk = None, None, None, None # global variables
def get_crt(
account_key,
csr,
acme_dir,
log=LOGGER,
CA=DEFAULT_CA,
disable_check=False,
directory_url=DEFAULT_DIRECTORY_URL,
contact=None,
):
directory, acct_headers, alg, jwk = None, None, None, None # global variables
# helper functions - base64 encode for jose spec
def _b64(b):
return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "")
return base64.urlsafe_b64encode(b).decode("utf8").replace("=", "")
# helper function - run external commands
def _cmd(cmd_list, stdin=None, cmd_input=None, err_msg="Command Line Error"):
proc = subprocess.Popen(cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
proc = subprocess.Popen(
cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
out, err = proc.communicate(cmd_input)
if proc.returncode != 0:
raise IOError("{0}\n{1}".format(err_msg, err))
@ -31,50 +44,87 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
# helper function - make request and automatically parse json response
def _do_request(url, data=None, err_msg="Error", depth=0):
try:
resp = urlopen(Request(url, data=data, headers={"Content-Type": "application/jose+json", "User-Agent": "acme-tiny"}))
resp_data, code, headers = resp.read().decode("utf8"), resp.getcode(), resp.headers
resp = urlopen(
Request(
url,
data=data,
headers={
"Content-Type": "application/jose+json",
"User-Agent": "acme-tiny",
},
)
)
resp_data, code, headers = (
resp.read().decode("utf8"),
resp.getcode(),
resp.headers,
)
except IOError as e:
resp_data = e.read().decode("utf8") if hasattr(e, "read") else str(e)
code, headers = getattr(e, "code", None), {}
try:
resp_data = json.loads(resp_data) # try to parse json results
resp_data = json.loads(resp_data) # try to parse json results
except ValueError:
pass # ignore json parsing errors
if depth < 100 and code == 400 and resp_data['type'] == "urn:ietf:params:acme:error:badNonce":
raise IndexError(resp_data) # allow 100 retrys for bad nonces
pass # ignore json parsing errors
if (
depth < 100
and code == 400
and resp_data["type"] == "urn:ietf:params:acme:error:badNonce"
):
raise IndexError(resp_data) # allow 100 retrys for bad nonces
if code not in [200, 201, 204]:
raise ValueError("{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format(err_msg, url, data, code, resp_data))
raise ValueError(
"{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format(
err_msg, url, data, code, resp_data
)
)
return resp_data, code, headers
# helper function - make signed requests
def _send_signed_request(url, payload, err_msg, depth=0):
payload64 = "" if payload is None else _b64(json.dumps(payload).encode('utf8'))
new_nonce = _do_request(directory['newNonce'])[2]['Replay-Nonce']
payload64 = "" if payload is None else _b64(json.dumps(payload).encode("utf8"))
new_nonce = _do_request(directory["newNonce"])[2]["Replay-Nonce"]
protected = {"url": url, "alg": alg, "nonce": new_nonce}
protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']})
protected64 = _b64(json.dumps(protected).encode('utf8'))
protected_input = "{0}.{1}".format(protected64, payload64).encode('utf8')
out = _cmd(["openssl", "dgst", "-sha256", "-sign", account_key], stdin=subprocess.PIPE, cmd_input=protected_input, err_msg="OpenSSL Error")
data = json.dumps({"protected": protected64, "payload": payload64, "signature": _b64(out)})
protected.update(
{"jwk": jwk} if acct_headers is None else {"kid": acct_headers["Location"]}
)
protected64 = _b64(json.dumps(protected).encode("utf8"))
protected_input = "{0}.{1}".format(protected64, payload64).encode("utf8")
out = _cmd(
["openssl", "dgst", "-sha256", "-sign", account_key],
stdin=subprocess.PIPE,
cmd_input=protected_input,
err_msg="OpenSSL Error",
)
data = json.dumps(
{"protected": protected64, "payload": payload64, "signature": _b64(out)}
)
try:
return _do_request(url, data=data.encode('utf8'), err_msg=err_msg, depth=depth)
except IndexError: # retry bad nonces (they raise IndexError)
return _do_request(
url, data=data.encode("utf8"), err_msg=err_msg, depth=depth
)
except IndexError: # retry bad nonces (they raise IndexError)
return _send_signed_request(url, payload, err_msg, depth=(depth + 1))
# helper function - poll until complete
def _poll_until_not(url, pending_statuses, err_msg):
result, t0 = None, time.time()
while result is None or result['status'] in pending_statuses:
assert (time.time() - t0 < 3600), "Polling timeout" # 1 hour timeout
while result is None or result["status"] in pending_statuses:
assert time.time() - t0 < 3600, "Polling timeout" # 1 hour timeout
time.sleep(0 if result is None else 2)
result, _, _ = _send_signed_request(url, None, err_msg)
return result
# parse account key to get public key
log.info("Parsing account key...")
out = _cmd(["openssl", "rsa", "-in", account_key, "-noout", "-text"], err_msg="OpenSSL Error")
out = _cmd(
["openssl", "rsa", "-in", account_key, "-noout", "-text"],
err_msg="OpenSSL Error",
)
pub_pattern = r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)"
pub_hex, pub_exp = re.search(pub_pattern, out.decode('utf8'), re.MULTILINE|re.DOTALL).groups()
pub_hex, pub_exp = re.search(
pub_pattern, out.decode("utf8"), re.MULTILINE | re.DOTALL
).groups()
pub_exp = "{0:x}".format(int(pub_exp))
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
alg = "RS256"
@ -83,17 +133,24 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
"kty": "RSA",
"n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
}
accountkey_json = json.dumps(jwk, sort_keys=True, separators=(',', ':'))
thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
accountkey_json = json.dumps(jwk, sort_keys=True, separators=(",", ":"))
thumbprint = _b64(hashlib.sha256(accountkey_json.encode("utf8")).digest())
# find domains
log.info("Parsing CSR...")
out = _cmd(["openssl", "req", "-in", csr, "-noout", "-text"], err_msg="Error loading {0}".format(csr))
out = _cmd(
["openssl", "req", "-in", csr, "-noout", "-text"],
err_msg="Error loading {0}".format(csr),
)
domains = set([])
common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8'))
common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode("utf8"))
if common_name is not None:
domains.add(common_name.group(1))
subject_alt_names = re.search(r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL)
subject_alt_names = re.search(
r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n",
out.decode("utf8"),
re.MULTILINE | re.DOTALL,
)
if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "):
if san.startswith("DNS:"):
@ -102,34 +159,48 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
# get the ACME directory of urls
log.info("Getting directory...")
directory_url = CA + "/directory" if CA != DEFAULT_CA else directory_url # backwards compatibility with deprecated CA kwarg
directory_url = (
CA + "/directory" if CA != DEFAULT_CA else directory_url
) # backwards compatibility with deprecated CA kwarg
directory, _, _ = _do_request(directory_url, err_msg="Error getting directory")
log.info("Directory found!")
# create account, update contact details (if any), and set the global key identifier
log.info("Registering account...")
reg_payload = {"termsOfServiceAgreed": True}
account, code, acct_headers = _send_signed_request(directory['newAccount'], reg_payload, "Error registering")
account, code, acct_headers = _send_signed_request(
directory["newAccount"], reg_payload, "Error registering"
)
log.info("Registered!" if code == 201 else "Already registered!")
if contact is not None:
account, _, _ = _send_signed_request(acct_headers['Location'], {"contact": contact}, "Error updating contact details")
log.info("Updated contact details:\n{0}".format("\n".join(account['contact'])))
account, _, _ = _send_signed_request(
acct_headers["Location"],
{"contact": contact},
"Error updating contact details",
)
log.info("Updated contact details:\n{0}".format("\n".join(account["contact"])))
# create a new order
log.info("Creating new order...")
order_payload = {"identifiers": [{"type": "dns", "value": d} for d in domains]}
order, _, order_headers = _send_signed_request(directory['newOrder'], order_payload, "Error creating new order")
order, _, order_headers = _send_signed_request(
directory["newOrder"], order_payload, "Error creating new order"
)
log.info("Order created!")
# get the authorizations that need to be completed
for auth_url in order['authorizations']:
authorization, _, _ = _send_signed_request(auth_url, None, "Error getting challenges")
domain = authorization['identifier']['value']
for auth_url in order["authorizations"]:
authorization, _, _ = _send_signed_request(
auth_url, None, "Error getting challenges"
)
domain = authorization["identifier"]["value"]
log.info("Verifying {0}...".format(domain))
# find the http-01 challenge and write the challenge file
challenge = [c for c in authorization['challenges'] if c['type'] == "http-01"][0]
token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
challenge = [c for c in authorization["challenges"] if c["type"] == "http-01"][
0
]
token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge["token"])
keyauthorization = "{0}.{1}".format(token, thumbprint)
wellknown_path = os.path.join(acme_dir, token)
with open(wellknown_path, "w") as wellknown_file:
@ -137,38 +208,64 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
# check that the file is in place
try:
wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token)
assert (disable_check or _do_request(wellknown_url)[0] == keyauthorization)
wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(
domain, token
)
assert disable_check or _do_request(wellknown_url)[0] == keyauthorization
except (AssertionError, ValueError) as e:
raise ValueError("Wrote file to {0}, but couldn't download {1}: {2}".format(wellknown_path, wellknown_url, e))
raise ValueError(
"Wrote file to {0}, but couldn't download {1}: {2}".format(
wellknown_path, wellknown_url, e
)
)
# say the challenge is done
_send_signed_request(challenge['url'], {}, "Error submitting challenges: {0}".format(domain))
authorization = _poll_until_not(auth_url, ["pending"], "Error checking challenge status for {0}".format(domain))
if authorization['status'] != "valid":
raise ValueError("Challenge did not pass for {0}: {1}".format(domain, authorization))
_send_signed_request(
challenge["url"], {}, "Error submitting challenges: {0}".format(domain)
)
authorization = _poll_until_not(
auth_url,
["pending"],
"Error checking challenge status for {0}".format(domain),
)
if authorization["status"] != "valid":
raise ValueError(
"Challenge did not pass for {0}: {1}".format(domain, authorization)
)
os.remove(wellknown_path)
log.info("{0} verified!".format(domain))
# finalize the order with the csr
log.info("Signing certificate...")
csr_der = _cmd(["openssl", "req", "-in", csr, "-outform", "DER"], err_msg="DER Export Error")
_send_signed_request(order['finalize'], {"csr": _b64(csr_der)}, "Error finalizing order")
csr_der = _cmd(
["openssl", "req", "-in", csr, "-outform", "DER"], err_msg="DER Export Error"
)
_send_signed_request(
order["finalize"], {"csr": _b64(csr_der)}, "Error finalizing order"
)
# poll the order to monitor when it's done
order = _poll_until_not(order_headers['Location'], ["pending", "processing"], "Error checking order status")
if order['status'] != "valid":
order = _poll_until_not(
order_headers["Location"],
["pending", "processing"],
"Error checking order status",
)
if order["status"] != "valid":
raise ValueError("Order failed: {0}".format(order))
# download the certificate
certificate_pem, _, _ = _send_signed_request(order['certificate'], None, "Certificate download failed")
certificate_pem, _, _ = _send_signed_request(
order["certificate"], None, "Certificate download failed"
)
log.info("Certificate signed!")
return certificate_pem
def main(argv=None):
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent("""\
description=textwrap.dedent(
"""\
This script automates the process of getting a signed TLS certificate from Let's Encrypt using
the ACME protocol. It will need to be run on your server and have access to your private
account key, so PLEASE READ THROUGH IT! It's only ~200 lines, so it won't take long.
@ -178,21 +275,64 @@ def main(argv=None):
Example Crontab Renewal (once per month):
0 0 1 * * python /path/to/acme_tiny.py --account-key /path/to/account.key --csr /path/to/domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > /path/to/signed_chain.crt 2>> /var/log/acme_tiny.log
""")
"""
),
)
parser.add_argument(
"--account-key",
required=True,
help="path to your Let's Encrypt account private key",
)
parser.add_argument(
"--csr", required=True, help="path to your certificate signing request"
)
parser.add_argument(
"--acme-dir",
required=True,
help="path to the .well-known/acme-challenge/ directory",
)
parser.add_argument(
"--quiet",
action="store_const",
const=logging.ERROR,
help="suppress output except for errors",
)
parser.add_argument(
"--disable-check",
default=False,
action="store_true",
help="disable checking if the challenge file is hosted correctly before telling the CA",
)
parser.add_argument(
"--directory-url",
default=DEFAULT_DIRECTORY_URL,
help="certificate authority directory url, default is Let's Encrypt",
)
parser.add_argument(
"--ca", default=DEFAULT_CA, help="DEPRECATED! USE --directory-url INSTEAD!"
)
parser.add_argument(
"--contact",
metavar="CONTACT",
default=None,
nargs="*",
help="Contact details (e.g. mailto:aaa@bbb.com) for your account-key",
)
parser.add_argument("--account-key", required=True, help="path to your Let's Encrypt account private key")
parser.add_argument("--csr", required=True, help="path to your certificate signing request")
parser.add_argument("--acme-dir", required=True, help="path to the .well-known/acme-challenge/ directory")
parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors")
parser.add_argument("--disable-check", default=False, action="store_true", help="disable checking if the challenge file is hosted correctly before telling the CA")
parser.add_argument("--directory-url", default=DEFAULT_DIRECTORY_URL, help="certificate authority directory url, default is Let's Encrypt")
parser.add_argument("--ca", default=DEFAULT_CA, help="DEPRECATED! USE --directory-url INSTEAD!")
parser.add_argument("--contact", metavar="CONTACT", default=None, nargs="*", help="Contact details (e.g. mailto:aaa@bbb.com) for your account-key")
args = parser.parse_args(argv)
LOGGER.setLevel(args.quiet or LOGGER.level)
signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca, disable_check=args.disable_check, directory_url=args.directory_url, contact=args.contact)
signed_crt = get_crt(
args.account_key,
args.csr,
args.acme_dir,
log=LOGGER,
CA=args.ca,
disable_check=args.disable_check,
directory_url=args.directory_url,
contact=args.contact,
)
sys.stdout.write(signed_crt)
if __name__ == "__main__": # pragma: no cover
if __name__ == "__main__": # pragma: no cover
main(sys.argv[1:])

View file

@ -12,7 +12,14 @@ reference = json.loads(open(locale_folder + "en.json").read())
for locale_file in locale_files:
print(locale_file)
this_locale = json.loads(open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict)
this_locale = json.loads(
open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict
)
this_locale_fixed = {k: v for k, v in this_locale.items() if k in reference}
json.dump(this_locale_fixed, open(locale_folder + locale_file, "w"), indent=4, ensure_ascii=False)
json.dump(
this_locale_fixed,
open(locale_folder + locale_file, "w"),
indent=4,
ensure_ascii=False,
)

View file

@ -7,11 +7,13 @@ import json
import yaml
import subprocess
ignore = ["password_too_simple_",
"password_listed",
"backup_method_",
"backup_applying_method_",
"confirm_app_install_"]
ignore = [
"password_too_simple_",
"password_listed",
"backup_method_",
"backup_applying_method_",
"confirm_app_install_",
]
###############################################################################
# Find used keys in python code #
@ -49,7 +51,7 @@ def find_expected_string_keys():
# For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries)
# Also we expect to have "diagnosis_description_<name>" for each diagnosis
p3 = re.compile(r'[\"\'](diagnosis_[a-z]+_\w+)[\"\']')
p3 = re.compile(r"[\"\'](diagnosis_[a-z]+_\w+)[\"\']")
for python_file in glob.glob("data/hooks/diagnosis/*.py"):
content = open(python_file).read()
for m in p3.findall(content):
@ -57,7 +59,9 @@ def find_expected_string_keys():
# Ignore some name fragments which are actually concatenated with other stuff..
continue
yield m
yield "diagnosis_description_" + os.path.basename(python_file)[:-3].split("-")[-1]
yield "diagnosis_description_" + os.path.basename(python_file)[:-3].split("-")[
-1
]
# For each migration, expect to find "migration_description_<name>"
for path in glob.glob("src/yunohost/data_migrations/*.py"):
@ -66,7 +70,9 @@ def find_expected_string_keys():
yield "migration_description_" + os.path.basename(path)[:-3]
# For each default service, expect to find "service_description_<name>"
for service, info in yaml.safe_load(open("data/templates/yunohost/services.yml")).items():
for service, info in yaml.safe_load(
open("data/templates/yunohost/services.yml")
).items():
if info is None:
continue
yield "service_description_" + service
@ -75,7 +81,9 @@ def find_expected_string_keys():
# A unit operation is created either using the @is_unit_operation decorator
# or using OperationLogger(
cmd = "grep -hr '@is_unit_operation' src/yunohost/ -A3 2>/dev/null | grep '^def' | sed -E 's@^def (\\w+)\\(.*@\\1@g'"
for funcname in subprocess.check_output(cmd, shell=True).decode("utf-8").strip().split("\n"):
for funcname in (
subprocess.check_output(cmd, shell=True).decode("utf-8").strip().split("\n")
):
yield "log_" + funcname
p4 = re.compile(r"OperationLogger\(\n*\s*[\"\'](\w+)[\"\']")
@ -88,7 +96,9 @@ def find_expected_string_keys():
# Will be on a line like : ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", ...
p5 = re.compile(r" \(\n*\s*[\"\'](\w[\w\.]+)[\"\'],")
content = open("src/yunohost/settings.py").read()
for m in ("global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content)):
for m in (
"global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content)
):
yield m
# Keys for the actionmap ...
@ -134,13 +144,21 @@ def find_expected_string_keys():
for i in [1, 2, 3, 4]:
yield "password_too_simple_%s" % i
checks = ["outgoing_port_25_ok", "ehlo_ok", "fcrdns_ok",
"blacklist_ok", "queue_ok", "ehlo_bad_answer",
"ehlo_unreachable", "ehlo_bad_answer_details",
"ehlo_unreachable_details", ]
checks = [
"outgoing_port_25_ok",
"ehlo_ok",
"fcrdns_ok",
"blacklist_ok",
"queue_ok",
"ehlo_bad_answer",
"ehlo_unreachable",
"ehlo_bad_answer_details",
"ehlo_unreachable_details",
]
for check in checks:
yield "diagnosis_mail_%s" % check
###############################################################################
# Load en locale json keys #
###############################################################################
@ -149,6 +167,7 @@ def find_expected_string_keys():
def keys_defined_for_en():
return json.loads(open("locales/en.json").read()).keys()
###############################################################################
# Compare keys used and keys defined #
###############################################################################
@ -163,8 +182,10 @@ def test_undefined_i18n_keys():
undefined_keys = sorted(undefined_keys)
if undefined_keys:
raise Exception("Those i18n keys should be defined in en.json:\n"
" - " + "\n - ".join(undefined_keys))
raise Exception(
"Those i18n keys should be defined in en.json:\n"
" - " + "\n - ".join(undefined_keys)
)
def test_unused_i18n_keys():
@ -173,5 +194,6 @@ def test_unused_i18n_keys():
unused_keys = sorted(unused_keys)
if unused_keys:
raise Exception("Those i18n keys appears unused:\n"
" - " + "\n - ".join(unused_keys))
raise Exception(
"Those i18n keys appears unused:\n" " - " + "\n - ".join(unused_keys)
)

View file

@ -27,7 +27,9 @@ def find_inconsistencies(locale_file):
# should also be in the translated string, otherwise the .format
# will trigger an exception!
subkeys_in_ref = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", string))
subkeys_in_this_locale = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key]))
subkeys_in_this_locale = set(
k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key])
)
if any(k not in subkeys_in_ref for k in subkeys_in_this_locale):
yield """\n
@ -35,11 +37,16 @@ def find_inconsistencies(locale_file):
Format inconsistency for string {key} in {locale_file}:"
en.json -> {string}
{locale_file} -> {translated_string}
""".format(key=key, string=string.encode("utf-8"), locale_file=locale_file, translated_string=this_locale[key].encode("utf-8"))
""".format(
key=key,
string=string.encode("utf-8"),
locale_file=locale_file,
translated_string=this_locale[key].encode("utf-8"),
)
@pytest.mark.parametrize('locale_file', locale_files)
@pytest.mark.parametrize("locale_file", locale_files)
def test_translation_format_consistency(locale_file):
inconsistencies = list(find_inconsistencies(locale_file))
if inconsistencies:
raise Exception(''.join(inconsistencies))
raise Exception("".join(inconsistencies))

View file

@ -7,7 +7,7 @@ deps =
py37-{lint,invalidcode}: flake8
py37-black-{run,check}: black
commands =
py37-lint: flake8 src doc data tests --ignore E402,E501 --exclude src/yunohost/vendor
py37-lint: flake8 src doc data tests --ignore E402,E501,E203,W503 --exclude src/yunohost/vendor
py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F
py37-black-check: black --check --diff src doc data tests
py37-black-run: black src doc data tests