Merge branch 'dev' into sftp_permission

This commit is contained in:
Alexandre Aubin 2021-03-25 14:42:30 +01:00 committed by GitHub
commit f0c4498c80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
128 changed files with 10167 additions and 5736 deletions

View file

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

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

@ -26,4 +26,4 @@ install-postinstall:
script:
- apt-get update -o Acquire::Retries=3
- DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ./$YNH_BUILD_DIR/*.deb
- yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns
- yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace

View file

@ -3,14 +3,6 @@
########################################
# later we must fix lint and format-check jobs and remove "allow_failure"
lint27:
stage: lint
image: "before-install"
needs: []
allow_failure: true
script:
- tox -e py27-lint
lint37:
stage: lint
image: "before-install"
@ -19,17 +11,9 @@ lint37:
script:
- tox -e py37-lint
invalidcode27:
stage: lint
image: "before-install"
needs: []
script:
- tox -e py27-invalidcode
invalidcode37:
stage: lint
image: "before-install"
allow_failure: true
needs: []
script:
- tox -e py37-invalidcode
@ -37,7 +21,27 @@ invalidcode37:
format-check:
stage: lint
image: "before-install"
needs: []
allow_failure: true
needs: []
script:
- tox -e py37-black
- tox -e py37-black-check
format-run:
stage: lint
image: "before-install"
needs: []
before_script:
- apt-get update -y && apt-get install git hub -y
- git config --global user.email "yunohost@yunohost.org"
- git config --global user.name "$GITHUB_USER"
- hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo
- cd github_repo
script:
# checkout or create and checkout the branch
- hub checkout "ci-format-${CI_COMMIT_REF_NAME}" || hub checkout -b "ci-format-${CI_COMMIT_REF_NAME}"
- tox -e py37-black-run
- hub commit -am "[CI] Format code" || true
- hub pull-request -m "[CI] Format code" -b Yunohost:dev -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd
only:
refs:
- dev

View file

@ -34,9 +34,9 @@ full-tests:
PYTEST_ADDOPTS: "--color=yes"
before_script:
- *install_debs
- yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns
- yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace
script:
- python -m pytest --cov=yunohost tests/ src/yunohost/tests/ --junitxml=report.xml
- python3 -m pytest --cov=yunohost tests/ src/yunohost/tests/ --junitxml=report.xml
needs:
- job: build-yunohost
artifacts: true
@ -51,70 +51,70 @@ full-tests:
root-tests:
extends: .test-stage
script:
- python -m pytest tests
- python3 -m pytest tests
test-apps:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_apps.py
- python3 -m pytest tests/test_apps.py
test-appscatalog:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_appscatalog.py
- python3 -m pytest tests/test_appscatalog.py
test-appurl:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_appurl.py
- python3 -m pytest tests/test_appurl.py
test-apps-arguments-parsing:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_apps_arguments_parsing.py
- python3 -m pytest tests/test_apps_arguments_parsing.py
test-backuprestore:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_backuprestore.py
- python3 -m pytest tests/test_backuprestore.py
test-changeurl:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_changeurl.py
- python3 -m pytest tests/test_changeurl.py
test-permission:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_permission.py
- python3 -m pytest tests/test_permission.py
test-settings:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_settings.py
- python3 -m pytest tests/test_settings.py
test-user-group:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_user-group.py
- python3 -m pytest tests/test_user-group.py
test-regenconf:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_regenconf.py
- python3 -m pytest tests/test_regenconf.py
test-service:
extends: .test-stage
script:
- cd src/yunohost
- python -m pytest tests/test_service.py
- python3 -m pytest tests/test_service.py

View file

@ -1,22 +0,0 @@
language: python
matrix:
allow_failures:
- env: TOXENV=py27-lint
- env: TOXENV=py37-lint
- env: TOXENV=py37-invalidcode
include:
- python: 2.7
env: TOXENV=py27-lint
- python: 2.7
env: TOXENV=py27-invalidcode
- python: 3.7
env: TOXENV=py37-lint
- python: 3.7
env: TOXENV=py37-invalidcode
install:
- pip install tox
script:
- tox

View file

@ -1,4 +1,4 @@
#! /usr/bin/python
#! /usr/bin/python3
# -*- coding: utf-8 -*-
import os

View file

@ -1,4 +1,4 @@
#! /usr/bin/python
#! /usr/bin/python3
# -*- coding: utf-8 -*-
import sys

View file

@ -6,7 +6,7 @@ x509_fingerprint=$(openssl x509 -in /etc/yunohost/certs/yunohost.org/crt.pem -n
# Fetch SSH fingerprints
i=0
for key in $(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key.pub 2> /dev/null) ; do
for key in $(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key.pub 2> /dev/null) ; do
output=$(ssh-keygen -l -f $key)
fingerprint[$i]=" - $(echo $output | cut -d' ' -f2) $(echo $output| cut -d' ' -f4)"
i=$(($i + 1))
@ -43,22 +43,21 @@ LOGO_AND_FINGERPRINTS=$(cat << EOF
$LOGO
IP: ${local_ip}
X509 fingerprint: ${x509_fingerprint}
Local IP: ${local_ip:-(no ip detected?)}
Local SSL CA X509 fingerprint:
${x509_fingerprint}
SSH fingerprints:
${fingerprint[0]}
${fingerprint[1]}
${fingerprint[2]}
${fingerprint[3]}
${fingerprint[4]}
EOF
)
if [[ -f /etc/yunohost/installed ]]
echo "$LOGO_AND_FINGERPRINTS" > /etc/issue
if [[ ! -f /etc/yunohost/installed ]]
then
echo "$LOGO_AND_FINGERPRINTS" > /etc/issue
else
chvt 2
# Formatting
@ -73,7 +72,7 @@ be asked for :
- the administration password.
You can perform this step :
- from your web browser, by accessing : ${local_ip}
- from your web browser, by accessing : https://yunohost.local/ or ${local_ip}
- or in this terminal by answering 'yes' to the following question
If this is your first time with YunoHost, it is strongly recommended to take

View file

@ -87,7 +87,7 @@ user:
ask: ask_firstname
required: True
pattern: &pattern_firstname
- !!str ^([^\W\d_]{2,30}[ ,.'-]{0,3})+$
- !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$
- "pattern_firstname"
-l:
full: --lastname
@ -95,7 +95,7 @@ user:
ask: ask_lastname
required: True
pattern: &pattern_lastname
- !!str ^([^\W\d_]{2,30}[ ,.'-]{0,3})+$
- !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$
- "pattern_lastname"
-m:
full: --mail
@ -165,8 +165,11 @@ user:
full: --change-password
help: New password to set
metavar: PASSWORD
nargs: "?"
const: 0
extra:
pattern: *pattern_password
comment: good_practices_about_user_password
--add-mailforward:
help: Mailforward addresses to add
nargs: "*"
@ -307,7 +310,7 @@ user:
api: GET /users/permissions/<permission>
arguments:
permission:
help: Name of the permission to fetch info about
help: Name of the permission to fetch info about (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions)
### user_permission_update()
update:
@ -315,7 +318,7 @@ user:
api: PUT /users/permissions/<permission>
arguments:
permission:
help: Permission to manage (e.g. mail or nextcloud or wordpress.editors)
help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions)
-a:
full: --add
help: Group or usernames to grant this permission to
@ -346,7 +349,7 @@ user:
api: DELETE /users/permissions/<app>
arguments:
permission:
help: Permission to manage (e.g. mail or nextcloud or wordpress.editors)
help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions)
ssh:
subcategory_help: Manage ssh access
@ -428,6 +431,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:
@ -558,6 +569,13 @@ app:
full: --with-categories
help: Also return a list of app categories
action: store_true
### app_search()
search:
action_help: Search installable apps
arguments:
string:
help: Return matching app name or description with "string"
fetchlist:
deprecated: true
@ -1325,13 +1343,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
#############################
@ -1393,6 +1409,10 @@ tools:
--force-password:
help: Use this if you really want to set a weak password
action: store_true
--force-diskspace:
help: Use this if you really want to install Yunohost on a setup with less than 10 GB on the root filesystem
action: store_true
### tools_update()
update:
@ -1495,10 +1515,12 @@ tools:
help: list only migrations already performed
action: store_true
### tools_migrations_migrate()
migrate:
### tools_migrations_run()
run:
action_help: Run migrations
api: POST /migrations/migrate
api: POST /migrations/run
deprecated_alias:
- migrate
arguments:
targets:
help: Migrations to run (all pendings by default)
@ -1645,10 +1667,12 @@ log:
help: Include metadata about operations that are not the main operation but are sub-operations triggered by another ongoing operation... (e.g. initializing groups/permissions when installing an app)
action: store_true
### log_display()
display:
### log_show()
show:
action_help: Display a log content
api: GET /logs/display
api: GET /logs/<path>
deprecated_alias:
- display
arguments:
path:
help: Log file which to display the content
@ -1658,7 +1682,7 @@ log:
default: 50
type: int
--share:
help: Share the full log using yunopaste
help: (Deprecated, see yunohost log share) Share the full log using yunopaste
action: store_true
-i:
full: --filter-irrelevant
@ -1669,6 +1693,14 @@ log:
help: Include metadata about sub-operations of this operation... (e.g. initializing groups/permissions when installing an app)
action: store_true
### log_share()
share:
action_help: Share the full log on yunopaste (alias to show --share)
api: GET /logs/share
arguments:
path:
help: Log file to share
#############################
# Diagnosis #

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

@ -32,7 +32,7 @@ ynh_wait_dpkg_free() {
if echo "$dpkg_file" | grep --perl-regexp --quiet "^[[:digit:]]+$"
then
# If so, that a remaining of dpkg.
ynh_print_err "E: dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem."
ynh_print_err "dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem."
set -o xtrace # set -x
return 1
fi
@ -47,10 +47,11 @@ ynh_wait_dpkg_free() {
# Check either a package is installed or not
#
# example: ynh_package_is_installed --package=yunohost && echo "ok"
# example: ynh_package_is_installed --package=yunohost && echo "installed"
#
# usage: ynh_package_is_installed --package=name
# | arg: -p, --package= - the package name to check
# | ret: 0 if the package is installed, 1 else.
#
# Requires YunoHost version 2.2.4 or higher.
ynh_package_is_installed() {
@ -180,7 +181,7 @@ ynh_package_install_from_equivs () {
# Build and install the package
local TMPDIR=$(mktemp --directory)
# Force the compatibility level at 10, levels below are deprecated
# Force the compatibility level at 10, levels below are deprecated
echo 10 > /usr/share/equivs/template/debian/compat
# Note that the cd executes into a sub shell
@ -194,7 +195,7 @@ ynh_package_install_from_equivs () {
LC_ALL=C dpkg --force-depends --install "./${pkgname}_${pkgversion}_all.deb" 2>&1 | tee ./dpkg_log)
ynh_package_install --fix-broken || \
{ # If the installation failed
{ # If the installation failed
# (the following is ran inside { } to not start a subshell otherwise ynh_die wouldnt exit the original process)
# Parse the list of problematic dependencies from dpkg's log ...
# (relevant lines look like: "foo-ynh-deps depends on bar; however:")
@ -216,7 +217,8 @@ ynh_package_install_from_equivs () {
# example : ynh_install_app_dependencies dep1 dep2 "dep3|dep4|dep5"
#
# usage: ynh_install_app_dependencies dep [dep [...]]
# | arg: dep - the package name to install in dependence. Writing "dep3|dep4|dep5" can be used to specify alternatives. For example : dep1 dep2 "dep3|dep4|dep5" will require to install dep1 and dep 2 and (dep3 or dep4 or dep5).
# | arg: dep - the package name to install in dependence.
# | arg: "dep1|dep2|…" - You can specify alternatives. It will require to install (dep1 or dep2, etc).
#
# Requires YunoHost version 2.6.4 or higher.
ynh_install_app_dependencies () {
@ -224,13 +226,10 @@ ynh_install_app_dependencies () {
# Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below)
dependencies="$(echo "$dependencies" | sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g')"
local dependencies=${dependencies//|/ | }
local manifest_path="../manifest.json"
if [ ! -e "$manifest_path" ]; then
manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place
fi
local manifest_path="$YNH_APP_BASEDIR/manifest.json"
local version=$(grep '\"version\": ' "$manifest_path" | cut --delimiter='"' --fields=4) # Retrieve the version number in the manifest file.
if [ ${#version} -eq 0 ]; then
local version=$(jq -r '.version' "$manifest_path")
if [ -z "${version}" ] || [ "$version" == "null" ]; then
version="1.0"
fi
local dep_app=${app//_/-} # Replace all '_' by '-'
@ -253,7 +252,7 @@ ynh_install_app_dependencies () {
# Epic ugly hack to fix the goddamn dependency nightmare of sury
# Sponsored by the "Djeezusse Fokin Kraiste Why Do Adminsys Has To Be So Fucking Complicated I Should Go Grow Potatoes Instead Of This Shit" collective
# https://github.com/YunoHost/issues/issues/1407
#
#
# If we require to install php dependency
if echo $dependencies | grep --quiet 'php'
then
@ -459,10 +458,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 "/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/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 --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
@ -548,6 +548,9 @@ ynh_pin_repo () {
append="tee"
fi
# Sury pinning is managed by the regenconf in the core...
[[ "$name" != "extra_php_version" ]] || return 0
mkdir --parents "/etc/apt/preferences.d"
echo "Package: $package
Pin: $pin

View file

@ -13,13 +13,13 @@ CAN_BIND=${CAN_BIND:-1}
#
# This helper can be used both in a system backup hook, and in an app backup script
#
# Details: ynh_backup writes SRC and the relative DEST into a CSV file. And it
# `ynh_backup` writes `src_path` and the relative `dest_path` into a CSV file, and it
# creates the parent destination directory
#
# If DEST is ended by a slash it complete this path with the basename of SRC.
#
# Example in the context of a wordpress app
# If `dest_path` is ended by a slash it complete this path with the basename of `src_path`.
#
# Example in the context of a wordpress app :
# ```
# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf"
# # => This line will be added into CSV file
# # "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/etc/nginx/conf.d/$domain.d/$app.conf"
@ -40,26 +40,28 @@ CAN_BIND=${CAN_BIND:-1}
# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "/conf/"
# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/$app.conf"
#
# ```
#
# How to use --is_big:
# --is_big is used to specify that this part of the backup can be quite huge.
# How to use `--is_big`:
#
# `--is_big` is used to specify that this part of the backup can be quite huge.
# So, you don't want that your package does backup that part during ynh_backup_before_upgrade.
# In the same way, an user may doesn't want to backup this big part of the app for
# each of his backup. And so handle that part differently.
#
# As this part of your backup may not be done, your restore script has to handle it.
# In your restore script, use --not_mandatory with ynh_restore_file
# As well in your remove script, you should not remove those data ! Or an user may end up with
# a failed upgrade restoring an app without data anymore !
# each of his backup. And so handle that part differently.
#
# To have the benefit of --is_big while doing a backup, you can whether set the environement
# variable BACKUP_CORE_ONLY to 1 (BACKUP_CORE_ONLY=1) before the backup command. It will affect
# only that backup command.
# Or set the config do_not_backup_data to 1 into the settings.yml of the app. This will affect
# all backups for this app until the setting is removed.
# As this part of your backup may not be done, your restore script has to handle it.
# In your restore script, use `--not_mandatory` with `ynh_restore_file`
# As well in your remove script, you should not remove those data ! Or an user may end up with
# a failed upgrade restoring an app without data anymore !
#
# To have the benefit of `--is_big` while doing a backup, you can whether set the environement
# variable `BACKUP_CORE_ONLY` to 1 (`BACKUP_CORE_ONLY=1`) before the backup command. It will affect
# only that backup command.
# Or set the config `do_not_backup_data` to 1 into the `settings.yml` of the app. This will affect
# all backups for this app until the setting is removed.
#
# Requires YunoHost version 2.4.0 or higher.
# Requires YunoHost version 3.5.0 or higher for the argument --not_mandatory
# Requires YunoHost version 3.5.0 or higher for the argument `--not_mandatory`
ynh_backup() {
# TODO find a way to avoid injection by file strange naming !
@ -81,7 +83,7 @@ ynh_backup() {
# If backing up core only (used by ynh_backup_before_upgrade),
# don't backup big data items
if [ $is_big -eq 1 ] && ( [ ${do_not_backup_data:-0} -eq 1 ] || [ $BACKUP_CORE_ONLY -eq 1 ] )
if [ $is_big -eq 1 ] && ( [ ${do_not_backup_data:-0} -eq 1 ] || [ $BACKUP_CORE_ONLY -eq 1 ] )
then
if [ $BACKUP_CORE_ONLY -eq 1 ]
then
@ -221,26 +223,25 @@ with open(sys.argv[1], 'r') as backup_file:
# Restore a file or a directory
#
# Use the registered path in backup_list by ynh_backup to restore the file at
# the right place.
#
# usage: ynh_restore_file --origin_path=origin_path [--dest_path=dest_path] [--not_mandatory]
# | arg: -o, --origin_path= - Path where was located the file or the directory before to be backuped or relative path to $YNH_CWD where it is located in the backup archive
# | arg: -d, --dest_path= - Path where restore the file or the dir, if unspecified, the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in the archive, the destination will be searched into backup.csv
# | arg: -d, --dest_path= - Path where restore the file or the dir. If unspecified, the destination will be `ORIGIN_PATH` or if the `ORIGIN_PATH` doesn't exist in the archive, the destination will be searched into `backup.csv`
# | arg: -m, --not_mandatory - Indicate that if the file is missing, the restore process can ignore it.
#
# Use the registered path in backup_list by ynh_backup to restore the file at the right place.
#
# examples:
# ynh_restore_file "/etc/nginx/conf.d/$domain.d/$app.conf"
# ynh_restore_file -o "/etc/nginx/conf.d/$domain.d/$app.conf"
# # You can also use relative paths:
# ynh_restore_file "conf/nginx.conf"
# ynh_restore_file -o "conf/nginx.conf"
#
# If DEST_PATH already exists and is lighter than 500 Mo, a backup will be made in
# /home/yunohost.conf/backup/. Otherwise, the existing file is removed.
# If `DEST_PATH` already exists and is lighter than 500 Mo, a backup will be made in
# `/home/yunohost.conf/backup/`. Otherwise, the existing file is removed.
#
# if apps/wordpress/etc/nginx/conf.d/$domain.d/$app.conf exists, restore it into
# /etc/nginx/conf.d/$domain.d/$app.conf
# if `apps/$app/etc/nginx/conf.d/$domain.d/$app.conf` exists, restore it into
# `/etc/nginx/conf.d/$domain.d/$app.conf`
# if no, search for a match in the csv (eg: conf/nginx.conf) and restore it into
# /etc/nginx/conf.d/$domain.d/$app.conf
# `/etc/nginx/conf.d/$domain.d/$app.conf`
#
# Requires YunoHost version 2.6.4 or higher.
# Requires YunoHost version 3.5.0 or higher for the argument --not_mandatory
@ -345,14 +346,14 @@ ynh_store_file_checksum () {
}
# Verify the checksum and backup the file if it's different
#
# This helper is primarily meant to allow to easily backup personalised/manually
# modified config files.
#
# usage: ynh_backup_if_checksum_is_different --file=file
# | arg: -f, --file= - The file on which the checksum test will be perfomed.
# | ret: the name of a backup file, or nothing
#
# This helper is primarily meant to allow to easily backup personalised/manually
# modified config files.
#
# Requires YunoHost version 2.6.4 or higher.
ynh_backup_if_checksum_is_different () {
# Declare an array to define the options of this helper.
@ -399,14 +400,27 @@ 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:
# usage: ynh_backup_before_upgrade
#
# Usage in a package script:
# ```
# ynh_backup_before_upgrade
# ynh_clean_setup () {
# ynh_restore_upgradebackup
# }
# ynh_abort_if_errors
# ```
#
# Requires YunoHost version 2.7.2 or higher.
ynh_backup_before_upgrade () {
@ -423,7 +437,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 +449,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
@ -450,12 +464,16 @@ ynh_backup_before_upgrade () {
# Restore a previous backup if the upgrade process failed
#
# usage:
# usage: ynh_restore_upgradebackup
#
# Usage in a package script:
# ```
# ynh_backup_before_upgrade
# ynh_clean_setup () {
# ynh_restore_upgradebackup
# }
# ynh_abort_if_errors
# ```
#
# Requires YunoHost version 2.7.2 or higher.
ynh_restore_upgradebackup () {
@ -467,7 +485,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

@ -12,18 +12,14 @@
#
# usage 2: ynh_add_fail2ban_config --use_template [--others_var="list of others variables to replace"]
# | arg: -t, --use_template - Use this helper in template mode
# | arg: -v, --others_var= - List of others variables to replace separeted by a space
# | for example : 'var_1 var_2 ...'
# | arg: -v, --others_var= - List of others variables to replace separeted by a space 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
# This will use a template in `../conf/f2b_jail.conf` and `../conf/f2b_filter.conf`
# 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):
#
# ```
# f2b_jail.conf:
# [__APP__]
# enabled = true
@ -31,7 +27,8 @@
# filter = __APP__
# logpath = /var/log/__APP__/logfile.log
# maxretry = 3
#
# ```
# ```
# f2b_filter.conf:
# [INCLUDES]
# before = common.conf
@ -44,28 +41,31 @@
# failregex = ^%(__synapse_start_line)s INFO \- POST\-(\d+)\- <HOST> \- \d+ \- Received request\: POST /_matrix/client/r0/login\??<SKIPLINES>%(__synapse_start_line)s INFO \- POST\-\1\- Got login request with identifier: \{u'type': u'm.id.user', u'user'\: u'(.+?)'\}, medium\: None, address: None, user\: u'\5'<SKIPLINES>%(__synapse_start_line)s WARNING \- \- (Attempted to login as @\5\:.+ but they do not exist|Failed password login for user @\5\:.+)$
#
# ignoreregex =
# ```
#
# -----------------------------------------------------------------------------
#
# Note about the "failregex" option:
# regex to match the password failure messages in the logfile. The
# host must be matched by a group named "host". The tag "<HOST>" can
# be used for standard IP/hostname matching and is only an alias for
# (?:::f{4,6}:)?(?P<host>[\w\-.^_]+)
#
# You can find some more explainations about how to make a regex here :
# https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters
# regex to match the password failure messages in the logfile. The host must be
# matched by a group named "`host`". The tag "`<HOST>`" can be used for standard
# IP/hostname matching and is only an alias for `(?:::f{4,6}:)?(?P<host>[\w\-.^_]+)`
#
# You can find some more explainations about how to make a regex here :
# https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters
#
# Note that the logfile need to exist before to call this helper !!
#
# To validate your regex you can test with this command:
# ```
# fail2ban-regex /var/log/YOUR_LOG_FILE_PATH /etc/fail2ban/filter.d/YOUR_APP.conf
# ```
#
# 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 -A args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template [v]=others_var=)
local logpath
local failregex
local max_retry
@ -76,61 +76,37 @@ ynh_add_fail2ban_config () {
ynh_handle_getopts_args "$@"
max_retry=${max_retry:-3}
ports=${ports:-http,https}
others_var=${others_var:-}
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"
[[ -z "$others_var" ]] || ynh_print_warn --message="Packagers: using --others_var is unecessary since Yunohost 4.2"
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 +124,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
}

View file

@ -7,7 +7,7 @@
# | arg: -t, --total - Count total RAM+swap
# | arg: -s, --ignore_swap - Ignore swap, consider only real RAM
# | arg: -o, --only_swap - Ignore real RAM, consider only swap
# | ret: the amount of free ram
# | ret: the amount of free ram, in MB (MegaBytes)
#
# Requires YunoHost version 3.8.1 or higher.
ynh_get_ram () {
@ -35,7 +35,7 @@ ynh_get_ram () {
local free_ram=$(vmstat --stats --unit M | grep "free memory" | awk '{print $1}')
local free_swap=$(vmstat --stats --unit M | grep "free swap" | awk '{print $1}')
local free_ram_swap=$(( free_ram + free_swap ))
# Use the total amount of free ram
local ram=$free_ram_swap
if [ $ignore_swap -eq 1 ]
@ -52,7 +52,7 @@ ynh_get_ram () {
local total_ram=$(vmstat --stats --unit M | grep "total memory" | awk '{print $1}')
local total_swap=$(vmstat --stats --unit M | grep "total swap" | awk '{print $1}')
local total_ram_swap=$(( total_ram + total_swap ))
local ram=$total_ram_swap
if [ $ignore_swap -eq 1 ]
then
@ -70,13 +70,13 @@ ynh_get_ram () {
# Return 0 or 1 depending if the system has a given amount of RAM+swap free or total
#
# usage: ynh_require_ram --required=RAM required in Mb [--free|--total] [--ignore_swap|--only_swap]
# | arg: -r, --required= - The amount to require, in Mb
# usage: ynh_require_ram --required=RAM [--free|--total] [--ignore_swap|--only_swap]
# | arg: -r, --required= - The amount to require, in MB
# | arg: -f, --free - Count free RAM+swap
# | arg: -t, --total - Count total RAM+swap
# | arg: -s, --ignore_swap - Ignore swap, consider only real RAM
# | arg: -o, --only_swap - Ignore real RAM, consider only swap
# | exit: Return 1 if the ram is under the requirement, 0 otherwise.
# | ret: 1 if the ram is under the requirement, 0 otherwise.
#
# Requires YunoHost version 3.8.1 or higher.
ynh_require_ram () {

View file

@ -35,7 +35,7 @@ ynh_print_info() {
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
echo "$message" >> "$YNH_STDINFO"
echo "$message" >&$YNH_STDINFO
}
# Ignore the yunohost-cli log to prevent errors with conditional commands
@ -102,8 +102,7 @@ ynh_print_err () {
# Execute a command and print the result as an error
#
# usage: ynh_exec_err your_command
# usage: ynh_exec_err "your_command | other_command"
# usage: ynh_exec_err "your_command [ | other_command ]"
# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
@ -117,8 +116,7 @@ ynh_exec_err () {
# Execute a command and print the result as a warning
#
# usage: ynh_exec_warn your_command
# usage: ynh_exec_warn "your_command | other_command"
# usage: ynh_exec_warn "your_command [ | other_command ]"
# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
@ -132,8 +130,7 @@ ynh_exec_warn () {
# Execute a command and force the result to be printed on stdout
#
# usage: ynh_exec_warn_less your_command
# usage: ynh_exec_warn_less "your_command | other_command"
# usage: ynh_exec_warn_less "your_command [ | other_command ]"
# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
@ -147,8 +144,7 @@ ynh_exec_warn_less () {
# Execute a command and redirect stdout in /dev/null
#
# usage: ynh_exec_quiet your_command
# usage: ynh_exec_quiet "your_command | other_command"
# usage: ynh_exec_quiet "your_command [ | other_command ]"
# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
@ -162,8 +158,7 @@ ynh_exec_quiet () {
# Execute a command and redirect stdout and stderr in /dev/null
#
# usage: ynh_exec_fully_quiet your_command
# usage: ynh_exec_fully_quiet "your_command | other_command"
# usage: ynh_exec_fully_quiet "your_command [ | other_command ]"
# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.
@ -363,8 +358,7 @@ ynh_debug () {
# Execute a command and print the result as debug
#
# usage: ynh_debug_exec your_command
# usage: ynh_debug_exec "your_command | other_command"
# usage: ynh_debug_exec "your_command [ | other_command ]"
# | arg: command - command to execute
#
# When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe.

View file

@ -7,16 +7,14 @@
# | arg: -n, --nonappend - (optional) Replace the config file instead of appending this new config.
# | arg: -u, --specific_user= - run logrotate as the specified user and group. If not specified logrotate is runned as root.
#
# If no --logfile is provided, /var/log/${app} will be used as default.
# logfile can be just a directory, or a full path to a logfile :
# /parentdir/logdir
# /parentdir/logdir/logfile.log
# If no `--logfile` is provided, `/var/log/$app` will be used as default.
# `logfile` can point to a directory or a file.
#
# It's possible to use this helper multiple times, each config will be added to
# the same logrotate config file. Unless you use the option --non-append
# the same logrotate config file. Unless you use the option `--non-append`
#
# Requires YunoHost version 2.6.4 or higher.
# Requires YunoHost version 3.2.0 or higher for the argument --specific_user
# Requires YunoHost version 3.2.0 or higher for the argument `--specific_user`
ynh_use_logrotate () {
# Declare an array to define the options of this helper.
local legacy_args=lnuya

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

@ -0,0 +1,96 @@
#!/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
#
# usage: ynh_multimedia_addfolder --source_dir="source_dir" --dest_dir="dest_dir"
#
# | 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"
#
# This "directory" will be a symbolic link to a existing directory.
#
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

@ -1,17 +1,16 @@
#!/bin/bash
MYSQL_ROOT_PWD_FILE=/etc/yunohost/mysql
# Open a connection as a user
#
# example: ynh_mysql_connect_as --user="user" --password="pass" <<< "UPDATE ...;"
# example: ynh_mysql_connect_as --user="user" --password="pass" < /path/to/file.sql
#
# usage: ynh_mysql_connect_as --user=user --password=password [--database=database]
# | arg: -u, --user= - the user name to connect as
# | arg: -p, --password= - the user password
# | arg: -d, --database= - the database to connect to
#
# examples:
# ynh_mysql_connect_as --user="user" --password="pass" <<< "UPDATE ...;"
# ynh_mysql_connect_as --user="user" --password="pass" < /path/to/file.sql
#
# Requires YunoHost version 2.2.4 or higher.
ynh_mysql_connect_as() {
# Declare an array to define the options of this helper.
@ -49,8 +48,7 @@ ynh_mysql_execute_as_root() {
database="--database=$database"
fi
ynh_mysql_connect_as --user="root" --password="$(cat $MYSQL_ROOT_PWD_FILE)" \
$database <<< "$sql"
mysql -B "$database" <<< "$sql"
}
# Execute a command from a file as root user
@ -75,9 +73,7 @@ ynh_mysql_execute_file_as_root() {
database="--database=$database"
fi
ynh_mysql_connect_as --user="root" --password="$(cat $MYSQL_ROOT_PWD_FILE)" \
$database < "$file"
mysql -B "$database" < "$file"
}
# Create a database and grant optionnaly privilegies to a user
@ -125,11 +121,11 @@ ynh_mysql_drop_db() {
# Dump a database
#
# example: ynh_mysql_dump_db --database=roundcube > ./dump.sql
#
# usage: ynh_mysql_dump_db --database=database
# | arg: -d, --database= - the database name to dump
# | ret: the mysqldump output
# | ret: The mysqldump output
#
# example: ynh_mysql_dump_db --database=roundcube > ./dump.sql
#
# Requires YunoHost version 2.2.4 or higher.
ynh_mysql_dump_db() {
@ -140,7 +136,7 @@ ynh_mysql_dump_db() {
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
mysqldump --user="root" --password="$(cat $MYSQL_ROOT_PWD_FILE)" --single-transaction --skip-dump-date "$database"
mysqldump --single-transaction --skip-dump-date "$database"
}
# Create a user
@ -161,7 +157,7 @@ ynh_mysql_create_user() {
#
# usage: ynh_mysql_user_exists --user=user
# | arg: -u, --user= - the user for which to check existence
# | exit: Return 1 if the user doesn't exist, 0 otherwise.
# | ret: 0 if the user exists, 1 otherwise.
#
# Requires YunoHost version 2.2.4 or higher.
ynh_mysql_user_exists()
@ -200,8 +196,8 @@ ynh_mysql_drop_user() {
# | arg: -n, --db_name= - Name of the database
# | arg: -p, --db_pwd= - Password of the database. If not provided, a password will be generated
#
# After executing this helper, the password of the created database will be available in $db_pwd
# It will also be stored as "mysqlpwd" into the app settings.
# After executing this helper, the password of the created database will be available in `$db_pwd`
# It will also be stored as "`mysqlpwd`" into the app settings.
#
# Requires YunoHost version 2.6.4 or higher.
ynh_mysql_setup_db () {
@ -214,12 +210,13 @@ ynh_mysql_setup_db () {
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
local new_db_pwd=$(ynh_string_random) # Generate a random password
# Generate a random password
local new_db_pwd=$(ynh_string_random)
# If $db_pwd is not provided, use new_db_pwd instead for db_pwd
db_pwd="${db_pwd:-$new_db_pwd}"
ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd" # Create the database
ynh_app_setting_set --app=$app --key=mysqlpwd --value=$db_pwd # Store the password in the app's config
ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd"
ynh_app_setting_set --app=$app --key=mysqlpwd --value=$db_pwd
}
# Remove a database if it exists, and the associated user
@ -232,16 +229,14 @@ ynh_mysql_setup_db () {
ynh_mysql_remove_db () {
# Declare an array to define the options of this helper.
local legacy_args=un
local -A args_array=( [u]=db_user= [n]=db_name= )
local -Ar args_array=( [u]=db_user= [n]=db_name= )
local db_user
local db_name
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
local mysql_root_password=$(cat $MYSQL_ROOT_PWD_FILE)
if mysqlshow --user=root --password=$mysql_root_password | grep --quiet "^| $db_name"
then # Check if the database exists
ynh_mysql_drop_db $db_name # Remove the database
if mysqlshow | grep -q "^| $db_name "; then
ynh_mysql_drop_db $db_name
else
ynh_print_warn --message="Database $db_name not found"
fi

View file

@ -2,12 +2,12 @@
# Find a free port and return it
#
# example: port=$(ynh_find_port --port=8080)
#
# usage: ynh_find_port --port=begin_port
# | arg: -p, --port= - port to start to search
# | ret: the port number
#
# example: port=$(ynh_find_port --port=8080)
#
# Requires YunoHost version 2.6.4 or higher.
ynh_find_port () {
# Declare an array to define the options of this helper.
@ -27,11 +27,11 @@ 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"
#
# usage: ynh_find_port --port=XYZ
# | arg: -p, --port= - port to check
# | exit: Return 1 if the port is already used by another process.
# | ret: 0 if the port is available, 1 if it is already used by another process.
#
# example: ynh_port_available --port=1234 || ynh_die --message="Port 1234 is needs to be available for this app"
#
# Requires YunoHost version 3.8.0 or higher.
ynh_port_available () {
@ -89,12 +89,12 @@ EOF
# Validate an IPv4 address
#
# example: ynh_validate_ip4 111.222.333.444
#
# usage: ynh_validate_ip4 --ip_address=ip_address
# | arg: -i, --ip_address= - the ipv4 address to check
# | ret: 0 for valid ipv4 addresses, 1 otherwise
#
# example: ynh_validate_ip4 111.222.333.444
#
# Requires YunoHost version 2.2.4 or higher.
ynh_validate_ip4()
{
@ -111,12 +111,12 @@ ynh_validate_ip4()
# Validate an IPv6 address
#
# example: ynh_validate_ip6 2000:dead:beef::1
#
# usage: ynh_validate_ip6 --ip_address=ip_address
# | arg: -i, --ip_address= - the ipv6 address to check
# | arg: -i, --ip_address= - the ipv6 address to check
# | ret: 0 for valid ipv6 addresses, 1 otherwise
#
# example: ynh_validate_ip6 2000:dead:beef::1
#
# Requires YunoHost version 2.2.4 or higher.
ynh_validate_ip6()
{

View file

@ -2,69 +2,33 @@
# Create a dedicated nginx config
#
# usage: ynh_add_nginx_config "list of others variables to replace"
# usage: ynh_add_nginx_config
#
# | arg: list - (Optional) list of others variables to replace separated by spaces. For example : 'path_2 port_2 ...'
# This will use a template in `../conf/nginx.conf`
# See the documentation of `ynh_add_config` for a description of the template
# format and how placeholders are replaced with actual variables.
#
# 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)
# 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* `'/'`
#
# And dynamic variables (from the last example) :
# __PATH_2__ by $path_2
# __PORT_2__ by $port_2
# 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

@ -1,6 +1,6 @@
#!/bin/bash
n_version=6.7.0
n_version=7.0.2
n_install_dir="/opt/node_n"
node_version_path="$n_install_dir/n/versions/node"
# N_PREFIX is the directory of n, it needs to be loaded as a environment variable.
@ -16,9 +16,8 @@ export N_PREFIX="$n_install_dir"
ynh_install_n () {
ynh_print_info --message="Installation of N - Node.js version management"
# Build an app.src for n
mkdir --parents "../conf"
echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz
SOURCE_SUM=92e00fa86d1c4e8dc6ca8df7e75fc93afe8f71949890ef67c40555df4efc4abe" > "../conf/n.src"
SOURCE_SUM=fa80a8685f0fb1b4187fc0a1228b44f0ea2f244e063fe8f443b8913ea595af89" > "$YNH_APP_BASEDIR/conf/n.src"
# Download and extract n
ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n
# Install n
@ -28,11 +27,14 @@ SOURCE_SUM=92e00fa86d1c4e8dc6ca8df7e75fc93afe8f71949890ef67c40555df4efc4abe" > "
# Load the version of node for an app, and set variables.
#
# ynh_use_nodejs has to be used in any app scripts before using node for the first time.
# usage: ynh_use_nodejs
#
# `ynh_use_nodejs` has to be used in any app scripts before using node for the first time.
# This helper will provide alias and variables to use in your scripts.
#
# To use npm or node, use the alias `ynh_npm` and `ynh_node`
# Those alias will use the correct version installed for the app
# To use npm or node, use the alias `ynh_npm` and `ynh_node`.
#
# Those alias will use the correct version installed for the app.
# For example: use `ynh_npm install` instead of `npm install`
#
# With `sudo` or `ynh_exec_as`, use instead the fallback variables `$ynh_npm` and `$ynh_node`
@ -40,30 +42,32 @@ SOURCE_SUM=92e00fa86d1c4e8dc6ca8df7e75fc93afe8f71949890ef67c40555df4efc4abe" > "
# Exemple: `ynh_exec_as $app $ynh_node_load_PATH $ynh_npm install`
#
# $PATH contains the path of the requested version of node.
# However, $PATH is duplicated into $node_PATH to outlast any manipulation of $PATH
# However, $PATH is duplicated into $node_PATH to outlast any manipulation of `$PATH`
# You can use the variable `$ynh_node_load_PATH` to quickly load your node version
# in $PATH for an usage into a separate script.
# in $PATH for an usage into a separate script.
# Exemple: $ynh_node_load_PATH $final_path/script_that_use_npm.sh`
#
#
# Finally, to start a nodejs service with the correct version, 2 solutions
# Either the app is dependent of node or npm, but does not called it directly.
# In such situation, you need to load PATH
# `Environment="__NODE_ENV_PATH__"`
# `ExecStart=__FINALPATH__/my_app`
# You will replace __NODE_ENV_PATH__ with $ynh_node_load_PATH
# In such situation, you need to load PATH :
# ```
# Environment="__NODE_ENV_PATH__"
# ExecStart=__FINALPATH__/my_app
# ```
# You will replace __NODE_ENV_PATH__ with $ynh_node_load_PATH.
#
# Or node start the app directly, then you don't need to load the PATH variable
# `ExecStart=__YNH_NODE__ my_app run`
# You will replace __YNH_NODE__ with $ynh_node
# ```
# ExecStart=__YNH_NODE__ my_app run
# ```
# You will replace __YNH_NODE__ with $ynh_node
#
#
# 2 other variables are also available
# - $nodejs_path: The absolute path to node binaries for the chosen version.
# - $nodejs_version: Just the version number of node for this app. Stored as 'nodejs_version' in settings.yml.
#
# usage: ynh_use_nodejs
#
# Requires YunoHost version 2.7.12 or higher.
ynh_use_nodejs () {
nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version)
@ -97,10 +101,10 @@ ynh_use_nodejs () {
# usage: ynh_install_nodejs --nodejs_version=nodejs_version
# | arg: -n, --nodejs_version= - Version of node to install. When possible, your should prefer to use major version number (e.g. 8 instead of 8.10.0). The crontab will then handle the update of minor versions when needed.
#
# n (Node version management) uses the PATH variable to store the path of the version of node it is going to use.
# `n` (Node version management) uses the `PATH` variable to store the path of the version of node it is going to use.
# That's how it changes the version
#
# Refer to ynh_use_nodejs for more information about available commands and variables
# Refer to `ynh_use_nodejs` for more information about available commands and variables
#
# Requires YunoHost version 2.7.12 or higher.
ynh_install_nodejs () {
@ -177,12 +181,12 @@ ynh_install_nodejs () {
# Remove the version of node used by the app.
#
# This helper will check if another app uses the same version of node,
# if not, this version of node will be removed.
# If no other app uses node, n will be also removed.
#
# usage: ynh_remove_nodejs
#
# This helper will check if another app uses the same version of node.
# - If not, this version of node will be removed.
# - If no other app uses node, n will be also removed.
#
# Requires YunoHost version 2.7.12 or higher.
ynh_remove_nodejs () {
nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version)

403
data/helpers.d/permission Normal file
View file

@ -0,0 +1,403 @@
#!/bin/bash
# Create a new permission for the app
#
# Example 1: `ynh_permission_create --permission=admin --url=/admin --additional_urls=domain.tld/admin /superadmin --allowed=alice bob \
# --label="My app admin" --show_tile=true`
#
# This example will create a new permission permission with this following effect:
# - A tile named "My app admin" in the SSO will be available for the users alice and bob. This tile will point to the relative url '/admin'.
# - Only the user alice and bob will have the access to theses following url: /admin, domain.tld/admin, /superadmin
#
#
# Example 2:
#
# ynh_permission_create --permission=api --url=domain.tld/api --auth_header=false --allowed=visitors \
# --label="MyApp API" --protected=true
#
# This example will create a new protected permission. So the admin won't be able to add/remove the visitors group of this permission.
# In case of an API with need to be always public it avoid that the admin break anything.
# With this permission all client will be allowed to access to the url 'domain.tld/api'.
# Note that in this case no tile will be show on the SSO.
# Note that the auth_header parameter is to 'false'. So no authentication header will be passed to the application.
# Generally the API is requested by an application and enabling the auth_header has no advantage and could bring some issues in some case.
# So in this case it's better to disable this option for all API.
#
#
# 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. 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:
# / -> domain.tld/app
# /admin -> domain.tld/app/admin
# domain.tld/app/api -> domain.tld/app/api
#
# 'url' or 'additional_urls' can be treated as a PCRE (not lua) regex if it starts with "re:".
# For example:
# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
#
# Note that globally the parameter 'url' and 'additional_urls' are same. The only difference is:
# - 'url' is only one url, 'additional_urls' can be a list of urls. There are no limitation of 'additional_urls'
# - 'url' is used for the url of tile in the SSO (if enabled with the 'show_tile' parameter)
#
#
# About the authentication header (auth_header parameter).
# The SSO pass (by default) to the application theses following HTTP header (linked to the authenticated user) to the application:
# - "Auth-User": username
# - "Remote-User": username
# - "Email": user email
#
# Generally this feature is usefull to authenticate automatically the user in the application but in some case the application don't work with theses header and theses header need to be disabled to have the application to work correctly.
# See https://github.com/YunoHost/issues/issues/1420 for more informations
#
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_create() {
# Declare an array to define the options of this helper.
local legacy_args=puAhaltP
local -A args_array=( [p]=permission= [u]=url= [A]=additional_urls= [h]=auth_header= [a]=allowed= [l]=label= [t]=show_tile= [P]=protected= )
local permission
local url
local additional_urls
local auth_header
local allowed
local label
local show_tile
local protected
ynh_handle_getopts_args "$@"
url=${url:-}
additional_urls=${additional_urls:-}
auth_header=${auth_header:-}
allowed=${allowed:-}
label=${label:-}
show_tile=${show_tile:-}
protected=${protected:-}
if [[ -n $url ]]
then
url=",url='$url'"
fi
if [[ -n $additional_urls ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# By example:
# --additional_urls /urlA /urlB
# will be:
# additional_urls=['/urlA', '/urlB']
additional_urls=",additional_urls=['${additional_urls//;/\',\'}']"
fi
if [[ -n $auth_header ]]
then
if [ $auth_header == "true" ]
then
auth_header=",auth_header=True"
else
auth_header=",auth_header=False"
fi
fi
if [[ -n $allowed ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# By example:
# --allowed alice bob
# will be:
# allowed=['alice', 'bob']
allowed=",allowed=['${allowed//;/\',\'}']"
fi
if [[ -n ${label:-} ]]; then
label=",label='$label'"
else
label=",label='$permission'"
fi
if [[ -n ${show_tile:-} ]]
then
if [ $show_tile == "true" ]
then
show_tile=",show_tile=True"
else
show_tile=",show_tile=False"
fi
fi
if [[ -n ${protected:-} ]]
then
if [ $protected == "true" ]
then
protected=",protected=True"
else
protected=",protected=False"
fi
fi
yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission' $url $additional_urls $auth_header $allowed $label $show_tile $protected)"
}
# Remove a permission for the app (note that when the app is removed all permission is automatically removed)
#
# example: ynh_permission_delete --permission=editors
#
# usage: ynh_permission_delete --permission="permission"
# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed)
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_delete() {
# Declare an array to define the options of this helper.
local legacy_args=p
local -A args_array=( [p]=permission= )
local permission
ynh_handle_getopts_args "$@"
yunohost tools shell -c "from yunohost.permission import permission_delete; permission_delete('$app.$permission')"
}
# Check if a permission exists
#
# usage: ynh_permission_exists --permission=permission
# | arg: -p, --permission= - the permission to check
# | exit: Return 1 if the permission doesn't exist, 0 otherwise
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_exists() {
# Declare an array to define the options of this helper.
local legacy_args=p
local -A args_array=( [p]=permission= )
local permission
ynh_handle_getopts_args "$@"
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)
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_url() {
# Declare an array to define the options of this helper.
local legacy_args=puarhc
local -A args_array=( [p]=permission= [u]=url= [a]=add_url= [r]=remove_url= [h]=auth_header= [c]=clear_urls )
local permission
local url
local add_url
local remove_url
local auth_header
local clear_urls
ynh_handle_getopts_args "$@"
url=${url:-}
add_url=${add_url:-}
remove_url=${remove_url:-}
auth_header=${auth_header:-}
clear_urls=${clear_urls:-}
if [[ -n $url ]]
then
url=",url='$url'"
fi
if [[ -n $add_url ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --add_url /urlA /urlB
# will be:
# add_url=['/urlA', '/urlB']
add_url=",add_url=['${add_url//;/\',\'}']"
fi
if [[ -n $remove_url ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --remove_url /urlA /urlB
# will be:
# remove_url=['/urlA', '/urlB']
remove_url=",remove_url=['${remove_url//;/\',\'}']"
fi
if [[ -n $auth_header ]]
then
if [ $auth_header == "true" ]
then
auth_header=",auth_header=True"
else
auth_header=",auth_header=False"
fi
fi
if [[ -n $clear_urls ]] && [ $clear_urls -eq 1 ]
then
clear_urls=",clear_urls=True"
fi
yunohost tools shell -c "from yunohost.permission import permission_url; permission_url('$app.$permission' $url $add_url $remove_url $auth_header $clear_urls)"
}
# Update a permission for the app
#
# 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.
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_update() {
# Declare an array to define the options of this helper.
local legacy_args=parltP
local -A args_array=( [p]=permission= [a]=add= [r]=remove= [l]=label= [t]=show_tile= [P]=protected= )
local permission
local add
local remove
local label
local show_tile
local protected
ynh_handle_getopts_args "$@"
add=${add:-}
remove=${remove:-}
label=${label:-}
show_tile=${show_tile:-}
protected=${protected:-}
if [[ -n $add ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --add alice bob
# will be:
# add=['alice', 'bob']
add=",add=['${add//';'/"','"}']"
fi
if [[ -n $remove ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --remove alice bob
# will be:
# remove=['alice', 'bob']
remove=",remove=['${remove//';'/"','"}']"
fi
if [[ -n $label ]]
then
label=",label='$label'"
fi
if [[ -n $show_tile ]]
then
if [ $show_tile == "true" ]
then
show_tile=",show_tile=True"
else
show_tile=",show_tile=False"
fi
fi
if [[ -n $protected ]]; then
if [ $protected == "true" ]
then
protected=",protected=True"
else
protected=",protected=False"
fi
fi
yunohost tools shell -c "from yunohost.permission import user_permission_update; user_permission_update('$app.$permission' $add $remove $label $show_tile $protected , force=True)"
}
# Check if a permission has an user
#
# example: ynh_permission_has_user --permission=main --user=visitors
#
# usage: ynh_permission_has_user --permission=permission --user=user
# | arg: -p, --permission= - the permission to check
# | arg: -u, --user= - the user seek in the permission
# | exit: Return 1 if the permission doesn't have that user or doesn't exist, 0 otherwise
#
# Requires YunoHost version 3.7.1 or higher.
ynh_permission_has_user() {
local legacy_args=pu
# Declare an array to define the options of this helper.
local -A args_array=( [p]=permission= [u]=user= )
local permission
local user
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
if ! ynh_permission_exists --permission=$permission
then
return 1
fi
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
#
# usage: ynh_legacy_permissions_exists
# | exit: Return 1 if the permission doesn't exist, 0 otherwise
#
# Requires YunoHost version 4.1.2 or higher.
ynh_legacy_permissions_exists () {
for permission in "skipped" "unprotected" "protected"
do
if ynh_permission_exists --permission="legacy_${permission}_uris"; then
return 0
fi
done
return 1
}
# Remove all legacy permissions
#
# usage: ynh_legacy_permissions_delete_all
#
# example:
# if ynh_legacy_permissions_exists
# then
# ynh_legacy_permissions_delete_all
# # You can recreate the required permissions here with ynh_permission_create
# fi
# Requires YunoHost version 4.1.2 or higher.
ynh_legacy_permissions_delete_all () {
for permission in "skipped" "unprotected" "protected"
do
if ynh_permission_exists --permission="legacy_${permission}_uris"; then
ynh_permission_delete --permission="legacy_${permission}_uris"
fi
done
}

View file

@ -85,6 +85,19 @@ ynh_add_fpm_config () {
# Set the default PHP-FPM version by default
phpversion="${phpversion:-$YNH_PHP_VERSION}"
local old_phpversion=$(ynh_app_setting_get --app=$app --key=phpversion)
# If the PHP version changed, remove the old fpm conf
if [ -n "$old_phpversion" ] && [ "$old_phpversion" != "$phpversion" ]
then
local old_php_fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir)
local old_php_finalphpconf="$old_php_fpm_config_dir/pool.d/$app.conf"
ynh_backup_if_checksum_is_different --file="$old_php_finalphpconf"
ynh_remove_fpm_config
fi
# If the requested PHP version is not the default version for YunoHost
if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ]
then
@ -119,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 ]
@ -138,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
@ -165,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
@ -257,12 +254,12 @@ WantedBy=multi-user.target
ynh_systemd_action --service_name=$fpm_service --action=restart
else
# Validate that the new php conf doesn't break php-fpm entirely
php-fpm${phpversion} --test 2>/dev/null \
&& ynh_systemd_action --service_name=$fpm_service --action=reload \
|| { php-fpm${phpversion} --test || true;
ynh_secure_remove --file="$finalphpconf";
ynh_die --message="The new configuration broke php-fpm?"
}
if ! php-fpm${phpversion} --test 2>/dev/null
then
php-fpm${phpversion} --test || true
ynh_secure_remove --file="$finalphpconf"
ynh_die --message="The new configuration broke php-fpm?"
fi
ynh_systemd_action --service_name=$fpm_service --action=reload
fi
}
@ -278,7 +275,7 @@ ynh_remove_fpm_config () {
local dedicated_service=$(ynh_app_setting_get --app=$app --key=fpm_dedicated_service)
dedicated_service=${dedicated_service:-0}
# Get the version of PHP used by this app
local phpversion=$(ynh_app_setting_get $app phpversion)
local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion)
# Assume default PHP-FPM version by default
phpversion="${phpversion:-$YNH_DEFAULT_PHP_VERSION}"
@ -340,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
@ -354,7 +351,7 @@ ynh_install_php () {
fi
# Add an extra repository for those packages
ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --priority=995 --name=extra_php_version --priority=600
ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600
# Install requested dependencies from this extra repository.
# Install PHP-FPM first, otherwise PHP will install apache as a dependency.
@ -377,7 +374,7 @@ ynh_install_php () {
# Requires YunoHost version 3.8.1 or higher.
ynh_remove_php () {
# Get the version of PHP used by this app
local phpversion=$(ynh_app_setting_get $app phpversion)
local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion)
if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] || [ -z "$phpversion" ]
then
@ -560,3 +557,65 @@ ynh_get_scalable_phpfpm () {
fi
fi
}
readonly YNH_DEFAULT_COMPOSER_VERSION=1.10.17
# Declare the actual composer version to use.
# A packager willing to use another version of composer can override the variable into its _common.sh.
YNH_COMPOSER_VERSION=${YNH_COMPOSER_VERSION:-$YNH_DEFAULT_COMPOSER_VERSION}
# Execute a command with Composer
#
# usage: ynh_composer_exec [--phpversion=phpversion] [--workdir=$final_path] --commands="commands"
# | arg: -v, --phpversion - PHP version to use with composer
# | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path.
# | arg: -c, --commands - Commands to execute.
#
ynh_composer_exec () {
# Declare an array to define the options of this helper.
local legacy_args=vwc
declare -Ar args_array=( [v]=phpversion= [w]=workdir= [c]=commands= )
local phpversion
local workdir
local commands
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
workdir="${workdir:-$final_path}"
phpversion="${phpversion:-$YNH_PHP_VERSION}"
COMPOSER_HOME="$workdir/.composer" \
php${phpversion} "$workdir/composer.phar" $commands \
-d "$workdir" --quiet --no-interaction
}
# Install and initialize Composer in the given directory
#
# usage: ynh_install_composer [--phpversion=phpversion] [--workdir=$final_path] [--install_args="--optimize-autoloader"] [--composerversion=composerversion]
# | arg: -v, --phpversion - PHP version to use with composer
# | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path.
# | arg: -a, --install_args - Additional arguments provided to the composer install. Argument --no-dev already include
# | arg: -c, --composerversion - Composer version to install
#
ynh_install_composer () {
# Declare an array to define the options of this helper.
local legacy_args=vwac
declare -Ar args_array=( [v]=phpversion= [w]=workdir= [a]=install_args= [c]=composerversion=)
local phpversion
local workdir
local install_args
local composerversion
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
workdir="${workdir:-$final_path}"
phpversion="${phpversion:-$YNH_PHP_VERSION}"
install_args="${install_args:-}"
composerversion="${composerversion:-$YNH_COMPOSER_VERSION}"
curl -sS https://getcomposer.org/installer \
| COMPOSER_HOME="$workdir/.composer" \
php${phpversion} -- --quiet --install-dir="$workdir" --version=$composerversion \
|| ynh_die --message="Unable to install Composer."
# install dependencies
ynh_composer_exec --phpversion="${phpversion}" --workdir="$workdir" --commands="install --no-dev $install_args" \
|| ynh_die --message="Unable to install core dependencies with Composer."
}

View file

@ -5,15 +5,15 @@ PSQL_VERSION=11
# Open a connection as a user
#
# examples:
# ynh_psql_connect_as 'user' 'pass' <<< "UPDATE ...;"
# ynh_psql_connect_as 'user' 'pass' < /path/to/file.sql
#
# usage: ynh_psql_connect_as --user=user --password=password [--database=database]
# | arg: -u, --user= - the user name to connect as
# | arg: -p, --password= - the user password
# | arg: -d, --database= - the database to connect to
#
# examples:
# ynh_psql_connect_as 'user' 'pass' <<< "UPDATE ...;"
# ynh_psql_connect_as 'user' 'pass' < /path/to/file.sql
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_connect_as() {
# Declare an array to define the options of this helper.
@ -127,12 +127,12 @@ ynh_psql_drop_db() {
# Dump a database
#
# example: ynh_psql_dump_db 'roundcube' > ./dump.sql
#
# usage: ynh_psql_dump_db --database=database
# | arg: -d, --database= - the database name to dump
# | ret: the psqldump output
#
# example: ynh_psql_dump_db 'roundcube' > ./dump.sql
#
# Requires YunoHost version 3.5.0 or higher.
ynh_psql_dump_db() {
# Declare an array to define the options of this helper.
@ -243,7 +243,7 @@ ynh_psql_setup_db() {
local new_db_pwd=$(ynh_string_random) # Generate a random password
# If $db_pwd is not provided, use new_db_pwd instead for db_pwd
db_pwd="${db_pwd:-$new_db_pwd}"
ynh_psql_create_user "$db_user" "$db_pwd"
elif [ -z $db_pwd ]; then
ynh_die --message="The user $db_user exists, please provide his password"
@ -286,19 +286,20 @@ ynh_psql_remove_db() {
}
# Create a master password and set up global settings
# It also make sure that postgresql is installed and running
# Please always call this script in install and restore scripts
#
# usage: ynh_psql_test_if_first_run
#
# It also make sure that postgresql is installed and running
# Please always call this script in install and restore scripts
#
# Requires YunoHost version 2.7.13 or higher.
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

@ -78,7 +78,8 @@ ynh_app_setting_delete() {
#
ynh_app_setting()
{
ACTION="$1" APP="$2" KEY="$3" VALUE="${4:-}" python2.7 - <<EOF
set +o xtrace # set +x
ACTION="$1" APP="$2" KEY="$3" VALUE="${4:-}" python3 - <<EOF
import os, yaml, sys
app, action = os.environ['APP'], os.environ['ACTION'].lower()
key, value = os.environ['KEY'], os.environ.get('VALUE', None)
@ -102,16 +103,17 @@ else:
with open(setting_file, "w") as f:
yaml.safe_dump(settings, f, default_flow_style=False)
EOF
set -o xtrace # set -x
}
# Check availability of a web path
#
# example: ynh_webpath_available --domain=some.domain.tld --path_url=/coffee
#
# usage: ynh_webpath_available --domain=domain --path_url=path
# | arg: -d, --domain= - the domain/host of the url
# | arg: -p, --path_url= - the web path to check the availability of
#
# example: ynh_webpath_available --domain=some.domain.tld --path_url=/coffee
#
# Requires YunoHost version 2.6.4 or higher.
ynh_webpath_available () {
# Declare an array to define the options of this helper.
@ -127,13 +129,13 @@ ynh_webpath_available () {
# Register/book a web path for an app
#
# example: ynh_webpath_register --app=wordpress --domain=some.domain.tld --path_url=/coffee
#
# usage: ynh_webpath_register --app=app --domain=domain --path_url=path
# | arg: -a, --app= - the app for which the domain should be registered
# | arg: -d, --domain= - the domain/host of the web path
# | arg: -p, --path_url= - the web path to be registered
#
# example: ynh_webpath_register --app=wordpress --domain=some.domain.tld --path_url=/coffee
#
# Requires YunoHost version 2.6.4 or higher.
ynh_webpath_register () {
# Declare an array to define the options of this helper.
@ -147,372 +149,3 @@ ynh_webpath_register () {
yunohost app register-url $app $domain $path_url
}
# Create a new permission for the app
#
# example 1: ynh_permission_create --permission=admin --url=/admin --additional_urls=domain.tld/admin /superadmin --allowed=alice bob \
# --label="My app admin" --show_tile=true
#
# This example will create a new permission permission with this following effect:
# - A tile named "My app admin" in the SSO will be available for the users alice and bob. This tile will point to the relative url '/admin'.
# - Only the user alice and bob will have the access to theses following url: /admin, domain.tld/admin, /superadmin
#
#
# example 2: ynh_permission_create --permission=api --url=domain.tld/api --auth_header=false --allowed=visitors \
# --label="MyApp API" --protected=true
#
# This example will create a new protected permission. So the admin won't be able to add/remove the visitors group of this permission.
# In case of an API with need to be always public it avoid that the admin break anything.
# With this permission all client will be allowed to access to the url 'domain.tld/api'.
# Note that in this case no tile will be show on the SSO.
# Note that the auth_header parameter is to 'false'. So no authentication header will be passed to the application.
# Generally the API is requested by an application and enabling the auth_header has no advantage and could bring some issues in some case.
# So in this case it's better to disable this option for all API.
#
#
# 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'
#
# If provided, 'url' or 'additional_urls' is assumed to be relative to the app domain/path if they
# start with '/'. For example:
# / -> domain.tld/app
# /admin -> domain.tld/app/admin
# domain.tld/app/api -> domain.tld/app/api
#
# 'url' or 'additional_urls' can be treated as a PCRE (not lua) regex if it starts with "re:".
# For example:
# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
#
# Note that globally the parameter 'url' and 'additional_urls' are same. The only difference is:
# - 'url' is only one url, 'additional_urls' can be a list of urls. There are no limitation of 'additional_urls'
# - 'url' is used for the url of tile in the SSO (if enabled with the 'show_tile' parameter)
#
#
# About the authentication header (auth_header parameter).
# The SSO pass (by default) to the application theses following HTTP header (linked to the authenticated user) to the application:
# - "Auth-User": username
# - "Remote-User": username
# - "Email": user email
#
# Generally this feature is usefull to authenticate automatically the user in the application but in some case the application don't work with theses header and theses header need to be disabled to have the application to work correctly.
# See https://github.com/YunoHost/issues/issues/1420 for more informations
#
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_create() {
# Declare an array to define the options of this helper.
local legacy_args=puAhaltP
local -A args_array=( [p]=permission= [u]=url= [A]=additional_urls= [h]=auth_header= [a]=allowed= [l]=label= [t]=show_tile= [P]=protected= )
local permission
local url
local additional_urls
local auth_header
local allowed
local label
local show_tile
local protected
ynh_handle_getopts_args "$@"
url=${url:-}
additional_urls=${additional_urls:-}
auth_header=${auth_header:-}
allowed=${allowed:-}
label=${label:-}
show_tile=${show_tile:-}
protected=${protected:-}
if [[ -n $url ]]
then
url=",url='$url'"
fi
if [[ -n $additional_urls ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# By example:
# --additional_urls /urlA /urlB
# will be:
# additional_urls=['/urlA', '/urlB']
additional_urls=",additional_urls=['${additional_urls//;/\',\'}']"
fi
if [[ -n $auth_header ]]
then
if [ $auth_header == "true" ]
then
auth_header=",auth_header=True"
else
auth_header=",auth_header=False"
fi
fi
if [[ -n $allowed ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# By example:
# --allowed alice bob
# will be:
# allowed=['alice', 'bob']
allowed=",allowed=['${allowed//;/\',\'}']"
fi
if [[ -n ${label:-} ]]; then
label=",label='$label'"
else
label=",label='$permission'"
fi
if [[ -n ${show_tile:-} ]]
then
if [ $show_tile == "true" ]
then
show_tile=",show_tile=True"
else
show_tile=",show_tile=False"
fi
fi
if [[ -n ${protected:-} ]]
then
if [ $protected == "true" ]
then
protected=",protected=True"
else
protected=",protected=False"
fi
fi
yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission' $url $additional_urls $auth_header $allowed $label $show_tile $protected)"
}
# Remove a permission for the app (note that when the app is removed all permission is automatically removed)
#
# example: ynh_permission_delete --permission=editors
#
# usage: ynh_permission_delete --permission="permission"
# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed)
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_delete() {
# Declare an array to define the options of this helper.
local legacy_args=p
local -A args_array=( [p]=permission= )
local permission
ynh_handle_getopts_args "$@"
yunohost tools shell -c "from yunohost.permission import permission_delete; permission_delete('$app.$permission')"
}
# Check if a permission exists
#
# usage: ynh_permission_exists --permission=permission
# | arg: -p, --permission= - the permission to check
# | exit: Return 1 if the permission doesn't exist, 0 otherwise
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_exists() {
# Declare an array to define the options of this helper.
local legacy_args=p
local -A args_array=( [p]=permission= )
local permission
ynh_handle_getopts_args "$@"
yunohost user permission list --short | grep --word-regexp --quiet "$app.$permission"
}
# 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)
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_url() {
# Declare an array to define the options of this helper.
local legacy_args=puarhc
local -A args_array=( [p]=permission= [u]=url= [a]=add_url= [r]=remove_url= [h]=auth_header= [c]=clear_urls )
local permission
local url
local add_url
local remove_url
local auth_header
local clear_urls
ynh_handle_getopts_args "$@"
url=${url:-}
add_url=${add_url:-}
remove_url=${remove_url:-}
auth_header=${auth_header:-}
clear_urls=${clear_urls:-}
if [[ -n $url ]]
then
url=",url='$url'"
fi
if [[ -n $add_url ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --add_url /urlA /urlB
# will be:
# add_url=['/urlA', '/urlB']
add_url=",add_url=['${add_url//;/\',\'}']"
fi
if [[ -n $remove_url ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --remove_url /urlA /urlB
# will be:
# remove_url=['/urlA', '/urlB']
remove_url=",remove_url=['${remove_url//;/\',\'}']"
fi
if [[ -n $auth_header ]]
then
if [ $auth_header == "true" ]
then
auth_header=",auth_header=True"
else
auth_header=",auth_header=False"
fi
fi
if [[ -n $clear_urls ]] && [ $clear_urls -eq 1 ]
then
clear_urls=",clear_urls=True"
fi
yunohost tools shell -c "from yunohost.permission import permission_url; permission_url('$app.$permission' $url $add_url $remove_url $auth_header $clear_urls)"
}
# Update a permission for the app
#
# 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.
#
# Requires YunoHost version 3.7.0 or higher.
ynh_permission_update() {
# Declare an array to define the options of this helper.
local legacy_args=parltP
local -A args_array=( [p]=permission= [a]=add= [r]=remove= [l]=label= [t]=show_tile= [P]=protected= )
local permission
local add
local remove
local label
local show_tile
local protected
ynh_handle_getopts_args "$@"
add=${add:-}
remove=${remove:-}
label=${label:-}
show_tile=${show_tile:-}
protected=${protected:-}
if [[ -n $add ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --add alice bob
# will be:
# add=['alice', 'bob']
add=",add=['${add//';'/"','"}']"
fi
if [[ -n $remove ]]
then
# Convert a list from getopts to python list
# Note that getopts separate the args with ';'
# For example:
# --remove alice bob
# will be:
# remove=['alice', 'bob']
remove=",remove=['${remove//';'/"','"}']"
fi
if [[ -n $label ]]
then
label=",label='$label'"
fi
if [[ -n $show_tile ]]
then
if [ $show_tile == "true" ]
then
show_tile=",show_tile=True"
else
show_tile=",show_tile=False"
fi
fi
if [[ -n $protected ]]; then
if [ $protected == "true" ]
then
protected=",protected=True"
else
protected=",protected=False"
fi
fi
yunohost tools shell -c "from yunohost.permission import user_permission_update; user_permission_update('$app.$permission' $add $remove $label $show_tile $protected , force=True)"
}
# Check if a permission has an user
#
# example: ynh_permission_has_user --permission=main --user=visitors
#
# usage: ynh_permission_has_user --permission=permission --user=user
# | arg: -p, --permission= - the permission to check
# | arg: -u, --user= - the user seek in the permission
# | exit: Return 1 if the permission doesn't have that user or doesn't exist, 0 otherwise
#
# Requires YunoHost version 3.7.1 or higher.
ynh_permission_has_user() {
local legacy_args=pu
# Declare an array to define the options of this helper.
local -A args_array=( [p]=permission= [u]=user= )
local permission
local user
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
if ! ynh_permission_exists --permission=$permission
then
return 1
fi
yunohost user permission info "$app.$permission" | grep --word-regexp --quiet "$user"
}

View file

@ -2,12 +2,12 @@
# Generate a random string
#
# example: pwd=$(ynh_string_random --length=8)
#
# usage: ynh_string_random [--length=string_length]
# | arg: -l, --length= - the string length to generate (default: 24)
# | ret: the generated string
#
# example: pwd=$(ynh_string_random --length=8)
#
# Requires YunoHost version 2.2.4 or higher.
ynh_string_random() {
# Declare an array to define the options of this helper.
@ -30,9 +30,8 @@ ynh_string_random() {
# | arg: -r, --replace_string= - String that will replace matches
# | arg: -f, --target_file= - File in which the string will be replaced.
#
# As this helper is based on sed command, regular expressions and
# references to sub-expressions can be used
# (see sed manual page for more information)
# As this helper is based on sed command, regular expressions and references to
# sub-expressions can be used (see sed manual page for more information)
#
# Requires YunoHost version 2.6.4 or higher.
ynh_replace_string () {
@ -86,14 +85,15 @@ ynh_replace_special_string () {
}
# Sanitize a string intended to be the name of a database
# (More specifically : replace - and . by _)
#
# example: dbname=$(ynh_sanitize_dbid $app)
#
# usage: ynh_sanitize_dbid --db_name=name
# | arg: -n, --db_name= - name to correct/sanitize
# | ret: the corrected name
#
# example: dbname=$(ynh_sanitize_dbid $app)
#
# Underscorify the string (replace - and . by _)
#
# Requires YunoHost version 2.2.4 or higher.
ynh_sanitize_dbid () {
# Declare an array to define the options of this helper.

View file

@ -3,61 +3,32 @@
# 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 ...'
# | 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)
#
# 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 :
# This will use the template `../conf/<templatename>.service`.
#
# __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 -A args_array=( [s]=service= [t]=template= [v]=others_var=)
local service
local template
local others_var
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
local service="${service:-$app}"
local template="${template:-systemd.service}"
service="${service:-$app}"
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"
[[ -z "$others_var" ]] || ynh_print_warn --message="Packagers: using --others_var is unecessary since Yunohost 4.2"
# 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
ynh_add_config --template="$YNH_APP_BASEDIR/conf/$template" --destination="/etc/systemd/system/$service.service"
# 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
}
@ -90,10 +61,10 @@ ynh_remove_systemd_config () {
# Start (or other actions) a service, print a log in case of failure and optionnaly wait until the service is completely started
#
# 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: -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: -p, --log_path= - Log file - Path to the log file. Default : /var/log/$app/$app.log
# | 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
#
@ -149,11 +120,10 @@ ynh_systemd_action() {
# If a log is specified for this service, show also the content of this log
if [ -e "$log_path" ]
then
ynh_print_err --message="--"
ynh_exec_err tail --lines=$length "$log_path"
fi
# Fail the app script, since the service failed.
ynh_die
ynh_clean_check_starting
return 1
fi
# Start the timeout and try to find line_match
@ -197,20 +167,18 @@ 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 () {
if [ -n "$pid_tail" ]
if [ -n "${pid_tail:-}" ]
then
# Stop the execution of tail.
kill -SIGTERM $pid_tail 2>&1
fi
if [ -n "$templog" ]
if [ -n "${templog:-}" ]
then
ynh_secure_remove "$templog" 2>&1
ynh_secure_remove --file="$templog" 2>&1
fi
}

View file

@ -2,11 +2,11 @@
# Check if a YunoHost user exists
#
# example: ynh_user_exists 'toto' || exit 1
#
# usage: ynh_user_exists --username=username
# | arg: -u, --username= - the username to check
# | exit: Return 1 if the user doesn't exist, 0 otherwise
# | ret: 0 if the user exists, 1 otherwise.
#
# example: ynh_user_exists 'toto' || echo "User does not exist"
#
# Requires YunoHost version 2.2.4 or higher.
ynh_user_exists() {
@ -17,17 +17,17 @@ 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
#
# example: mail=$(ynh_user_get_info 'toto' 'mail')
#
# usage: ynh_user_get_info --username=username --key=key
# | arg: -u, --username= - the username to retrieve info from
# | arg: -k, --key= - the key to retrieve
# | ret: string - the key's value
# | ret: the value associate to that key
#
# example: mail=$(ynh_user_get_info 'toto' 'mail')
#
# Requires YunoHost version 2.2.4 or higher.
ynh_user_get_info() {
@ -39,27 +39,26 @@ 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
#
# example: for u in $(ynh_user_list); do ...
#
# usage: ynh_user_list
# | ret: string - one username per line
# | ret: one username per line as strings
#
# example: for u in $(ynh_user_list); do ... ; done
#
# 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
#
# usage: ynh_system_user_exists --username=username
# | arg: -u, --username= - the username to check
# | exit: Return 1 if the user doesn't exist, 0 otherwise
# | ret: 0 if the user exists, 1 otherwise.
#
# Requires YunoHost version 2.2.4 or higher.
ynh_system_user_exists() {
@ -77,7 +76,7 @@ ynh_system_user_exists() {
#
# usage: ynh_system_group_exists --group=group
# | arg: -g, --group= - the group to check
# | exit: Return 1 if the group doesn't exist, 0 otherwise
# | ret: 0 if the group exists, 1 otherwise.
#
# Requires YunoHost version 3.5.0.2 or higher.
ynh_system_group_exists() {
@ -93,17 +92,20 @@ ynh_system_group_exists() {
# Create a system user
#
# examples:
# # Create a nextcloud user with no home directory and /usr/sbin/nologin login shell (hence no login capability)
# ynh_system_user_create --username=nextcloud
# # Create a discourse user using /var/www/discourse as home directory and the default login shell
# ynh_system_user_create --username=discourse --home_dir=/var/www/discourse --use_shell
#
# usage: ynh_system_user_create --username=user_name [--home_dir=home_dir] [--use_shell]
# | arg: -u, --username= - Name of the system user that will be create
# | arg: -h, --home_dir= - Path of the home dir for the user. Usually the final path of the app. If this argument is omitted, the user will be created without home
# | arg: -s, --use_shell - Create a user using the default login shell if present. If this argument is omitted, the user will be created with /usr/sbin/nologin shell
#
# Create a nextcloud user with no home directory and /usr/sbin/nologin login shell (hence no login capability) :
# ```
# ynh_system_user_create --username=nextcloud
# ```
# Create a discourse user using /var/www/discourse as home directory and the default login shell :
# ```
# ynh_system_user_create --username=discourse --home_dir=/var/www/discourse --use_shell
# ```
#
# Requires YunoHost version 2.6.4 or higher.
ynh_system_user_create () {
# Declare an array to define the options of this helper.
@ -163,3 +165,19 @@ ynh_system_user_delete () {
delgroup $username
fi
}
# Execute a command as another user
#
# usage: ynh_exec_as $USER COMMAND [ARG ...]
#
# Requires YunoHost version 4.1.7 or higher.
ynh_exec_as() {
local user=$1
shift 1
if [[ $user = $(whoami) ]]; then
eval "$@"
else
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]
@ -19,6 +21,9 @@
# Requires YunoHost version 2.6.4 or higher.
ynh_exit_properly () {
local exit_code=$?
rm -rf "/var/cache/yunohost/download/"
if [ "$exit_code" -eq 0 ]; then
exit 0 # Exit without error if the script ended correctly
fi
@ -46,9 +51,8 @@ ynh_exit_properly () {
# usage: ynh_abort_if_errors
#
# This configure the rest of the script execution such that, if an error occurs
# or if an empty variable is used, the execution of the script stops
# immediately and a call to `ynh_clean_setup` is triggered if it has been
# defined by your script.
# or if an empty variable is used, the execution of the script stops immediately
# and a call to `ynh_clean_setup` is triggered if it has been defined by your script.
#
# Requires YunoHost version 2.6.4 or higher.
ynh_abort_if_errors () {
@ -61,45 +65,37 @@ ynh_abort_if_errors () {
#
# usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id]
# | arg: -d, --dest_dir= - Directory where to setup sources
# | arg: -s, --source_id= - Name of the app, if the package contains more than one app
# | arg: -s, --source_id= - Name of the source, defaults to `app`
#
# The file conf/app.src need to contains:
# This helper will read `conf/${source_id}.src`, download and install the sources.
#
# The src file need to contains:
# ```
# SOURCE_URL=Address to download the app archive
# SOURCE_SUM=Control sum
# # (Optional) Program to check the integrity (sha256sum, md5sum...)
# # default: sha256
# # (Optional) Program to check the integrity (sha256sum, md5sum...). Default: sha256
# SOURCE_SUM_PRG=sha256
# # (Optional) Archive format
# # default: tar.gz
# # (Optional) Archive format. Default: tar.gz
# SOURCE_FORMAT=tar.gz
# # (Optional) Put false if sources are directly in the archive root
# # default: true
# # Instead of true, SOURCE_IN_SUBDIR could be the number of sub directories
# # to remove.
# # (Optional) Put false if sources are directly in the archive root. Default: true
# # Instead of true, SOURCE_IN_SUBDIR could be the number of sub directories to remove.
# SOURCE_IN_SUBDIR=false
# # (Optionnal) Name of the local archive (offline setup support)
# # default: ${src_id}.${src_format}
# # (Optionnal) Name of the local archive (offline setup support). Default: ${src_id}.${src_format}
# SOURCE_FILENAME=example.tar.gz
# # (Optional) If it set as false don't extract the source.
# # (Optional) If it set as false don't extract the source. Default: true
# # (Useful to get a debian package or a python wheel.)
# # default: true
# SOURCE_EXTRACT=(true|false)
# ```
#
# Details:
# This helper downloads sources from SOURCE_URL if there is no local source
# archive in /opt/yunohost-apps-src/APP_ID/SOURCE_FILENAME
#
# Next, it checks the integrity with "SOURCE_SUM_PRG -c --status" command.
#
# If it's ok, the source archive will be uncompressed in $dest_dir. If the
# SOURCE_IN_SUBDIR is true, the first level directory of the archive will be
# removed.
# If SOURCE_IN_SUBDIR is a numeric value, 2 for example, the 2 first level
# directories will be removed
#
# Finally, patches named sources/patches/${src_id}-*.patch and extra files in
# sources/extra_files/$src_id will be applied to dest_dir
# The helper will:
# - Check if there is a local source archive in `/opt/yunohost-apps-src/$APP_ID/$SOURCE_FILENAME`
# - Download `$SOURCE_URL` if there is no local archive
# - Check the integrity with `$SOURCE_SUM_PRG -c --status`
# - Uncompress the archive to `$dest_dir`.
# - If `$SOURCE_IN_SUBDIR` is true, the first level directory of the archive will be removed.
# - If `$SOURCE_IN_SUBDIR` is a numeric value, the N first level directories will be removed.
# - Patches named `sources/patches/${src_id}-*.patch` will be applied to `$dest_dir`
# - Extra files in `sources/extra_files/$src_id` will be copied to dest_dir
#
# Requires YunoHost version 2.6.4 or higher.
ynh_setup_source () {
@ -112,12 +108,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)
@ -138,12 +129,20 @@ ynh_setup_source () {
if [ "$src_filename" = "" ]; then
src_filename="${source_id}.${src_format}"
fi
# (Unused?) mecanism where one can have the file in a special local cache to not have to download it...
local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${src_filename}"
mkdir -p /var/cache/yunohost/download/${YNH_APP_ID}/
src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${src_filename}"
if test -e "$local_src"
then # Use the local source file if it is present
then
cp $local_src $src_filename
else # If not, download the source
else
[ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?"
# NB. we have to declare the var as local first,
# otherwise 'local foo=$(false) || echo 'pwet'" does'nt work
# because local always return 0 ...
@ -197,34 +196,34 @@ ynh_setup_source () {
fi
# Apply patches
if (( $(find $YNH_CWD/../sources/patches/ -type f -name "${source_id}-*.patch" 2> /dev/null | wc --lines) > "0" ))
if (( $(find $YNH_APP_BASEDIR/sources/patches/ -type f -name "${source_id}-*.patch" 2> /dev/null | wc --lines) > "0" ))
then
(cd "$dest_dir"
for p in $YNH_CWD/../sources/patches/${source_id}-*.patch
for p in $YNH_APP_BASEDIR/sources/patches/${source_id}-*.patch
do
patch --strip=1 < $p
done) || ynh_die --message="Unable to apply patches"
fi
# Add supplementary files
if test -e "$YNH_CWD/../sources/extra_files/${source_id}"; then
cp --archive $YNH_CWD/../sources/extra_files/$source_id/. "$dest_dir"
if test -e "$YNH_APP_BASEDIR/sources/extra_files/${source_id}"; then
cp --archive $YNH_APP_BASEDIR/sources/extra_files/$source_id/. "$dest_dir"
fi
}
# Curl abstraction to help with POST requests to local pages (such as installation forms)
#
# example: ynh_local_curl "/install.php?installButton" "foo=$var1" "bar=$var2"
#
# usage: ynh_local_curl "page_uri" "key1=value1" "key2=value2" ...
# | arg: page_uri - Path (relative to $path_url) of the page where POST data will be sent
# | arg: page_uri - Path (relative to `$path_url`) of the page where POST data will be sent
# | arg: key1=value1 - (Optionnal) POST key and corresponding value
# | arg: key2=value2 - (Optionnal) Another POST key and corresponding value
# | arg: ... - (Optionnal) More POST keys and values
#
# example: ynh_local_curl "/install.php?installButton" "foo=$var1" "bar=$var2"
#
# For multiple calls, cookies are persisted between each call for the same app
#
# $domain and $path_url should be defined externally (and correspond to the domain.tld and the /path (of the app?))
# `$domain` and `$path_url` should be defined externally (and correspond to the domain.tld and the /path (of the app?))
#
# Requires YunoHost version 2.6.4 or higher.
ynh_local_curl () {
@ -253,7 +252,7 @@ ynh_local_curl () {
# Wait untils nginx has fully reloaded (avoid curl fail with http2)
sleep 2
local cookiefile=/tmp/ynh-$app-cookie.txt
touch $cookiefile
chown root $cookiefile
@ -265,20 +264,22 @@ ynh_local_curl () {
# Create a dedicated config file from a template
#
# usage: ynh_add_config --template="template" --destination="destination"
# | arg: -t, --template= - Template config file to use
# | arg: -d, --destination= - Destination of the config file
#
# examples:
# ynh_add_config --template=".env" --destination="$final_path/.env"
# ynh_add_config --template="../conf/.env" --destination="$final_path/.env"
# ynh_add_config --template="/etc/nginx/sites-available/default" --destination="etc/nginx/sites-available/mydomain.conf"
#
# usage: ynh_add_config --template="template" --destination="destination"
# | arg: -t, --template= - Template config file to use
# | arg: -d, --destination= - Destination of the config file
#
# The template can be by default the name of a file in the conf directory
# of a YunoHost Package, a relative path or an absolute path
# The helper will use the template $template to generate a config file
# $destination by replacing the following keywords with global variables
# of a YunoHost Package, a relative path or an absolute path.
#
# The helper will use the template `template` to generate a config file
# `destination` by replacing the following keywords with global variables
# that should be defined before calling this helper :
# ```
# __PATH__ by $path_url
# __NAME__ by $app
# __NAMETOCHANGE__ by $app
@ -286,15 +287,18 @@ ynh_local_curl () {
# __FINALPATH__ by $final_path
# __PHPVERSION__ by $YNH_PHP_VERSION
# __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH
#
# ```
# And any dynamic variables that should be defined before calling this helper like:
# ```
# __DOMAIN__ by $domain
# __APP__ by $app
# __VAR_1__ by $var_1
# __VAR_2__ by $var_2
# ```
#
# The helper will verify the checksum and backup the destination file
# if it's different before applying the new template.
#
# And it will calculate and store the destination file checksum
# into the app settings when configuration is done.
#
@ -309,10 +313,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 +323,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,19 +389,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]+?[A-Z0-9_]*?[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
test -n "${!one_var:-}" || ynh_die --message="\$$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)
@ -421,7 +429,7 @@ ynh_render_template() {
local output_path=$2
mkdir -p "$(dirname $output_path)"
# Taken from https://stackoverflow.com/a/35009576
python2.7 -c 'import os, sys, jinja2; sys.stdout.write(
python3 -c 'import os, sys, jinja2; sys.stdout.write(
jinja2.Template(sys.stdin.read()
).render(os.environ));' < $template_path > $output_path
}
@ -502,12 +510,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
@ -554,22 +557,23 @@ ynh_read_manifest () {
if [ ! -e "$manifest" ]; then
# If the manifest isn't found, try the common place for backup and restore script.
manifest="../settings/manifest.json"
manifest="$YNH_APP_BASEDIR/manifest.json"
fi
jq ".$manifest_key" "$manifest" --raw-output
}
# Read the upstream version from the manifest, or from the env variable $YNH_APP_MANIFEST_VERSION if not given
# Read the upstream version from the manifest or `$YNH_APP_MANIFEST_VERSION`
#
# usage: ynh_app_upstream_version [--manifest="manifest.json"]
# | arg: -m, --manifest= - Path of the manifest to read
# | ret: the version number of the upstream app
#
# The version number in the manifest is defined by <upstreamversion>~ynh<packageversion>
# For example : 4.3-2~ynh3
# This include the number before ~ynh
# In the last example it return 4.3-2
# If the `manifest` is not specified, the envvar `$YNH_APP_MANIFEST_VERSION` will be used.
#
# The version number in the manifest is defined by `<upstreamversion>~ynh<packageversion>`.
#
# For example, if the manifest contains `4.3-2~ynh3` the function will return `4.3-2`
#
# Requires YunoHost version 3.5.0 or higher.
ynh_app_upstream_version () {
@ -583,12 +587,12 @@ ynh_app_upstream_version () {
if [[ "$manifest" != "" ]] && [[ -e "$manifest" ]];
then
version_key=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version")
version_key_=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version")
else
version_key=$YNH_APP_MANIFEST_VERSION
version_key_=$YNH_APP_MANIFEST_VERSION
fi
echo "${version_key/~ynh*/}"
echo "${version_key_/~ynh*/}"
}
# Read package version from the manifest
@ -597,10 +601,9 @@ ynh_app_upstream_version () {
# | arg: -m, --manifest= - Path of the manifest to read
# | ret: the version number of the package
#
# The version number in the manifest is defined by <upstreamversion>~ynh<packageversion>
# For example : 4.3-2~ynh3
# This include the number after ~ynh
# In the last example it return 3
# The version number in the manifest is defined by `<upstreamversion>~ynh<packageversion>`.
#
# For example, if the manifest contains `4.3-2~ynh3` the function will return `3`
#
# Requires YunoHost version 3.5.0 or higher.
ynh_app_package_version () {
@ -611,79 +614,52 @@ ynh_app_package_version () {
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
version_key=$YNH_APP_MANIFEST_VERSION
echo "${version_key/*~ynh/}"
version_key_=$YNH_APP_MANIFEST_VERSION
echo "${version_key_/*~ynh/}"
}
# Checks the app version to upgrade with the existing app version and returns:
#
# - UPGRADE_APP if the upstream app version has changed
# - UPGRADE_PACKAGE if only the YunoHost package has changed
#
# It stops the current script without error if the package is up-to-date
# usage: ynh_check_app_version_changed
# | ret: `UPGRADE_APP` if the upstream version changed, `UPGRADE_PACKAGE` otherwise.
#
# This helper should be used to avoid an upgrade of an app, or the upstream part
# of it, when it's not needed
#
# To force an upgrade, even if the package is up to date,
# you have to set the variable YNH_FORCE_UPGRADE before.
# example: sudo YNH_FORCE_UPGRADE=1 yunohost app upgrade MyApp
#
# usage: ynh_check_app_version_changed
#
# You can force an upgrade, even if the package is up to date, with the `--force` (or `-F`) argument :
# ```
# sudo yunohost app upgrade <appname> --force
# ```
# Requires YunoHost version 3.5.0 or higher.
ynh_check_app_version_changed () {
local force_upgrade=${YNH_FORCE_UPGRADE:-0}
local package_check=${PACKAGE_CHECK_EXEC:-0}
local return_value=${YNH_APP_UPGRADE_TYPE}
# By default, upstream app version has changed
local return_value="UPGRADE_APP"
local current_version=$(ynh_read_manifest --manifest="/etc/yunohost/apps/$YNH_APP_INSTANCE_NAME/manifest.json" --manifest_key="version" || echo 1.0)
local current_upstream_version="$(ynh_app_upstream_version --manifest="/etc/yunohost/apps/$YNH_APP_INSTANCE_NAME/manifest.json")"
local update_version=$(ynh_read_manifest --manifest="../manifest.json" --manifest_key="version" || echo 1.0)
local update_upstream_version="$(ynh_app_upstream_version)"
if [ "$current_version" == "$update_version" ]
if [ "$return_value" == "UPGRADE_FULL" ] || [ "$return_value" == "UPGRADE_FORCED" ] || [ "$return_value" == "DOWNGRADE_FORCED" ]
then
# Complete versions are the same
if [ "$force_upgrade" != "0" ]
then
ynh_print_info --message="Upgrade forced by YNH_FORCE_UPGRADE."
unset YNH_FORCE_UPGRADE
elif [ "$package_check" != "0" ]
then
ynh_print_info --message="Upgrade forced for package check."
else
ynh_die "Up-to-date, nothing to do" 0
fi
elif [ "$current_upstream_version" == "$update_upstream_version" ]
then
# Upstream versions are the same, only YunoHost package versions differ
return_value="UPGRADE_PACKAGE"
return_value="UPGRADE_APP"
fi
echo $return_value
}
# Compare the current package version against another version given as an argument.
# This is really useful when we need to do some actions only for some old package versions.
#
# usage: ynh_compare_current_package_version --comparison (lt|le|eq|ne|ge|gt) --version <X~ynhY>
# | arg: --comparison - Comparison type. Could be : `lt` (lower than), `le` (lower or equal), `eq` (equal), `ne` (not equal), `ge` (greater or equal), `gt` (greater than)
# | arg: --version - The version to compare. Need to be a version in the yunohost package version type (like `2.3.1~ynh4`)
# | ret: 0 if the evaluation is true, 1 if false.
#
# example: ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1
# This example will check if the installed version is lower than (lt) the version 2.3.2~ynh1
#
# Generally you might probably use it as follow in the upgrade script
# This helper is usually used when we need to do some actions only for some old package versions.
#
# if ynh_compare_current_package_version --comparaison lt --version 2.3.2~ynh1
# Generally you might probably use it as follow in the upgrade script :
# ```
# if ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1
# then
# # Do something that is needed for the package version older than 2.3.2~ynh1
# fi
#
# usage: ynh_compare_current_package_version --comparison lt|le|eq|ne|ge|gt
# | arg: --comparison - Comparison type. Could be : lt (lower than), le (lower or equal),
# | eq (equal), ne (not equal), ge (greater or equal), gt (greater than)
# | arg: --version - The version to compare. Need to be a version in the yunohost package version type (like 2.3.1~ynh4)
#
# Return 0 if the evaluation is true. 1 if false.
# ```
#
# Requires YunoHost version 3.8.0 or higher.
ynh_compare_current_package_version() {
@ -699,12 +675,12 @@ ynh_compare_current_package_version() {
# Check the syntax of the versions
if [[ ! $version =~ '~ynh' ]] || [[ ! $current_version =~ '~ynh' ]]
then
ynh_die "Invalid argument for version."
ynh_die --message="Invalid argument for version."
fi
# Check validity of the comparator
if [[ ! $comparison =~ (lt|le|eq|ne|ge|gt) ]]; then
ynh_die "Invialid comparator must be : lt, le, eq, ne, ge, gt"
ynh_die --message="Invalid comparator must be : lt, le, eq, ne, ge, gt"
fi
# Return the return value of dpkg --compare-versions

View file

@ -0,0 +1,9 @@
#!/bin/bash
source /usr/share/yunohost/helpers
ynh_abort_if_errors
YNH_CWD="${YNH_BACKUP_DIR%/}/conf/dkim"
mkdir -p "$YNH_CWD"
cd "$YNH_CWD"
ynh_backup --src_path="/etc/dkim"

View file

@ -0,0 +1,10 @@
#!/bin/bash
source /usr/share/yunohost/helpers
ynh_abort_if_errors
YNH_CWD="${YNH_BACKUP_DIR%/}/conf/ynh/dyndns"
mkdir -p $YNH_CWD
cd "$YNH_CWD"
# Backup the configuration
ynh_exec_warn_less ynh_backup --src_path="/etc/yunohost/dyndns" --not_mandatory

View file

@ -27,6 +27,31 @@ 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
chown root:root /var/cache/yunohost
chmod 700 /var/cache/yunohost
}
do_pre_regen() {
@ -63,11 +88,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 +111,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 +130,13 @@ do_post_regen() {
# Enfore permissions #
######################
chmod 750 /home/yunohost.conf
chmod 750 /home/yunohost.backup
chmod 750 /home/yunohost.backup/archives
chown root:root /home/yunohost.conf
chown admin:root /home/yunohost.backup
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
@ -101,6 +144,9 @@ do_post_regen() {
find /etc/yunohost/certs/ -type f -exec chmod 640 {} \;
find /etc/yunohost/certs/ -type d -exec chmod 750 {} \;
chown root:root /var/cache/yunohost
chmod 700 /var/cache/yunohost
# Misc configuration / state files
chown root:root $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null)
chmod 600 $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null)
@ -115,7 +161,7 @@ do_post_regen() {
}
_update_services() {
python2 - << EOF
python3 - << EOF
import yaml

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,39 @@ Package: $package
Pin: origin \"packages.sury.org\"
Pin-Priority: -1" >> "${pending_dir}/etc/apt/preferences.d/extra_php_version"
done
echo "
# Yes !
# This is what's preventing you from installing apache2 !
#
# Maybe take two fucking minutes to realize that if you try to install
# apache2, this will break nginx and break the entire YunoHost ecosystem.
# on your server.
#
# So, *NO*
# DO NOT do this.
# DO NOT remove these lines.
#
# I warned you. I WARNED YOU! But did you listen to me?
# Oooooh, noooo. You knew it all, didn't you?
Package: apache2
Pin: release *
Pin-Priority: -1
Package: apache2-bin
Pin: release *
Pin-Priority: -1
# Also yes, bind9 will conflict with dnsmasq.
# Same story than for apache2.
# Don't fucking install it.
Package: bind9
Pin: release *
Pin-Priority: -1
" >> "${pending_dir}/etc/apt/preferences.d/ban_packages"
}
do_post_regen() {

View file

@ -26,11 +26,13 @@ do_pre_regen() {
# Add possibility to specify a relay
# Could be useful with some isp with no 25 port open or more complex setup
export relay_port=""
export relay_user=""
export relay_host="$(yunohost settings get 'smtp.relay.host')"
if [ -n "${relay_host}" ]
then
export relay_port="$(yunohost settings get 'smtp.relay.port')"
export relay_user="$(yunohost settings get 'smtp.relay.user')"
relay_port="$(yunohost settings get 'smtp.relay.port')"
relay_user="$(yunohost settings get 'smtp.relay.user')"
relay_password="$(yunohost settings get 'smtp.relay.password')"
# Avoid to display "Relay account paswword" to other users

View file

@ -1,7 +1,6 @@
#!/bin/bash
set -e
MYSQL_PKG="$(dpkg --list | sed -ne 's/^ii \(mariadb-server-[[:digit:].]\+\) .*$/\1/p')"
. /usr/share/yunohost/helpers
do_pre_regen() {
@ -15,6 +14,45 @@ 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
MYSQL_PKG="$(dpkg --list | sed -ne 's/^ii \(mariadb-server-[[:digit:].]\+\) .*$/\1/p')"
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 !?" >&2
fi
# Legacy code to get rid of /etc/yunohost/mysql ...
# Nowadays, we can simply run mysql while being run as root of unix_socket/auth_socket is enabled...
if [ -f /etc/yunohost/mysql ]; then
# This is a trick to check if we're able to use mysql without password
# Expect instances installed in stretch to already have unix_socket
#configured, but not old instances from the jessie/wheezy era
if ! echo "" | mysql
then
password="$(cat /etc/yunohost/mysql)"
# Enable plugin unix_socket for root on localhost
mysql -u root -p"$password" <<< "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED WITH unix_socket WITH GRANT OPTION;"
fi
# If now we're able to login without password, drop the mysql password
if echo "" | mysql
then
rm /etc/yunohost/mysql
else
echo "Can't connect to mysql using unix_socket auth ... something went wrong while trying to get rid of mysql password !?" >&2
fi
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 +65,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,32 +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 name
# 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_board"]
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(),
)
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)
@ -61,36 +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",
)
def bad_sury_packages(self):
@ -101,10 +142,18 @@ 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)
def backports_in_sources_list(self):
cmd = "grep -q -nr '^ *deb .*-backports' /etc/apt/sources.list*"
return os.system(cmd) == 0
def is_vulnerable_to_meltdown(self):
# meltdown CVE: https://security-tracker.debian.org/tracker/CVE-2017-5754
@ -120,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
@ -133,17 +186,20 @@ 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
# "missing some kernel info (see -v), accuracy might be reduced"
# Dunno what to do about that but we probably don't want to harass
# users with this warning ...
output, err = call.communicate()
output, _ = call.communicate()
output = output.decode()
assert call.returncode in (0, 2, 3), "Return code: %s" % call.returncode
# If there are multiple lines, sounds like there was some messages
@ -159,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:
@ -130,12 +145,21 @@ class DNSRecordsDiagnoser(Diagnoser):
# Split expected/current
# from "v=DKIM1; k=rsa; p=hugekey;"
# to a set like {'v=DKIM1', 'k=rsa', 'p=...'}
# Additionally, for DKIM, because the key is pretty long,
# 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"].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
@ -150,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)
@ -164,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
@ -182,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))
@ -195,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):
"""
@ -209,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": list(ports)}, 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

@ -1,10 +1,11 @@
#!/usr/bin/env python
import os
import psutil
import subprocess
import datetime
import re
from moulinette.utils.process import check_output
from yunohost.diagnosis import Diagnoser
@ -16,7 +17,7 @@ class SystemResourcesDiagnoser(Diagnoser):
def run(self):
MB = 1024**2
MB = 1024 ** 2
GB = MB * 1024
#
@ -25,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"
@ -46,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"
@ -68,6 +75,11 @@ 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 d.mountpoint in ["/", "/var"] or not d.device.startswith("/dev/loop")
]
for disk_partition in disk_partitions:
device = disk_partition.device
mountpoint = disk_partition.mountpoint
@ -75,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:
@ -99,18 +119,50 @@ class SystemResourcesDiagnoser(Diagnoser):
yield item
#
# Check for minimal space on / + /var
# because some stupid VPS provider only configure a stupidly
# low amount of disk space for the root partition
# 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]
)
if main_space < 10 * GB:
yield dict(
meta={"test": "rootfstotalspace"},
data={"space": human_size(main_space)},
status="ERROR",
summary="diagnosis_rootfstotalspace_critical",
)
elif main_space < 14 * GB:
yield dict(
meta={"test": "rootfstotalspace"},
data={"space": human_size(main_space)},
status="WARNING",
summary="diagnosis_rootfstotalspace_warning",
)
#
# Recent kills by oom_reaper
#
kills_count = self.recent_kills_by_oom_reaper()
if kills_count:
kills_summary = "\n".join(["%s (x%s)" % (proc, count) for proc, count in kills_count])
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"):
@ -119,7 +171,7 @@ class SystemResourcesDiagnoser(Diagnoser):
def analyzed_kern_log():
cmd = 'tail -n 10000 /var/log/kern.log | grep "oom_reaper: reaped process" || true'
out = subprocess.check_output(cmd, shell=True).strip()
out = check_output(cmd)
lines = out.split("\n") if out else []
now = datetime.datetime.now()
@ -128,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
@ -136,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
@ -144,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

@ -1,5 +0,0 @@
# We don't backup/restore mysql password anymore
# c.f. https://github.com/YunoHost/yunohost/pull/912
# This is a dummy empty file as a workaround for
# https://github.com/YunoHost/issues/issues/1553 until it is fixed

View file

@ -0,0 +1,9 @@
#!/bin/bash
backup_dir="$1/conf/dkim"
cp -a $backup_dir/etc/dkim/. /etc/dkim
chown -R root:root /etc/dkim
chown _rspamd:root /etc/dkim
chown _rspamd:root /etc/dkim/*.mail.key

View file

@ -0,0 +1,9 @@
#!/bin/bash
source /usr/share/yunohost/helpers
ynh_abort_if_errors
YNH_CWD="${YNH_BACKUP_DIR%/}/conf/ynh/dyndns"
cd "$YNH_CWD"
# Restore file if exists
ynh_restore_file --origin_path="/etc/yunohost/dyndns" --not_mandatory

View file

@ -1,6 +1,7 @@
[Unit]
Description=YunoHost boot prompt
After=getty@tty2.service
After=network.target
[Service]
Type=simple

View file

@ -1,8 +1,8 @@
address=/{{ domain }}/{{ ipv4 }}
address=/xmpp-upload.{{ domain }}/{{ ipv4 }}
host-record={{ domain }},{{ ipv4 }}
host-record=xmpp-upload.{{ domain }},{{ ipv4 }}
{% if ipv6 %}
address=/{{ domain }}/{{ ipv6 }}
address=/xmpp-upload.{{ domain }}/{{ ipv6 }}
host-record={{ domain }},{{ ipv6 }}
host-record=xmpp-upload.{{ domain }},{{ ipv6 }}
{% endif %}
txt-record={{ domain }},"v=spf1 mx a -all"
mx-host={{ domain }},{{ domain }},5

View file

@ -2,4 +2,5 @@ location ^~ '/.well-known/acme-challenge/'
{
default_type "text/plain";
alias /tmp/acme-challenge-public/;
gzip off;
}

View file

@ -8,10 +8,4 @@ location /yunohost/admin/ {
more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; connect-src 'self' https://raw.githubusercontent.com https://paste.yunohost.org wss://$host; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; object-src 'none';";
more_set_headers "Content-Security-Policy-Report-Only:";
# Short cache on handlebars templates
location ~* \.(?:ms)$ {
expires 5m;
add_header Cache-Control "public";
}
}

View file

@ -53,12 +53,8 @@ smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtpd_tls_loglevel=1
# -- TLS for outgoing connections
{% if relay_host != "" %}
smtp_tls_security_level = encrypt
{% else %}
# Use TLS if this is supported by the remote SMTP server, otherwise use plaintext.
smtp_tls_security_level = may
{% endif %}
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_exclude_ciphers = aNULL, MD5, DES, ADH, RC4, 3DES
smtp_tls_mandatory_ciphers= high
@ -182,6 +178,9 @@ milter_default_action = accept
smtp_destination_concurrency_limit = 2
default_destination_rate_delay = 5s
# Avoid to be blacklisted due to too many recipient
smtpd_client_recipient_rate_limit=150
# Avoid email adress scanning
# By default it's possible to detect if the email adress exist
# So it's easly possible to scan a server to know which email adress is valid

View file

@ -8,7 +8,7 @@
BASE dc=yunohost,dc=org
URI ldap://localhost:389
#SIZELIMIT 12
SIZELIMIT 10000
#TIMELIMIT 15
#DEREF never

View file

@ -2,6 +2,8 @@ uPnP:
enabled: false
TCP: [22, 25, 80, 443, 587, 993, 5222, 5269]
UDP: []
TCP_TO_CLOSE: []
UDP_TO_CLOSE: []
ipv4:
TCP: [22, 25, 53, 80, 443, 587, 993, 5222, 5269]
UDP: [53, 5353]

206
debian/changelog vendored
View file

@ -1,3 +1,209 @@
yunohost (4.2.0) testing; urgency=low
- [mod] Python2 -> Python3 ([#1116](https://github.com/yunohost/yunohost/pull/1116), a97a9df3, 1387dff4, b53859db, f5ab4443, f9478b93, dc6033c3)
- [mod] refactoring: Drop legacy-way of passing arguments in hook_exec, prevent exposing secrets in command line args ([#1096](https://github.com/yunohost/yunohost/pull/1096))
- [mod] refactoring: use regen_conf instead of service_regen_conf in settings.py (9c11fd58)
- [mod] refactoring: More consistent local CA management for simpler postinstall ([#1062](https://github.com/yunohost/yunohost/pull/1062))
- [mod] refactoring: init folders during .deb install instead of regen conf ([#1063](https://github.com/yunohost/yunohost/pull/1063))
- [mod] refactoring: init ldap before the postinstall ([#1064](https://github.com/yunohost/yunohost/pull/1064))
- [mod] refactoring: simpler and more consistent logging initialization ([#1119](https://github.com/yunohost/yunohost/pull/1119), 0884a0c1)
- [mod] code-quality: add CI job to auto-format code, fix linter errors ([#1142](https://github.com/yunohost/yunohost/pull/1142), [#1161](https://github.com/yunohost/yunohost/pull/1161), 97f26015, [#1162](https://github.com/yunohost/yunohost/pull/1162))
- [mod] misc: Prevent the installation of apache2 ... ([#1148](https://github.com/yunohost/yunohost/pull/1148))
- [mod] misc: Drop old cache rules for .ms files, not relevant anymore ([#1150](https://github.com/yunohost/yunohost/pull/1150))
- [fix] misc: Abort postinstall if /etc/yunohost/apps ain't empty ([#1147](https://github.com/yunohost/yunohost/pull/1147))
- [mod] misc: No need for mysql root password anymore ([#912](https://github.com/YunoHost/yunohost/pull/912))
- [fix] app operations: wait for services to finish reloading (4a19a60b)
- [enh] ux: Improve error semantic such that the webadmin can autoredirect to the proper log view ([#1077](https://github.com/yunohost/yunohost/pull/1077), [#1187](https://github.com/YunoHost/yunohost/pull/1187))
- [mod] cli/api: Misc command and routes renaming / aliasing ([#1146](https://github.com/yunohost/yunohost/pull/1146))
- [enh] cli: Add a new "yunohost app search" command ([#1070](https://github.com/yunohost/yunohost/pull/1070))
- [enh] cli: Add '--remove-apps' (and '--force') options to "yunohost domain remove" ([#1125](https://github.com/yunohost/yunohost/pull/1125))
- [enh] diagnosis: Report low total space for rootfs ([#1145](https://github.com/yunohost/yunohost/pull/1145))
- [fix] upnp: Handle port closing ([#1154](https://github.com/yunohost/yunohost/pull/1154))
- [fix] dyndns: clean old madness, improve update strategy, improve cron management, delete dyndns key upon domain removal ([#1149](https://github.com/yunohost/yunohost/pull/1149))
- [enh] helpers: Adding composer helper ([#1090](https://github.com/yunohost/yunohost/pull/1090))
- [enh] helpers: Upgrade n to v7.0.2 ([#1178](https://github.com/yunohost/yunohost/pull/1178))
- [enh] helpers: Add multimedia helpers and hooks ([#1129](https://github.com/yunohost/yunohost/pull/1129), 47420c62)
- [enh] helpers: Normalize conf template handling for nginx, php-fpm, systemd and fail2ban using ynh_add_config ([#1118](https://github.com/yunohost/yunohost/pull/1118))
- [fix] helpers, doc: Update template for the new doc (grav) ([#1167](https://github.com/yunohost/yunohost/pull/1167), [#1168](https://github.com/yunohost/yunohost/pull/1168), 59d3e387)
- [enh] helpers: Define YNH_APP_BASEDIR to be able to properly point to conf folder depending on the app script we're running ([#1172](https://github.com/yunohost/yunohost/pull/1172))
- [enh] helpers: Use jq / output-as json to get info from yunohost commands instead of scraping with grep ([#1160](https://github.com/yunohost/yunohost/pull/1160))
- [fix] helpers: Misc fixes/enh (b85d959d, db93b82b, ce04570b, 07f8d6d7)
- [fix] helpers: download ynh_setup_source stuff in /var/cache/yunohost to prevent situations where it ends up in /etc/yunohost/apps/ (d98ec6ce)
- [i18n] Translations updated for Catalan, Chinese (Simplified), Czech, Dutch, French, German, Italian, Occitan, Polish
Thanks to all contributors <3 ! (Bram, Christian W., Daniel, Dave, Éric G., Félix P., Flavio C., Kay0u, Krzysztof N., ljf, Mathieu M., Miloš K., MrMorals, Nils V.Z., penguin321, ppr, Quentí, Radek S, Scapharnaum, Sébastien M., xaloc33, yalh76, Yifei D.)
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 25 Mar 2021 01:00:00 +0100
yunohost (4.1.7.4) stable; urgency=low
- [fix] sec: Enforce permissions for /home/yunohost.backup and .conf (41b5a123)
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 11 Mar 2021 03:08:10 +0100
yunohost (4.1.7.3) stable; urgency=low
- [fix] log: Some secrets were not redacted (0c172cd3)
- [fix] log: For some reason sometimes we were redacting 'empty string' which made everything explode (88b414c8)
- [fix] helpers: Various fixes for ynh_add_config / ynh_replace_vars (a43cd72c, 2728801d, 9bbc3b72, 2402a1db, 6ce02270)
- [fix] helpers: Fix permission helpers doc format (d12f403f)
- [fix] helpers: ynh_systemd_action did not properly clean the 'tail' process when service action failed (05969184)
- [fix] i18n: Translation typo in italian translation ... (bd8644a6)
Thanks to all contributors <3 ! (Kay0u, yalh76)
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 02 Mar 2021 02:03:35 +0100
yunohost (4.1.7.2) stable; 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))
- [fix] Stupid yolopatch for not-normalized app path settings ([#1141](https://github.com/yunohost/yunohost/pull/1141))
- [i18n] Update translations for German
Thanks to all contributors <3 ! (Christian W., Daniel, penguin321)
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 20 Jan 2021 01:46:02 +0100
yunohost (4.1.5) stable; urgency=low
- [fix] Update helpers ([#1136](https://github.com/yunohost/yunohost/pull/11346))
- [fix] Certificate during regen conf on some setup (1d2b1d9)
- [fix] Empty password is not an error if it's optional ([#1135](https://github.com/yunohost/yunohost/pull/11345))
- [fix] Remove useless warnings during system backup ([#1138](https://github.com/yunohost/yunohost/pull/11348))
- [fix] We can now use "true" or "false" for a boolean ([#1134](https://github.com/yunohost/yunohost/pull/1134))
- [i18n] Translations updated for Catalan, French, Italian, Spanish
Thanks to all contributors <3 ! (Aleks, Kay0u, Omnia89, jorge-vitrubio, YohannEpitech, xaloc33)
-- Kayou <pierre@kayou.io> Thu, 14 Jan 2021 21:23:39 +0100
yunohost (4.1.4.4) stable; urgency=low
- [fix] Add the -F flag to grep command for fixed string mode, prevent special chars in the password to be interpreted as regex pattern ([#1132](https://github.com/yunohost/yunohost/pull/1132))
- [fix] apt helpers: explicitly return 0, otherwise the return code of last command is used, which in that case is 1 ... (c56883d0)
Thanks to all contributors <3 ! (Saxodwarf)
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 11 Jan 2021 14:17:37 +0100
yunohost (4.1.4.3) stable; urgency=low
- [fix] ynh_replace_vars in case var is defined but empty (30dde208)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 10 Jan 2021 01:58:35 +0100
yunohost (4.1.4.2) stable; urgency=low
- [fix] Prevent info from being redacted (because of foobar_key=) by the logging system (8f1b05f3)
- [fix] For some reason sometimes submetadata is None ... (00508c96)
- [enh] Reduce the noise in logs because of ynh_app_setting (ac4b62ce)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 09 Jan 2021 18:59:01 +0100
yunohost (4.1.4.1) stable; urgency=low
- [hotfix] Postfix conf always included the relay snippets (b25cde0b)
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 08 Jan 2021 16:21:07 +0100
yunohost (4.1.4) stable; urgency=low
- [fix] firewall: force source port for UPnP. ([#1109](https://github.com/yunohost/yunohost/pull/1109))
- Stable release
Thanks to all contributors <3 ! (Léo Le Bouter)
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 08 Jan 2021 03:09:14 +0100
yunohost (4.1.3) testing; urgency=low
- [enh] Do not advertise upgrades for bad-quality apps ([#1066](https://github.com/yunohost/yunohost/pull/1066))
- [enh] Display domain_path of app in the output of app list ([#1120](https://github.com/yunohost/yunohost/pull/1120))
- [enh] Diagnosis: report usage of backports repository in apt's sources.list ([#1069](https://github.com/yunohost/yunohost/pull/1069))
- [mod] Code cleanup, misc fixes (165d2b32, [#1121](https://github.com/yunohost/yunohost/pull/1121), [#1122](https://github.com/yunohost/yunohost/pull/1122), [#1123](https://github.com/yunohost/yunohost/pull/1123), [#1131](https://github.com/yunohost/yunohost/pull/1131))
- [mod] Also display app label on remove_domain with apps ([#1124](https://github.com/yunohost/yunohost/pull/1124))
- [enh] Be able to change user password in CLI without writing it in clear ([#1075](https://github.com/YunoHost/yunohost/pull/1075))
- [enh] New permissions helpers ([#1117](https://github.com/yunohost/yunohost/pull/1117))
- [i18n] Translations updated for French, German
Thanks to all contributors <3 ! (C. Wehrli, cricriiiiii, Kay0u, Bram, ljf, ppr)
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 07 Jan 2021 00:46:09 +0100
yunohost (4.1.2) testing; urgency=low
- [enh] diagnosis: Detect moar hardware name (b685a274)
- [fix] permissions: Handle regexes that may start with ^ or \ (bdff5937)
- [fix] permissions: Tile/protect status for legacy migration ([#1113](https://github.com/yunohost/yunohost/pull/1113))
- [fix] domain: double return prevent new code from working (0c977d8c)
- [fix] settings: When encountering unknown setting, also save the regular setting so we don't re-encounter the unknown settings everytime (d77d5afb)
- [fix] users: only ask for one letter for first/last name ([#1114](https://github.com/yunohost/yunohost/pull/1114))
- [fix] apt/sury: Tweak app helpers to not mess with Sury's pinning ([#1110](https://github.com/yunohost/yunohost/pull/1110))
- [i18n] Translations updated for German
Thanks to all contributors <3 ! (Bram, C. Wehrli, Kayou)
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 31 Dec 2020 16:26:51 +0100
yunohost (4.1.1) testing; urgency=low
- [fix] Backup/restore DKIM keys ([#1098](https://github.com/yunohost/yunohost/pull/1098), [#1100](https://github.com/yunohost/yunohost/pull/1100))
- [fix] Backup/restore Dyndns keys ([#1101](https://github.com/yunohost/yunohost/pull/1101))
- [fix] mail: Add a max limit to number of recipients ([#1094](https://github.com/yunohost/yunohost/pull/1094))
- [fix] mail: Do not enforce encryption for relays .. some don't support it ... (11fe9d7e)
- [i18n] Translations updated for French, German, Italian, Occitan
Misc small fixes:
- [fix] misc: Prevent running `yunohost domain dns-conf` on arbirary domains ([#1099](https://github.com/yunohost/yunohost/pull/1099))
- [enh] misc: We don't care that 'apt-key output should not be parsed' (5422a49d)
- [fix] dnsmasq: Avoid to define wildcard records locally ([#1102](https://github.com/yunohost/yunohost/pull/1102))
- [fix] ssowat: Fix indent ([#1103](https://github.com/yunohost/yunohost/pull/1103))
- [fix] nginx: Force-disable gzip for acme-challenge (c5d06af2)
- [enh] app helpers: Handle change php version ([#1107](https://github.com/yunohost/yunohost/pull/1107))
- [fix] permissions: Misc fixes ([#1104](https://github.com/yunohost/yunohost/pull/1104), [#1105](https://github.com/yunohost/yunohost/pull/1105))
- [fix] certificates: Use organization name to check if from Lets Encrypt ([#1093](https://github.com/yunohost/yunohost/pull/1093))
- [enh] ldap: Increase ldap search size limit? ([#1074](https://github.com/yunohost/yunohost/pull/1074))
- [fix] app helpers: Avoid unecessarily reloading php7.3 too fast ([#1108](https://github.com/yunohost/yunohost/pull/1108))
- [fix] log: Fix a small issue where metadata could be None (because of empty yaml maybe?) (f9143d53)
Thanks to all contributors <3 ! (Christian Wehrli, Eric COURTEAU, Flavio Cristoforetti, Kay0u, Kayou, ljf, ljf (zamentur), Quentí)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 19 Dec 2020 01:33:36 +0100
yunohost (4.1.0) testing; urgency=low
- [enh] Extends permissions features, improve legacy settings handling (YunoHost#861)

18
debian/control vendored
View file

@ -2,19 +2,18 @@ Source: yunohost
Section: utils
Priority: extra
Maintainer: YunoHost Contributors <contrib@yunohost.org>
Build-Depends: debhelper (>=9), dh-systemd, dh-python, python-all (>= 2.7), python-yaml, python-jinja2
Build-Depends: debhelper (>=9), dh-systemd, dh-python, python3-all (>= 3.7), python3-yaml, python3-jinja2
Standards-Version: 3.9.6
X-Python-Version: >= 2.7
Homepage: https://yunohost.org/
Package: yunohost
Essential: yes
Architecture: all
Depends: ${python:Depends}, ${misc:Depends}
, moulinette (>= 4.1), ssowat (>= 4.0)
, python-psutil, python-requests, python-dnspython, python-openssl
, python-miniupnpc, python-dbus, python-jinja2
, python-toml, python-packaging, python-publicsuffix
Depends: ${python3:Depends}, ${misc:Depends}
, moulinette (>= 4.2), ssowat (>= 4.0)
, python3-psutil, python3-requests, python3-dnspython, python3-openssl
, python3-miniupnpc, python3-dbus, python3-jinja2
, python3-toml, python3-packaging, python3-publicsuffix
, apt, apt-transport-https, apt-utils, dirmngr
, php7.3-common, php7.3-fpm, php7.3-ldap, php7.3-intl
, mariadb-server, php7.3-mysql
@ -27,13 +26,14 @@ Depends: ${python: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
, bash-completion, rsyslog
, php7.3-gd, php7.3-curl, php-gettext
, python-pip
, python3-pip
, unattended-upgrades
, libdbd-ldap-perl, libnet-dns-perl
Suggests: htop, vim, rsync, acpi-support-base, udisks2

19
debian/postinst vendored
View file

@ -6,16 +6,25 @@ do_configure() {
rm -rf /var/cache/moulinette/*
if [ ! -f /etc/yunohost/installed ]; then
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/06-slapd init
bash /usr/share/yunohost/hooks/conf_regen/15-nginx init
# 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
then
echo "Sounds like /etc/yunohost/installed mysteriously disappeared ... You should probably contact the Yunohost support ..."
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
else
echo "Regenerating configuration, this might take a while..."
yunohost tools regen-conf --output-as none
echo "Launching migrations..."
yunohost tools migrations migrate --auto
yunohost tools migrations run --auto
echo "Re-diagnosing server health..."
yunohost diagnosis run --force

6
debian/rules vendored
View file

@ -5,12 +5,12 @@
#export DH_VERBOSE=1
%:
dh ${@} --with=python2,systemd
dh ${@} --with=python3,systemd
override_dh_auto_build:
# Generate bash completion file
python data/actionsmap/yunohost_completion.py
python doc/generate_manpages.py --gzip --output doc/yunohost.8.gz
python3 data/actionsmap/yunohost_completion.py
python3 doc/generate_manpages.py --gzip --output doc/yunohost.8.gz
override_dh_installinit:
dh_installinit -pyunohost --name=yunohost-api --restart-after-upgrade

View file

@ -1,25 +1,33 @@
#!/usr/env/python2.7
#!/usr/env/python3
import os
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,12 +59,12 @@ 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)
for i in actionsmap.keys():
for i in list(actionsmap.keys()):
if i.startswith("_"):
del actionsmap[i]
@ -78,8 +82,8 @@ def main():
output.write(result)
else:
with gzip.open(output_path, mode="w", compresslevel=9) as output:
output.write(result)
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,60 @@
---
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 }}**<br/>
[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**:<br/>
{{ h.details }}
{%- 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

@ -140,7 +140,7 @@
"domain_dyndns_already_subscribed": "Ja us heu subscrit a un domini DynDNS",
"domain_dyndns_root_unknown": "Domini DynDNS principal desconegut",
"domain_hostname_failed": "No s'ha pogut establir un nou nom d'amfitrió. Això podria causar problemes més tard (podria no passar res).",
"domain_uninstall_app_first": "Aquestes aplicacions encara estan instal·lades en el vostre domini: {apps}. Desinstal·leu les abans d'eliminar el domini",
"domain_uninstall_app_first": "Aquestes aplicacions encara estan instal·lades en el vostre domini:\n{apps}\n\nDesinstal·leu-les utilitzant l'ordre «yunohost app remove id_de_lapplicació» o moveu-les a un altre domini amb «yunohost app change-url id_de_lapplicació» abans d'eliminar el domini",
"domain_unknown": "Domini desconegut",
"domains_available": "Dominis disponibles:",
"done": "Fet",
@ -199,9 +199,9 @@
"log_corrupted_md_file": "El fitxer de metadades YAML associat amb els registres està malmès: « {md_file} »\nError: {error}",
"log_category_404": "La categoria de registres « {category} » no existeix",
"log_link_to_log": "El registre complet d'aquesta operació: «<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>»",
"log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre «yunohost log display {name} »",
"log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre «yunohost log show {name}{name} »",
"log_link_to_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, <a href=\"#/tools/logs/{name}\">proveïu el registre complete de l'operació clicant aquí</a>",
"log_help_to_get_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre «yunohost log display {name} --share »",
"log_help_to_get_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre «yunohost log share {name} »",
"log_does_exists": "No hi ha cap registre per l'operació amb el nom«{log} », utilitzeu «yunohost log list» per veure tots els registre d'operació disponibles",
"log_operation_unit_unclosed_properly": "L'operació no s'ha tancat de forma correcta",
"log_app_change_url": "Canvia l'URL de l'aplicació « {} »",
@ -292,7 +292,7 @@
"migrations_migration_has_failed": "La migració {id} ha fallat, cancel·lant. Error: {exception}",
"migrations_no_migrations_to_run": "No hi ha cap migració a fer",
"migrations_skip_migration": "Saltant migració {id}...",
"migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations migrate».",
"migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations run».",
"migrations_need_to_accept_disclaimer": "Per fer la migració {id}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció «--accept-disclaimer».",
"no_internet_connection": "El servidor no està connectat a Internet",
"not_enough_disk_space": "No hi ha prou espai en «{path:s}»",
@ -606,7 +606,7 @@
"diagnosis_dns_point_to_doc": "Consulteu la documentació a <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> si necessiteu ajuda per configurar els registres DNS.",
"diagnosis_mail_outgoing_port_25_ok": "El servidor de correu electrònic SMTP pot enviar correus electrònics (el port de sortida 25 no està bloquejat).",
"diagnosis_mail_outgoing_port_25_blocked_details": "Primer heu d'intentar desbloquejar el port 25 en la interfície del vostre router o en la interfície del vostre allotjador. (Alguns proveïdors d'allotjament demanen enviar un tiquet de suport en aquests casos).",
"diagnosis_mail_ehlo_ok": "El servidor de correu electrònic SMTP no és accessible des de l'exterior i per tant no pot rebre correus electrònics!",
"diagnosis_mail_ehlo_ok": "El servidor de correu electrònic SMTP és accessible des de l'exterior i per tant pot rebre correus electrònics!",
"diagnosis_mail_ehlo_unreachable": "El servidor de correu electrònic SMTP no és accessible des de l'exterior amb IPv{ipversion}. No podrà rebre correus electrònics.",
"diagnosis_mail_ehlo_bad_answer": "Un servei no SMTP a respost en el port 25 amb IPv{ipversion}",
"diagnosis_mail_ehlo_bad_answer_details": "Podria ser que sigui per culpa d'una altra màquina responent en lloc del servidor.",
@ -712,5 +712,10 @@
"app_label_deprecated": "Aquesta ordre està desestimada! Si us plau utilitzeu la nova ordre «yunohost user permission update» per gestionar l'etiqueta de l'aplicació.",
"app_argument_password_no_default": "Hi ha hagut un error al analitzar l'argument de la contrasenya «{name}»: l'argument de contrasenya no pot tenir un valor per defecte per raons de seguretat",
"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}»"
"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}",
"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,13 @@
{}
{
"password_too_simple_1": "Heslo musí být aspoň 8 znaků dlouhé",
"app_already_installed": "{app:s} je již nainstalován/a",
"already_up_to_date": "Neprovedena žádná akce. Vše je již aktuální.",
"admin_password_too_long": "Zvolte prosím heslo kratší než 127 znaků",
"admin_password_changed": "Heslo správce bylo změněno",
"admin_password_change_failed": "Nebylo možné změnit heslo",
"admin_password": "Heslo správce",
"additional_urls_already_removed": "Dotatečný odkaz '{url:s}' byl již odebrán u oprávnění '{permission:s}'",
"additional_urls_already_added": "Dotatečný odkaz '{url:s}' byl již přidán v dodatečných odkazech pro oprávnění '{permission:s}'",
"action_invalid": "Nesprávné akce '{action:s}'",
"aborting": "Přerušení."
}

View file

@ -41,45 +41,45 @@
"backup_running_hooks": "Datensicherunghook wird ausgeführt...",
"custom_app_url_required": "Es muss eine URL angegeben werden, um deine benutzerdefinierte App {app:s} zu aktualisieren",
"domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden",
"domain_created": "Die Domain wurde angelegt",
"domain_creation_failed": "Konnte Domain nicht erzeugen",
"domain_created": "Domäne erstellt",
"domain_creation_failed": "Konnte Domäne nicht erzeugen",
"domain_deleted": "Domain wurde gelöscht",
"domain_deletion_failed": "Domain {domain}: {error} konnte nicht gelöscht werden",
"domain_dyndns_already_subscribed": "Du hast dich schon für eine DynDNS-Domain angemeldet",
"domain_dyndns_already_subscribed": "Sie haben sich schon für eine DynDNS-Domäne registriert",
"domain_dyndns_root_unknown": "Unbekannte DynDNS Hauptdomain",
"domain_exists": "Die Domain existiert bereits",
"domain_uninstall_app_first": "Mindestens eine App ist noch für diese Domain installiert. Bitte deinstalliere zuerst die App, bevor du die Domain löschst",
"domain_exists": "Die Domäne existiert bereits",
"domain_uninstall_app_first": "Diese Apps sind noch auf Ihrer Domäne installiert; \n{apps}\n\nBitte deinstallieren Sie sie mit dem Befehl 'yunohost app remove the_app_id' oder verschieben Sie sie mit 'yunohost app change-url the_app_id'",
"domain_unknown": "Unbekannte Domain",
"done": "Erledigt",
"downloading": "Wird heruntergeladen…",
"dyndns_cron_installed": "DynDNS Cronjob erfolgreich angelegt",
"dyndns_cron_remove_failed": "Der DynDNS Cronjob konnte nicht entfernt werden",
"dyndns_cron_installed": "DynDNS Cronjob erfolgreich erstellt",
"dyndns_cron_remove_failed": "Der DynDNS Cronjob konnte aufgrund dieses Fehlers nicht entfernt werden: {error}",
"dyndns_cron_removed": "DynDNS-Cronjob gelöscht",
"dyndns_ip_update_failed": "Konnte die IP-Adresse für DynDNS nicht aktualisieren",
"dyndns_ip_updated": "Aktualisierung Ihrer IP-Adresse bei DynDNS",
"dyndns_key_generating": "Generierung des DNS-Schlüssels..., das könnte eine Weile dauern.",
"dyndns_registered": "Deine DynDNS Domain wurde registriert",
"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",
"mail_alias_remove_failed": "E-Mail Alias '{mail:s}' konnte nicht entfernt werden",
"mail_domain_unknown": "Die Domäne '{domain:s}' dieser E-Mail-Adresse ist ungültig. Wähle bitte eine Domäne, welche durch diesen Server verwaltet wird.",
"mail_forward_remove_failed": "Mailweiterleitung '{mail:s}' konnte nicht entfernt werden",
"mail_forward_remove_failed": "Die Weiterleitungs-E-Mail '{mail:s}' konnte nicht gelöscht werden",
"main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden",
"main_domain_changed": "Die Hauptdomain wurde geändert",
"no_internet_connection": "Der Server ist nicht mit dem Internet verbunden",
@ -160,7 +160,7 @@
"backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path:s})",
"domains_available": "Verfügbare Domains:",
"dyndns_key_not_found": "DNS-Schlüssel für die Domain wurde nicht gefunden",
"dyndns_no_domain_registered": "Es wurde keine Domain mit DynDNS registriert",
"dyndns_no_domain_registered": "Keine Domain mit DynDNS registriert",
"ldap_init_failed_to_create_admin": "Die LDAP Initialisierung konnte keinen admin Benutzer erstellen",
"mailbox_used_space_dovecot_down": "Der Dovecot Mailbox Dienst muss gestartet sein, wenn du den von der Mailbox belegten Speicher angezeigen lassen willst",
"package_unknown": "Unbekanntes Paket '{pkgname}'",
@ -170,24 +170,24 @@
"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.",
"certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain:s} wurde erfolgreich erneuert.",
"certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain:s} wurde erfolgreich erneuert",
"certmanager_hit_rate_limit": "Es wurden innerhalb kurzer Zeit zu viele Zertifikate für dieselbe Domain {domain:s} ausgestellt. Bitte versuchen Sie es später nochmal. Besuchen Sie https://letsencrypt.org/docs/rate-limits/ für mehr Informationen",
"certmanager_cert_signing_failed": "Das neue Zertifikat konnte nicht signiert werden",
"certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain:s} (Datei: {file:s}) konnte nicht gelesen werden",
"certmanager_conflicting_nginx_file": "Die Domain konnte nicht für die ACME challenge vorbereitet werden: Die nginx Konfigurationsdatei {filepath:s} verursacht Probleme und sollte vorher entfernt werden",
"domain_cannot_remove_main": "Die primäre Domain konnten nicht entfernt werden. Lege zuerst einen neue primäre Domain fest",
"domain_cannot_remove_main": "Die primäre Domain konnten nicht entfernt werden. Lege zuerst einen neue primäre Domain Sie können die Domäne '{domain:s}' nicht entfernen, weil Sie die Hauptdomäne ist. Sie müssen zuerst eine andere Domäne als Hauptdomäne festlegen. Sie können das mit dem Befehl <cmd>'yunohost domain main-domain -n <another-domain></cmd> tun. Hier ist eine Liste der möglichen Domänen: {other_domains:s}",
"certmanager_self_ca_conf_file_not_found": "Die Konfigurationsdatei der Zertifizierungsstelle für selbstsignierte Zertifikate wurde nicht gefunden (Datei {file:s})",
"certmanager_acme_not_configured_for_domain": "Die ACME Challenge kann im Moment nicht für {domain} ausgeführt werden, weil in ihrer nginx conf das entsprechende Code-Snippet fehlt... Bitte stellen Sie sicher, dass Ihre nginx-Konfiguration mit 'yunohost tools regen-conf nginx --dry-run --with-diff' auf dem neuesten Stand ist.",
"certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht aufgelöst werden (Datei: {file:s})",
"certmanager_http_check_timeout": "Eine Zeitüberschreitung ist aufgetreten, als der Server versuchte sich selbst über HTTP mit der öffentlichen IP (Domain '{domain:s}' mit der IP '{ip:s}') zu erreichen. Möglicherweise ist dafür hairpinning oder eine falsch konfigurierte Firewall/Router deines Servers dafür verantwortlich.",
"certmanager_couldnt_fetch_intermediate_cert": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte die Teilzertifikate von Let's Encrypt zusammenzusetzen. Die Installation/Erneuerung des Zertifikats wurde abgebrochen — bitte versuche es später erneut.",
"domain_hostname_failed": "Erstellen des neuen Hostnamens fehlgeschlagen",
"domain_hostname_failed": "Sie können keinen neuen Hostnamen verwenden. Das kann zukünftige Probleme verursachen (es kann auch sein, dass es funktioniert).",
"yunohost_ca_creation_success": "Die lokale Zertifizierungs-Authorität wurde angelegt.",
"app_already_installed_cant_change_url": "Diese Application ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfe ob `app changeurl` verfügbar ist.",
"app_change_url_failed_nginx_reload": "NGINX konnte nicht neu gestartet werden. Hier ist der Output von 'nginx -t':\n{nginx_errors:s}",
@ -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,52 +253,52 @@
"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",
"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_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",
"global_settings_unknown_type": "Unerwartete Situation, die Einstellung {setting:s} scheint den Typ {unknown_type:s} zu haben, ist aber kein vom System unterstützter Typ.",
"dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint.... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo dpkg --configure -a` ausführst.",
"dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint.... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo apt install --fix-broken` sowie/oder `sudo dpkg --configure -a` ausführst.",
"global_settings_unknown_setting_from_settings_file": "Unbekannter Schlüssel in den Einstellungen: '{setting_key:s}', verwerfen und speichern in /etc/yunohost/settings-unknown.json",
"log_link_to_log": "Vollständiges Log dieser Operation: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'",
"global_settings_setting_example_bool": "Beispiel einer booleschen Option",
"log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log display {name}'",
"log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log show {name}{name}'",
"global_settings_setting_security_nginx_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den Webserver NGINX. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)",
"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 display {name} --share', um Hilfe zu erhalten",
"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": "Installation von {app} fehlgeschlagen: {error}",
"app_install_failed": "{app} kann nicht installiert werden: {error}",
"app_install_script_failed": "Im Installationsscript ist ein Fehler aufgetreten",
"app_remove_after_failed_install": "Entfernen der App nach fehlgeschlagener Installation...",
"app_upgrade_script_failed": "Es ist ein Fehler im App-Upgrade-Skript aufgetreten",
@ -337,7 +337,7 @@
"diagnosis_found_errors": "Habe {errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!",
"diagnosis_found_warnings": "Habe {warnings} Ding(e) gefunden, die verbessert werden könnten für {category}.",
"diagnosis_ip_dnsresolution_working": "Domänen-Namens-Auflösung funktioniert!",
"diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber seien Sie vorsichtig wenn Sie eine eigene <code>/etc/resolv.conf</code> verwendest.",
"diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber seien Sie vorsichtig wenn Sie Ihren eigenen <code>/etc/resolv.conf</code> verwenden.",
"diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, können Sie zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues' in der Kommandozeile ausführen.",
"backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}",
"backup_archive_cant_retrieve_info_json": "Die Informationen für das Archiv '{archive}' konnten nicht geladen werden... Die Datei info.json wurde nicht gefunden (oder ist kein gültiges json).",
@ -359,7 +359,7 @@
"diagnosis_domain_expiration_error": "Einige Domänen werden SEHR BALD ablaufen!",
"diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.",
"diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!",
"diagnosis_domain_expiration_not_found": "Konnte die Ablaufdaten für einige Domänen nicht überprüfen.",
"diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden",
"diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von Yunohost verwaltet werden. Andernfalls können Sie mittels <cmd>yunohost dyndns update --force</cmd> ein Update erzwingen.",
"diagnosis_dns_point_to_doc": "Bitte schauen Sie in die Dokumentation unter <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> wenn Sie Hilfe bei der Konfiguration der DNS-Einträge brauchen.",
"diagnosis_dns_discrepancy": "Der folgende DNS-Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen: <br>Typ: <code>{type}</code><br>Name: <code>{name}</code><br> Aktueller Wert: <code>{current}</code><br> Erwarteter Wert: <code>{value}</code>",
@ -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).",
@ -464,10 +464,157 @@
"domain_cannot_add_xmpp_upload": "Eine hinzugefügte Domain darf nicht mit 'xmpp-upload.' beginnen. Dieser Name ist für das XMPP-Upload-Feature von YunoHost reserviert.",
"group_cannot_be_deleted": "Die Gruppe {group} kann nicht manuell entfernt werden.",
"group_cannot_edit_primary_group": "Die Gruppe '{group}' kann nicht manuell bearbeitet werden. Es ist die primäre Gruppe, welche dazu gedacht ist, nur einen spezifischen Benutzer zu enthalten.",
"diagnosis_processes_killed_by_oom_reaper": "Einige Prozesse wurden vom System beendet, weil nicht genügend Arbeitsspeicher vorhanden ist. Das passiert normalerweise, wenn das System nicht genügend Arbeitsspeicher zur Verfügung hat oder wenn ein Prozess zu viel Speicher verbraucht. Zusammenfassung der beendeten Prozesse: {kills_summary}",
"diagnosis_processes_killed_by_oom_reaper": "Das System hat einige Prozesse beendet, weil ihm der Arbeitsspeicher ausgegangen ist. Das passiert normalerweise, wenn das System ingesamt nicht genügend Arbeitsspeicher zur Verfügung hat oder wenn ein einzelner Prozess zu viel Speicher verbraucht. Zusammenfassung der beendeten Prozesse: \n{kills_summary}",
"diagnosis_description_ports": "Offene Ports",
"additional_urls_already_added": "Zusätzliche URL '{url:s}' bereits hinzugefügt in der zusätzlichen URL für Berechtigung '{permission:s}'",
"additional_urls_already_removed": "Zusätzliche URL '{url:s}' bereits entfernt in der zusätzlichen URL für Berechtigung '{permission:s}'",
"app_label_deprecated": "Dieser Befehl ist veraltet! Bitte nutzen Sie den neuen Befehl 'yunohost user permission update' um das Applabel zu verwalten.",
"diagnosis_http_hairpinning_issue_details": "Das ist wahrscheinlich aufgrund Ihrer ISP Box / Router. Als Konsequenz können Personen von ausserhalb Ihres Netzwerkes aber nicht von innerhalb Ihres lokalen Netzwerkes (wie wahrscheinlich Sie selber?) wie gewohnt auf Ihren Server zugreifen, wenn Sie ihre Domäne oder Ihre öffentliche IP verwenden. Sie können die Situation wahrscheinlich verbessern, indem Sie ein einen Blick in <a href='https://yunohost.org/dns_local_network'>https://yunohost.org/dns_local_network</a> werfen"
"diagnosis_http_hairpinning_issue_details": "Das ist wahrscheinlich aufgrund Ihrer ISP Box / Router. Als Konsequenz können Personen von ausserhalb Ihres Netzwerkes aber nicht von innerhalb Ihres lokalen Netzwerkes (wie wahrscheinlich Sie selber?) wie gewohnt auf Ihren Server zugreifen, wenn Sie ihre Domäne oder Ihre öffentliche IP verwenden. Sie können die Situation wahrscheinlich verbessern, indem Sie ein einen Blick in <a href='https://yunohost.org/dns_local_network'>https://yunohost.org/dns_local_network</a> werfen",
"diagnosis_http_nginx_conf_not_up_to_date": "Jemand hat anscheinend die Konfiguration von Nginx manuell geändert. Diese Änderung verhindert, dass Yunohost eine Diagnose durchführen kann, wenn er via HTTP erreichbar ist.",
"diagnosis_http_bad_status_code": "Anscheinend beantwortet ein anderes Gerät als Ihr Server die Anfrage (Vielleicht ihr Internetrouter).<br>1. Die häufigste Ursache ist, dass Port 80 (und 443) <a href='https://yunohost.org/isp_box_config'>nicht richtig auf Ihren Server weitergeleitet wird</a>.<br> 2. Bei komplexeren Setups: Vergewissern Sie sich, dass keine Firewall und keine Reverse-Proxy interferieren.",
"diagnosis_never_ran_yet": "Sie haben kürzlich einen neuen Yunohost-Server installiert aber es gibt davon noch keinen Diagnosereport. Sie sollten eine Diagnose anstossen. Sie können das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwenden Sie dafür den Befehl 'yunohost diagnosis run'.",
"diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile <cmd>yunohost tools regen-conf nginx --dry-run --with-diff</cmd> ein. Dieses Tool zeigt ihnen den Unterschied an. Wenn Sie damit einverstanden sind, können Sie mit <cmd>yunohost tools regen-conf nginx --force</cmd> die Änderungen übernehmen.",
"diagnosis_backports_in_sources_list": "Sie haben anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie wissen was Sie tun.",
"diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}",
"domain_name_unknown": "Domäne '{domain}' unbekannt",
"group_user_not_in_group": "Der Benutzer {user} ist nicht in der Gruppe {group}",
"group_user_already_in_group": "Der Benutzer {user} ist bereits in der Gruppe {group}",
"group_cannot_edit_visitors": "Die Gruppe \"Besucher\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe und repräsentiert anonyme Besucher",
"group_cannot_edit_all_users": "Die Gruppe \"all_users\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe die dafür gedacht ist alle Benutzer in Yunohost zu halten",
"group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber Yunohost wird sie entfernen...",
"group_already_exist_on_system": "Die Gruppe {group} existiert bereits in den Systemgruppen",
"group_already_exist": "Die Gruppe {group} existiert bereits",
"global_settings_setting_smtp_relay_password": "SMTP Relay Host Passwort",
"global_settings_setting_smtp_relay_user": "SMTP Relay Benutzer Account",
"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.",
"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.",
"permission_already_up_to_date": "Die Berechtigung wurde nicht aktualisiert, weil die Anfragen für Hinzufügen/Entfernen stimmen mit dem aktuellen Status bereits überein",
"permission_already_exist": "Berechtigung '{permission}' existiert bereits",
"permission_already_disallowed": "Für die Gruppe '{group}' wurde die Berechtigung '{permission}' deaktiviert",
"permission_already_allowed": "Die Gruppe '{group}' hat die Berechtigung '{permission}' bereits erhalten",
"pattern_password_app": "Entschuldigen Sie bitte! Passwörter dürfen folgende Zeichen nicht enthalten: {forbidden_chars}",
"pattern_email_forward": "Es muss sich um eine gültige E-Mail-Adresse handeln. Das Symbol '+' wird akzeptiert (zum Beispiel : maxmuster@beispiel.com oder maxmuster+yunohost@beispiel.com)",
"password_too_simple_4": "Dass Passwort muss mindestens 12 Zeichen lang sein und Zahlen, Klein- und Grossbuchstaben und Sonderzeichen enthalten",
"password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Zahlen, Klein- und Grossbuchstaben und Sonderzeichen enthalten",
"regenconf_file_manually_removed": "Die Konfigurationsdatei '{conf}' wurde manuell gelöscht und wird nicht erstellt",
"regenconf_file_manually_modified": "Die Konfigurationsdatei '{conf}' wurde manuell bearbeitet und wird nicht aktualisiert",
"regenconf_file_kept_back": "Die Konfigurationsdatei '{conf}' sollte von \"regen-conf\" (Kategorie {category}) gelöscht werden, wurde aber beibehalten.",
"regenconf_file_copy_failed": "Die neue Konfigurationsdatei '{new}' kann nicht nach '{conf}' kopiert werden",
"regenconf_file_backed_up": "Die Konfigurationsdatei '{conf}' wurde unter '{backup}' gespeichert",
"permission_require_account": "Berechtigung {permission} ist nur für Benutzer mit einem Konto sinnvoll und kann daher nicht für Besucher aktiviert werden.",
"permission_protected": "Die Berechtigung ist geschützt. Sie können die Besuchergruppe nicht zu dieser Berechtigung hinzufügen oder daraus entfernen.",
"permission_updated": "Berechtigung '{permission:s}' aktualisiert",
"permission_update_failed": "Die Berechtigung '{permission}' kann nicht aktualisiert werden : {error}",
"permission_not_found": "Berechtigung nicht gefunden",
"permission_deletion_failed": "Entfernung der Berechtigung nicht möglich '{permission}': {error}",
"permission_deleted": "Berechtigung gelöscht",
"permission_currently_allowed_for_all_users": "Diese Berechtigung wird derzeit allen Benutzern zusätzlich zu anderen Gruppen erteilt. Möglicherweise möchten Sie entweder die Berechtigung 'all_users' entfernen oder die anderen Gruppen entfernen, für die sie derzeit zulässig sind.",
"permission_creation_failed": "Berechtigungserstellung nicht möglich '{permission}' : {error}",
"permission_created": "Berechtigung '{permission: s}' erstellt",
"permission_cannot_remove_main": "Entfernung einer Hauptberechtigung nicht genehmigt",
"regenconf_file_updated": "Konfigurationsdatei '{conf}' aktualisiert",
"regenconf_file_removed": "Konfigurationsdatei '{conf}' entfernt",
"regenconf_file_remove_failed": "Konnte die Konfigurationsdatei '{conf}' nicht entfernen",
"postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Sie werden sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn Sie YunoHost trotz dieser Warnung installieren wollen, wiederholen Sie den Befehl mit --force-diskspace",
"regenconf_up_to_date": "Die Konfiguration ist bereits aktuell für die Kategorie '{category}'",
"regenconf_now_managed_by_yunohost": "Die Konfigurationsdatei '{conf}' wird jetzt von YunoHost (Kategorie {category}) verwaltet.",
"regenconf_updated": "Konfiguration aktualisiert für '{category}'",
"regenconf_pending_applying": "Wende die anstehende Konfiguration für die Kategorie {category} an...",
"regenconf_failed": "Konnte die Konfiguration für die Kategorie(n) {categories} nicht neu erstellen",
"regenconf_dry_pending_applying": "Überprüfe die anstehende Konfiguration, welche für die Kategorie {category}' aktualisiert worden wäre…",
"regenconf_would_be_updated": "Die Konfiguration wäre für die Kategorie '{category}' aktualisiert worden",
"restore_system_part_failed": "Die Systemteile '{part:s}' konnten nicht wiederhergestellt werden",
"restore_removing_tmp_dir_failed": "Ein altes, temporäres Directory konnte nicht entfernt werden",
"restore_not_enough_disk_space": "Nicht genug Speicher (Speicher: {free_space:d} B, benötigter Speicher: {needed_space:d} B, Sicherheitspuffer: {margin:d} B)",
"restore_may_be_not_enough_disk_space": "Dein System scheint nicht genug Speicherplatz zu haben (frei: {free_space:d} B, benötigter Platz: {needed_space:d} B, Sicherheitspuffer: {margin:d} B)",
"restore_extracting": "Packe die benötigten Dateien aus dem Archiv aus…",
"restore_already_installed_apps": "Folgende Apps können nicht wiederhergestellt werden, weil sie schon installiert sind: {apps}",
"regex_with_only_domain": "Du kannst regex nicht als Domain verwenden, sondern nur als Pfad"
}

View file

@ -141,12 +141,13 @@
"confirm_app_install_thirdparty": "DANGER! This app is not part of Yunohost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers:s}'",
"custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}",
"diagnosis_basesystem_hardware": "Server hardware architecture is {virt} {arch}",
"diagnosis_basesystem_hardware_board": "Server board model is {model}",
"diagnosis_basesystem_hardware_model": "Server model is {model}",
"diagnosis_basesystem_host": "Server is running Debian {debian_version}",
"diagnosis_basesystem_kernel": "Server is running Linux kernel {kernel_version}",
"diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})",
"diagnosis_basesystem_ynh_main_version": "Server is running YunoHost {main_version} ({repo})",
"diagnosis_basesystem_ynh_inconsistent_versions": "You are running inconsistent versions of the YunoHost packages... most probably because of a failed or partial upgrade.",
"diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage from installing packages from backports, because it's likely to create unstabilities or conflicts on your system.",
"diagnosis_package_installed_from_sury": "Some system packages should be downgraded",
"diagnosis_package_installed_from_sury_details": "Some packages were inadvertendly installed from a third-party repository called Sury. The Yunohost team improved the strategy that handle these packages, but it's expected that some setups that installed PHP7.3 apps while still on Stretch have some remaining inconsistencies. To fix this situation, you should try running the following command: <cmd>{cmd_to_fix}</cmd>",
"diagnosis_display_tip": "To see the issues found, you can go to the Diagnosis section of the webadmin, or run 'yunohost diagnosis show --issues' from the command-line.",
@ -231,6 +232,8 @@
"diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!",
"diagnosis_regenconf_manually_modified": "Configuration file <code>{file}</code> appears to have been manually modified.",
"diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with <cmd>yunohost tools regen-conf {category} --dry-run --with-diff</cmd> and force the reset to the recommended configuration with <cmd>yunohost tools regen-conf {category} --force</cmd>",
"diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly... It's recommended to have at least 16 GB for the root filesystem.",
"diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.",
"diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability",
"diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.",
"diagnosis_description_basesystem": "Base system",
@ -277,7 +280,8 @@
"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_uninstall_app_first": "Those applications are still installed on your domain: {apps}. Please uninstall them before proceeding to domain removal",
"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",
"domains_available": "Available domains:",
@ -287,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.",
@ -359,9 +360,9 @@
"iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it",
"log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'",
"log_link_to_log": "Full log of this operation: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'",
"log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'",
"log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}{name}'",
"log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by <a href=\"#/tools/logs/{name}\">clicking here</a> to get help",
"log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log display {name} --share' to get help",
"log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help",
"log_does_exists": "There is no operation log with the name '{log}', use 'yunohost log list' to see all available operation logs",
"log_operation_unit_unclosed_properly": "Operation unit has not been closed properly",
"log_app_change_url": "Change the URL of the '{}' app",
@ -468,7 +469,7 @@
"migrations_running_forward": "Running migration {id}...",
"migrations_skip_migration": "Skipping migration {id}...",
"migrations_success_forward": "Migration {id} completed",
"migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations migrate`.",
"migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations run`.",
"not_enough_disk_space": "Not enough free space on '{path:s}'",
"invalid_number": "Must be a number",
"operation_interrupted": "The operation was manually interrupted?",
@ -507,6 +508,7 @@
"permission_require_account": "Permission {permission} only makes sense for users having an account, and therefore cannot be enabled for visitors.",
"port_already_closed": "Port {port:d} is already closed for {ip_version:s} connections",
"port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections",
"postinstall_low_rootfsspace": "The root filesystem has a total space less than 10 GB, which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16GB for the root filesystem. If you want to install YunoHost despite this warning, re-run the postinstall with --force-diskspace",
"regenconf_file_backed_up": "Configuration file '{conf}' backed up to '{backup}'",
"regenconf_file_copy_failed": "Could not copy the new configuration file '{new}' to '{conf}'",
"regenconf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by regen-conf (category {category}) but was kept back.",
@ -625,8 +627,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

@ -295,7 +295,7 @@
"restore_extracting": "Eltirante bezonatajn dosierojn el la ar theivo…",
"upnp_port_open_failed": "Ne povis malfermi havenon per UPnP",
"log_app_upgrade": "Ĝisdatigu la aplikon '{}'",
"log_help_to_get_failed_log": "La operacio '{desc}' ne povis finiĝi. Bonvolu dividi la plenan ŝtipon de ĉi tiu operacio per la komando 'yunohost log display {name} --share' por akiri helpon",
"log_help_to_get_failed_log": "La operacio '{desc}' ne povis finiĝi. Bonvolu dividi la plenan ŝtipon de ĉi tiu operacio per la komando 'yunohost log share {name}' por akiri helpon",
"migration_description_0002_migrate_to_tsig_sha256": "Plibonigu sekurecon de DynDNS TSIG-ĝisdatigoj per SHA-512 anstataŭ MD5",
"port_already_closed": "Haveno {port:d} estas jam fermita por {ip_version:s} rilatoj",
"hook_name_unknown": "Nekonata hoko-nomo '{name:s}'",
@ -358,7 +358,7 @@
"dyndns_registration_failed": "Ne povis registri DynDNS-domajnon: {error:s}",
"migration_0003_not_jessie": "La nuna Debian-distribuo ne estas Jessie!",
"user_unknown": "Nekonata uzanto: {user:s}",
"migrations_to_be_ran_manually": "Migrado {id} devas funkcii permane. Bonvolu iri al Iloj → Migradoj en la retpaĝa paĝo, aŭ kuri `yunohost tools migrations migrate`.",
"migrations_to_be_ran_manually": "Migrado {id} devas funkcii permane. Bonvolu iri al Iloj → Migradoj en la retpaĝa paĝo, aŭ kuri `yunohost tools migrations run`.",
"migration_0008_warning": "Se vi komprenas tiujn avertojn kaj volas ke YunoHost preterlasu vian nunan agordon, faru la migradon. Alie, vi ankaŭ povas salti la migradon, kvankam ĝi ne rekomendas.",
"certmanager_cert_renew_success": "Ni Ĉifru atestilon renovigitan por la domajno '{domain:s}'",
"global_settings_reset_success": "Antaŭaj agordoj nun estas rezervitaj al {path:s}",
@ -397,7 +397,7 @@
"password_too_simple_4": "La pasvorto bezonas almenaŭ 12 signojn kaj enhavas ciferon, majuskle, pli malaltan kaj specialajn signojn",
"migration_0003_main_upgrade": "Komencanta ĉefa ĝisdatigo …",
"regenconf_file_updated": "Agordodosiero '{conf}' ĝisdatigita",
"log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log display {name}'",
"log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log show {name}{name}'",
"global_settings_setting_security_nginx_compatibility": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)",
"no_internet_connection": "La servilo ne estas konektita al la interreto",
"migration_0008_dsa": "• La DSA-ŝlosilo estos malŝaltita. Tial vi eble bezonos nuligi spuran averton de via SSH-kliento kaj revizii la fingrospuron de via servilo;",

View file

@ -1,7 +1,7 @@
{
"action_invalid": "Acción no válida '{action:s} 1'",
"admin_password": "Contraseña administrativa",
"admin_password_change_failed": "No se puede cambiar la contraseña",
"admin_password_change_failed": "No se pudo cambiar la contraseña",
"admin_password_changed": "La contraseña de administración fue cambiada",
"app_already_installed": "{app:s} ya está instalada",
"app_argument_choice_invalid": "Use una de estas opciones «{choices:s}» para el argumento «{name:s}»",
@ -12,7 +12,7 @@
"app_install_files_invalid": "Estos archivos no se pueden instalar",
"app_manifest_invalid": "Algo va mal con el manifiesto de la aplicación: {error}",
"app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada",
"app_not_installed": "No se pudo encontrar la aplicación «{app:s}» en la lista de aplicaciones instaladas: {all_apps}",
"app_not_installed": "No se pudo encontrar «{app:s}» en la lista de aplicaciones instaladas: {all_apps}",
"app_not_properly_removed": "La {app:s} 0 no ha sido desinstalada correctamente",
"app_removed": "Eliminado {app:s}",
"app_requirements_checking": "Comprobando los paquetes necesarios para {app}…",
@ -28,8 +28,8 @@
"ask_main_domain": "Dominio principal",
"ask_new_admin_password": "Nueva contraseña administrativa",
"ask_password": "Contraseña",
"backup_app_failed": "No se pudo respaldar la aplicación «{app:s}»",
"backup_archive_app_not_found": "No se pudo encontrar la aplicación «{app:s}» en el archivo de respaldo",
"backup_app_failed": "No se pudo respaldar «{app:s}»",
"backup_archive_app_not_found": "No se pudo encontrar «{app:s}» en el archivo de respaldo",
"backup_archive_name_exists": "Ya existe un archivo de respaldo con este nombre.",
"backup_archive_name_unknown": "Copia de seguridad local desconocida '{name:s}'",
"backup_archive_open_failed": "No se pudo abrir el archivo de respaldo",
@ -44,7 +44,7 @@
"backup_output_directory_forbidden": "Elija un directorio de salida diferente. Las copias de seguridad no se pueden crear en /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives subcarpetas",
"backup_output_directory_not_empty": "Debe elegir un directorio de salida vacío",
"backup_output_directory_required": "Debe proporcionar un directorio de salida para la copia de seguridad",
"backup_running_hooks": "Ejecutando los hooks de copia de seguridad...",
"backup_running_hooks": "Ejecutando los hooks de copia de respaldo...",
"custom_app_url_required": "Debe proporcionar una URL para actualizar su aplicación personalizada {app:s}",
"domain_cert_gen_failed": "No se pudo generar el certificado",
"domain_created": "Dominio creado",
@ -54,7 +54,7 @@
"domain_dyndns_already_subscribed": "Ya se ha suscrito a un dominio de DynDNS",
"domain_dyndns_root_unknown": "Dominio raíz de DynDNS desconocido",
"domain_exists": "El dominio ya existe",
"domain_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminar el dominio",
"domain_uninstall_app_first": "Estas aplicaciones están todavía instaladas en tu dominio:\n{apps}\n\nPor favor desinstálalas utilizando <code>yunohost app remove the_app_id</code> o cambialas a otro dominio usando <code>yunohost app change-url the_app_id</code> antes de continuar con el borrado del dominio.",
"domain_unknown": "Dominio desconocido",
"done": "Hecho.",
"downloading": "Descargando…",
@ -168,9 +168,9 @@
"certmanager_certificate_fetching_or_enabling_failed": "El intento de usar el nuevo certificado para {domain:s} no ha funcionado…",
"certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio «{domain:s}» no ha sido emitido por Let's Encrypt. ¡No se puede renovar automáticamente!",
"certmanager_attempt_to_renew_valid_cert": "¡El certificado para el dominio «{domain:s}» no está a punto de expirar! (Puede usar --force si sabe lo que está haciendo)",
"certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} a través de HTTP. Compruebe que la configuración del DNS y de NGINX es correcta",
"certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} a través de HTTP. Por favor compruebe en los diagnósticos la categoría 'Web'para más información. (Si sabe lo que está haciendo, utilice '--no-checks' para no realizar estas comprobaciones.)",
"certmanager_error_no_A_record": "No se ha encontrado un registro DNS «A» para el dominio {domain:s}. Debe hacer que su nombre de dominio apunte a su máquina para poder instalar un certificado de Let's Encrypt. (Si sabe lo que está haciendo, use «--no-checks» para desactivar esas comprobaciones.)",
"certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio '{domain:s}' es diferente de la IP de este servidor. Si recientemente modificó su registro A, espere a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabe lo que está haciendo, use '--no-checks' para desactivar esos cheques)",
"certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio '{domain:s}' es diferente de la IP de este servidor. Por favor comprueba los 'registros DNS' (básicos) la categoría de diagnósticos para mayor información. Si recientemente modificó su registro 'A', espere a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabe lo que está haciendo, use '--no-checks' para desactivar esos cheques)",
"certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain:s} (archivo: {file:s}), razón: {reason:s}",
"certmanager_cert_install_success_selfsigned": "Instalado correctamente un certificado autofirmado para el dominio «{domain:s}»",
"certmanager_cert_install_success": "Instalado correctamente un certificado de Let's Encrypt para el dominio «{domain:s}»",
@ -184,7 +184,7 @@
"certmanager_unable_to_parse_self_CA_name": "No se pudo procesar el nombre de la autoridad de autofirma (archivo: {file:s})",
"domains_available": "Dominios disponibles:",
"backup_archive_broken_link": "No se pudo acceder al archivo de respaldo (enlace roto a {path:s})",
"certmanager_acme_not_configured_for_domain": "El certificado para el dominio «{domain:s}» no parece que esté instalado correctamente. Ejecute primero «cert-install» para este dominio.",
"certmanager_acme_not_configured_for_domain": "El reto ACME no ha podido ser realizado para {domain} porque su configuración de nginx no tiene el el código correcto... Por favor, asegurate que la configuración de nginx es correcta ejecutando en el terminal `yunohost tools regen-conf nginx --dry-run --with-diff`.",
"certmanager_http_check_timeout": "Tiempo de espera agotado cuando el servidor intentaba conectarse consigo mismo a través de HTTP usando una dirección IP pública (dominio «{domain:s}» con IP «{ip:s}»). Puede que esté experimentando un problema de redirección («hairpinning»), o que el cortafuegos o el enrutador de su servidor esté mal configurado.",
"certmanager_couldnt_fetch_intermediate_cert": "Tiempo de espera agotado intentando obtener el certificado intermedio de Let's Encrypt. Cancelada la instalación o renovación del certificado. Vuelva a intentarlo más tarde.",
"domain_hostname_failed": "No se pudo establecer un nuevo nombre de anfitrión («hostname»). Esto podría causar problemas más tarde (no es seguro... podría ir bien).",
@ -197,16 +197,16 @@
"app_location_unavailable": "Este URL o no está disponible o está en conflicto con otra(s) aplicación(es) instalada(s):\n{apps:s}",
"app_already_up_to_date": "La aplicación {app:s} ya está actualizada",
"app_upgrade_some_app_failed": "No se pudieron actualizar algunas aplicaciones",
"app_make_default_location_already_used": "No puede hacer que la aplicación «{app}» sea la predeterminada en el dominio, «{domain}» ya está siendo usado por otra aplicación «{other_app}»",
"app_upgrade_app_name": "Actualizando ahora {app}…",
"app_make_default_location_already_used": "No pudo hacer que la aplicación «{app}» sea la predeterminada en el dominio, «{domain}» ya está siendo usado por la aplicación «{other_app}»",
"app_upgrade_app_name": "Ahora actualizando {app}…",
"backup_abstract_method": "Este método de respaldo aún no se ha implementado",
"backup_applying_method_borg": "Enviando todos los archivos para la copia de seguridad al repositorio de borg-backup…",
"backup_applying_method_copy": "Copiando todos los archivos a la copia de seguridad…",
"backup_applying_method_copy": "Copiando todos los archivos en la copia de respaldo…",
"backup_applying_method_custom": "Llamando al método de copia de seguridad personalizado «{method:s}»…",
"backup_applying_method_tar": "Creando el archivo TAR de respaldo…",
"backup_archive_system_part_not_available": "La parte del sistema «{part:s}» no está disponible en esta copia de seguridad",
"backup_archive_writing_error": "No se pudieron añadir los archivos «{source:s}» (llamados en el archivo «{dest:s}») para ser respaldados en el archivo comprimido «{archive:s}»",
"backup_ask_for_copying_if_needed": "¿Quiere realizar la copia de seguridad usando {size:s} MB temporalmente? (Se usa este modo ya que algunos archivos no se pudieron preparar usando un método más eficiente.)",
"backup_ask_for_copying_if_needed": "¿Quiere realizar la copia de seguridad usando {size:s}MB temporalmente? (Se usa este modo ya que algunos archivos no se pudieron preparar usando un método más eficiente.)",
"backup_borg_not_implemented": "El método de respaldo de Borg aún no ha sido implementado",
"backup_cant_mount_uncompress_archive": "No se pudo montar el archivo descomprimido como protegido contra escritura",
"backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar el archivo",
@ -218,7 +218,7 @@
"backup_php5_to_php7_migration_may_fail": "No se pudo convertir su archivo para que sea compatible con PHP 7, puede que no pueda restaurar sus aplicaciones de PHP (motivo: {error:s})",
"backup_system_part_failed": "No se pudo respaldar la parte del sistema «{part:s}»",
"backup_with_no_backup_script_for_app": "La aplicación «{app:s}» no tiene un guión de respaldo. Omitiendo.",
"backup_with_no_restore_script_for_app": "La aplicación «{app:s}» no tiene un guión de restauración, no podrá restaurar automáticamente la copia de seguridad de esta aplicación.",
"backup_with_no_restore_script_for_app": "«{app:s}» no tiene un script de restauración, no podá restaurar automáticamente la copia de seguridad de esta aplicación.",
"dyndns_could_not_check_provide": "No se pudo verificar si {provider:s} puede ofrecer {domain:s}.",
"dyndns_domain_not_provided": "El proveedor de DynDNS {provider:s} no puede proporcionar el dominio {domain:s}.",
"experimental_feature": "Aviso : esta funcionalidad es experimental y no se considera estable, no debería usarla a menos que sepa lo que está haciendo.",
@ -303,7 +303,7 @@
"permission_created": "Creado el permiso «{permission:s}»",
"permission_already_exist": "El permiso «{permission}» ya existe",
"pattern_password_app": "Las contraseñas no pueden incluir los siguientes caracteres: {forbidden_chars}",
"migrations_to_be_ran_manually": "La migración {id} hay que ejecutarla manualmente. Vaya a Herramientas → Migraciones en la página web de administración o ejecute `yunohost tools migrations migrate`.",
"migrations_to_be_ran_manually": "La migración {id} hay que ejecutarla manualmente. Vaya a Herramientas → Migraciones en la página web de administración o ejecute `yunohost tools migrations run`.",
"migrations_success_forward": "Migración {id} completada",
"migrations_skip_migration": "Omitiendo migración {id}…",
"migrations_running_forward": "Ejecutando migración {id}…",
@ -408,9 +408,9 @@
"log_app_change_url": "Cambiar el URL de la aplicación «{}»",
"log_operation_unit_unclosed_properly": "La unidad de operación no se ha cerrado correctamente",
"log_does_exists": "No existe ningún registro de actividades con el nombre '{log}', ejecute 'yunohost log list' para ver todos los registros de actividades disponibles",
"log_help_to_get_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, comparta el registro completo de esta operación ejecutando la orden «yunohost log display {name} --share»",
"log_help_to_get_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, comparta el registro completo de esta operación ejecutando la orden «yunohost log share {name}»",
"log_link_to_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, proporcione el registro completo de esta operación <a href=\"#/tools/logs/{name}\">pulsando aquí</a>",
"log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log display {name}»",
"log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log show {name}{name}»",
"log_link_to_log": "Registro completo de esta operación: «<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>»",
"log_category_404": "La categoría de registro «{category}» no existe",
"log_corrupted_md_file": "El archivo de metadatos YAML asociado con el registro está dañado: «{md_file}\nError: {error}»",
@ -446,14 +446,14 @@
"dyndns_could_not_check_available": "No se pudo comprobar si {domain:s} está disponible en {provider:s}.",
"domain_dns_conf_is_just_a_recommendation": "Esta orden muestra la configuración *recomendada*. No configura el DNS en realidad. Es su responsabilidad configurar la zona de DNS en su registrador según esta recomendación.",
"dpkg_lock_not_available": "Esta orden no se puede ejecutar en este momento ,parece que programa está usando el bloqueo de dpkg (el gestor de paquetes del sistema)",
"dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/apt (los gestores de paquetes del sistema) parecen estar en un estado roto... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo dpkg --configure -a`.",
"dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/APT (los gestores de paquetes del sistema) parecen estar mal configurados... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo apt install --fix-broken` y/o `sudo dpkg --configure -a`.",
"confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de Yunohost. La instalación de aplicaciones de terceros puede comprometer la integridad y la seguridad de su sistema. Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers:s}'",
"confirm_app_install_danger": "¡PELIGRO! ¡Se sabe que esta aplicación sigue siendo experimental (si no explícitamente no funciona)! Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers:s}'",
"confirm_app_install_warning": "Aviso: esta aplicación puede funcionar pero no está bien integrada en YunoHost. Algunas herramientas como la autentificación única y respaldo/restauración podrían no estar disponibles. ¿Instalar de todos modos? [{answers:s}] ",
"backup_unable_to_organize_files": "No se pudo usar el método rápido de organización de los archivos en el archivo",
"backup_permission": "Permiso de respaldo para la aplicación {app:s}",
"backup_permission": "Permiso de respaldo para {app:s}",
"backup_output_symlink_dir_broken": "El directorio de su archivo «{path:s}» es un enlace simbólico roto. Tal vez olvidó (re)montarlo o conectarlo al medio de almacenamiento al que apunta.",
"backup_mount_archive_for_restore": "Preparando el archivo para la restauración…",
"backup_mount_archive_for_restore": "Preparando el archivo para restaurarlo…",
"backup_method_tar_finished": "Creado el archivo TAR de respaldo",
"backup_method_custom_finished": "Terminado el método «{method:s}» de respaldo personalizado",
"backup_method_copy_finished": "Terminada la copia de seguridad",
@ -463,10 +463,10 @@
"ask_new_path": "Nueva ruta",
"ask_new_domain": "Nuevo dominio",
"app_upgrade_several_apps": "Las siguientes aplicaciones se actualizarán: {apps}",
"app_start_restore": "Restaurando aplicación «{app}»…",
"app_start_restore": "Restaurando «{app}»…",
"app_start_backup": "Obteniendo archivos para el respaldo de «{app}»…",
"app_start_remove": "Eliminando aplicación «{app}»…",
"app_start_install": "Instalando aplicación «{app}»…",
"app_start_remove": "Eliminando «{app}»…",
"app_start_install": "Instalando «{app}»…",
"app_not_upgraded": "La aplicación '{failed_app}' no se pudo actualizar y, como consecuencia, se cancelaron las actualizaciones de las siguientes aplicaciones: {apps}",
"app_action_cannot_be_ran_because_required_services_down": "Estos servicios necesarios deberían estar funcionando para ejecutar esta acción: {services}. Pruebe a reiniciarlos para continuar (y posiblemente investigar por qué están caídos).",
"already_up_to_date": "Nada que hacer. Todo está actualizado.",
@ -509,7 +509,7 @@
"diagnosis_basesystem_ynh_main_version": "El servidor está ejecutando YunoHost {main_version} ({repo})",
"diagnosis_basesystem_ynh_inconsistent_versions": "Está ejecutando versiones inconsistentes de los paquetes de YunoHost ... probablemente debido a una actualización parcial o fallida.",
"diagnosis_failed_for_category": "Error de diagnóstico para la categoría '{category}': {error}",
"diagnosis_cache_still_valid": "(Caché aún válida para el diagnóstico de {category}. ¡Aún no se ha rediagnosticado!)",
"diagnosis_cache_still_valid": "(Caché aún válida para el diagnóstico de {category}. ¡No se volvera a comprobar de momento!)",
"diagnosis_found_errors_and_warnings": "¡Encontrado(s) error(es) significativo(s) {errors} (y aviso(s) {warnings}) relacionado(s) con {category}!",
"diagnosis_display_tip_web": "Puede ir a la sección de diagnóstico (en la pantalla principal) para ver los problemas encontrados.",
"diagnosis_display_tip_cli": "Puede ejecutar «yunohost diagnosis show --issues» para mostrar los problemas encontrados.",
@ -527,7 +527,7 @@
"diagnosis_no_cache": "Todavía no hay una caché de diagnóstico para la categoría '{category}'",
"diagnosis_ip_no_ipv4": "El servidor no cuenta con ipv4 funcional.",
"diagnosis_ip_not_connected_at_all": "¿¡Está conectado el servidor a internet!?",
"diagnosis_ip_broken_resolvconf": "DNS parece no funcionar en tu servidor, lo que parece estar relacionado con /etc/resolv.conf no apuntando a 127.0.0.1.",
"diagnosis_ip_broken_resolvconf": "La resolución de nombres de dominio parece no funcionar en tu servidor, lo que parece estar relacionado con que <code>/etc/resolv.conf</code> no apunta a <code>127.0.0.1</code>.",
"diagnosis_dns_missing_record": "Según la configuración DNS recomendada, deberías añadir un registro DNS\ntipo: {type}\nnombre: {name}\nvalor: {value}",
"diagnosis_diskusage_low": "El almacenamiento {mountpoint} (en dispositivo {device}) solo tiene {free} ({free_percent}%) de espacio disponible. Ten cuidado.",
"diagnosis_services_bad_status_tip": "Puedes intentar reiniciar el servicio, y si no funciona, echar un vistazo a los logs del servicio usando 'yunohost service log {service}' o a través de la sección 'Servicios' en webadmin.",
@ -535,11 +535,11 @@
"diagnosis_ip_no_ipv6": "El servidor no cuenta con IPv6 funcional.",
"diagnosis_ip_dnsresolution_working": "¡DNS no está funcionando!",
"diagnosis_ip_broken_dnsresolution": "Parece que no funciona la resolución de nombre de dominio por alguna razón... ¿Hay algún firewall bloqueando peticiones DNS?",
"diagnosis_ip_weird_resolvconf": "Parece que DNS funciona, pero ten cuidado, porque estás utilizando /etc/resolv.conf modificado.",
"diagnosis_ip_weird_resolvconf_details": "En su lugar, este fichero debería ser un enlace simbólico a /etc/resolvconf/run/resolv.conf apuntando a 127.0.0.1 (dnsmasq). Los servidores de nombre de domino deben configurarse a través de /etc/resolv.dnsmasq.conf.",
"diagnosis_dns_good_conf": "Buena configuración DNS para el dominio {domain} (categoría {category})",
"diagnosis_dns_bad_conf": "Configuración mala o faltante de los DNS para el dominio {domain} (categoría {category})",
"diagnosis_dns_discrepancy": "El registro DNS con tipo {type} y nombre {name} no se corresponde a la configuración recomendada.\nValor actual: {current}\nValor esperado: {value}",
"diagnosis_ip_weird_resolvconf": "La resolución de nombres de dominio DNS funciona, aunque parece que estás utilizando <code>/etc/resolv.conf</code> personalizada.",
"diagnosis_ip_weird_resolvconf_details": "El fichero <code>/etc/resolv.conf</code> debería ser un enlace simbólico a <code>/etc/resolvconf/run/resolv.conf</code> a su vez debe apuntar a <code>127.0.0.1</code> (dnsmasq). Si lo que quieres es configurar la resolución DNS manualmente, porfavor modifica <code>/etc/resolv.dnsmasq.conf</code>.",
"diagnosis_dns_good_conf": "La configuración de registros DNS es correcta para {domain} (categoría {category})",
"diagnosis_dns_bad_conf": "Algunos registros DNS faltan o están mal cofigurados para el dominio {domain} (categoría {category})",
"diagnosis_dns_discrepancy": "El siguiente registro DNS parace que no sigue la configuración recomendada <br>Tipo: <code>{type}</code><br>Nombre: <code>{name}</code><br>Valor Actual: <code>{current}</code><br>Valor esperado: <code>{value}</code>",
"diagnosis_services_bad_status": "El servicio {service} está {status} :(",
"diagnosis_diskusage_verylow": "El almacenamiento {mountpoint} (en el dispositivo {device}) sólo tiene {free} ({free_percent}%) de espacio disponible. Deberías considerar la posibilidad de limpiar algo de espacio.",
"diagnosis_diskusage_ok": "¡El almacenamiento {mountpoint} (en el dispositivo {device}) todavía tiene {free} ({free_percent}%) de espacio libre!",
@ -556,8 +556,8 @@
"diagnosis_mail_ougoing_port_25_ok": "El puerto de salida 25 no esta bloqueado y los correos electrónicos pueden ser enviados a otros servidores.",
"diagnosis_mail_outgoing_port_25_blocked": "El puerto de salida 25 parece estar bloqueado. Intenta desbloquearlo con el panel de configuración de tu proveedor de servicios de Internet (o proveedor de halbergue). Mientras tanto, el servidor no podrá enviar correos electrónicos a otros servidores.",
"diagnosis_regenconf_allgood": "Todos los archivos de configuración están en linea con la configuración recomendada!",
"diagnosis_regenconf_manually_modified": "El archivo de configuración {file} fue modificado manualmente.",
"diagnosis_regenconf_manually_modified_details": "Esto este probablemente BIEN siempre y cuando sepas lo que estas haciendo ;) !",
"diagnosis_regenconf_manually_modified": "El archivo de configuración {file} parece que ha sido modificado manualmente.",
"diagnosis_regenconf_manually_modified_details": "¡Esto probablemente esta BIEN si sabes lo que estás haciendo! YunoHost dejará de actualizar este fichero automáticamente... Pero ten en cuenta que las actualizaciones de YunoHost pueden contener importantes cambios que están recomendados. Si quieres puedes comprobar las diferencias mediante <cmd>yunohost tools regen-conf {category} --dry-run --with-diff</cmd> o puedes forzar el volver a las opciones recomendadas mediante el comando <cmd>yunohost tools regen-conf {category} --force</cmd>",
"diagnosis_regenconf_manually_modified_debian": "El archivos de configuración {file} fue modificado manualmente comparado con el valor predeterminado de Debian.",
"diagnosis_regenconf_manually_modified_debian_details": "Esto este probablemente BIEN, pero igual no lo pierdas de vista...",
"diagnosis_security_all_good": "Ninguna vulnerabilidad critica de seguridad fue encontrada.",
@ -586,26 +586,26 @@
"log_app_config_apply": "Aplica la configuración de la aplicación '{}'",
"log_app_config_show_panel": "Muestra el panel de configuración de la aplicación '{}'",
"log_app_action_run": "Inicializa la acción de la aplicación '{}'",
"group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en el grupo de sistema, pero YunoHost lo suprimirá …",
"group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en los grupos del sistema, pero YunoHost lo suprimirá …",
"global_settings_setting_pop3_enabled": "Habilita el protocolo POP3 para el servidor de correo electrónico",
"domain_cannot_remove_main_add_new_one": "No se puede remover '{domain:s}' porque es su principal y único dominio. Primero debe agregar un nuevo dominio con la linea de comando 'yunohost domain add <another-domain.com>', entonces configurarlo como dominio principal con 'yunohost domain main-domain -n <another-domain.com>' y finalmente borrar el dominio '{domain:s}' con 'yunohost domain remove {domain:s}'.'",
"diagnosis_never_ran_yet": "Este servidor todavía no tiene reportes de diagnostico. Puede iniciar un diagnostico completo desde la interface administrador web o con la linea de comando 'yunohost diagnosis run'.",
"diagnosis_unknown_categories": "Las siguientes categorías están desconocidas: {categories}",
"diagnosis_http_unreachable": "El dominio {domain} esta fuera de alcance desde internet y a través de HTTP.",
"diagnosis_http_bad_status_code": "El sistema de diagnostico no pudo comunicarse con su servidor. Puede ser otra maquina que contesto en lugar del servidor. Debería verificar en su firewall que el re-direccionamiento del puerto 80 esta correcto.",
"diagnosis_http_bad_status_code": "Parece que otra máquina (quizás el router de conexión a internet) haya respondido en vez de tu servidor.<br>1. La causa más común es que el puerto 80 (y el 443) <a href='https://yunohost.org/isp_box_config'>no hayan sido redirigidos a tu servidor</a>.<br>2. En situaciones más complejas: asegurate de que ni el cortafuegos ni el proxy inverso están interfiriendo.",
"diagnosis_http_connection_error": "Error de conexión: Ne se pudo conectar al dominio solicitado.",
"diagnosis_http_timeout": "El intento de contactar a su servidor desde internet corrió fuera de tiempo. Al parece esta incomunicado. Debería verificar que nginx corre en el puerto 80, y que la redireción del puerto 80 no interfiere con en el firewall.",
"diagnosis_http_timeout": "Tiempo de espera agotado al intentar contactar tu servidor desde el exterior. Parece que no sea alcanzable.<br>1. La causa más común es que el puerto 80 (y el 443) <a href='https://yunohost.org/isp_box_config'>no estén correctamente redirigidos a tu servidor</a>.<br>2. Deberías asegurarte que el servicio nginx está en marcha.<br>3. En situaciones más complejas: asegurate de que ni el cortafuegos ni el proxy inverso estén interfiriendo.",
"diagnosis_http_ok": "El Dominio {domain} es accesible desde internet a través de HTTP.",
"diagnosis_http_could_not_diagnose": "No se pudo verificar si el dominio es accesible desde internet.",
"diagnosis_http_could_not_diagnose_details": "Error: {error}",
"diagnosis_ports_forwarding_tip": "Para solucionar este incidente, debería configurar el \"port forwading\" en su router como especificado en https://yunohost.org/isp_box_config",
"diagnosis_ports_forwarding_tip": "Para solucionar este incidente, lo más seguro deberías configurar la redirección de los puertos en el router como se especifica en <a href='https://yunohost.org/isp_box_config'>https://yunohost.org/isp_box_config</a>",
"certmanager_warning_subdomain_dns_record": "El subdominio '{subdomain:s}' no se resuelve en la misma dirección IP que '{domain:s}'. Algunas funciones no estarán disponibles hasta que solucione esto y regenere el certificado.",
"domain_cannot_add_xmpp_upload": "No puede agregar dominios que comiencen con 'xmpp-upload'. Este tipo de nombre está reservado para la función de carga XMPP integrada en YunoHost.",
"yunohost_postinstall_end_tip": "¡La post-instalación completada! Para finalizar su configuración, considere:\n - agregar un primer usuario a través de la sección 'Usuarios' del webadmin (o 'yunohost user create <username>' en la línea de comandos);\n - diagnostique problemas potenciales a través de la sección 'Diagnóstico' de webadmin (o 'ejecución de diagnóstico yunohost' en la línea de comandos);\n - leyendo las partes 'Finalizando su configuración' y 'Conociendo a Yunohost' en la documentación del administrador: https://yunohost.org/admindoc.",
"diagnosis_dns_point_to_doc": "Por favor, consulta la documentación en <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> si necesitas ayuda para configurar los registros DNS.",
"diagnosis_ip_global": "IP Global: <code>{global}</code>",
"diagnosis_mail_outgoing_port_25_ok": "El servidor de email SMTP puede mandar emails (puerto saliente 25 no está bloqueado).",
"diagnosis_mail_outgoing_port_25_blocked_details": "Deberías intentar desbloquear el puerto 25 saliente en la interfaz de tu router o en la interfaz de tu provedor de hosting. (Algunos hosting pueden necesitar que les abras un ticket de soporte para esto).",
"diagnosis_mail_outgoing_port_25_blocked_details": "Primeramente deberías intentar desbloquear el puerto de salida 25 en la interfaz de control de tu router o en la interfaz de tu provedor de hosting. (Algunos hosting pueden necesitar que les abras un ticket de soporte para esto).",
"diagnosis_swap_tip": "Por favor tenga cuidado y sepa que si el servidor contiene swap en una tarjeta SD o un disco duro de estado sólido, esto reducirá drásticamente la vida útil del dispositivo.",
"diagnosis_domain_expires_in": "{domain} expira en {days} días.",
"diagnosis_domain_expiration_error": "¡Algunos dominios expirarán MUY PRONTO!",
@ -631,5 +631,62 @@
"app_manifest_install_ask_path": "Seleccione el path donde esta aplicación debería ser instalada",
"app_manifest_install_ask_domain": "Seleccione el dominio donde esta app debería ser instalada",
"app_label_deprecated": "Este comando está depreciado! Favor usar el nuevo comando 'yunohost user permission update' para administrar la etiqueta de app.",
"app_argument_password_no_default": "Error al interpretar argumento de contraseña'{name}': El argumento de contraseña no puede tener un valor por defecto por razón de seguridad"
"app_argument_password_no_default": "Error al interpretar argumento de contraseña'{name}': El argumento de contraseña no puede tener un valor por defecto por razón de seguridad",
"migration_0015_not_enough_free_space": "¡El espacio es muy bajo en `/var/`! Deberías tener almenos 1Gb de espacio libre para ejecutar la migración.",
"migration_0015_not_stretch": "¡La distribución actual de Debian no es Stretch!",
"migration_0015_yunohost_upgrade": "Iniciando la actualización del núcleo de YunoHost...",
"migration_0015_still_on_stretch_after_main_upgrade": "Algo fue mal durante la actualización principal, el sistema parece que está todavía en Debian Stretch",
"migration_0015_main_upgrade": "Comenzando la actualización principal...",
"migration_0015_patching_sources_list": "Adaptando las sources.lists...",
"migration_0015_start": "Comenzando la migración a Buster",
"migration_description_0019_extend_permissions_features": "Extiende/rehaz el sistema de gestión de permisos de la aplicación",
"migration_description_0018_xtable_to_nftable": "Migra las viejas reglas de tráfico de red al nuevo sistema nftable",
"migration_description_0017_postgresql_9p6_to_11": "Migra las bases de datos de PostgreSQL 9.6 a 11",
"migration_description_0016_php70_to_php73_pools": "Migra el «pool» de ficheros php7.0-fpm a php7.3",
"migration_description_0015_migrate_to_buster": "Actualiza el sistema a Debian Buster y YunoHost 4.x",
"migrating_legacy_permission_settings": "Migrando los antiguos parámetros de permisos...",
"invalid_regex": "Regex no valido: «{regex:s}»",
"global_settings_setting_backup_compress_tar_archives": "Cuando se creen nuevas copias de respaldo, comprimir los archivos (.tar.gz) en lugar de descomprimir los archivos (.tar). N.B.: activar esta opción quiere decir que los archivos serán más pequeños pero que el proceso tardará más y utilizará más CPU.",
"global_settings_setting_smtp_relay_password": "Clave de uso del SMTP",
"global_settings_setting_smtp_relay_user": "Cuenta de uso de SMTP",
"global_settings_setting_smtp_relay_port": "Puerto de envio / relay SMTP",
"global_settings_setting_smtp_relay_host": "El servidor relay de SMTP para enviar correo en lugar de esta instalación YunoHost. Útil si estás en una de estas situaciones: tu puerto 25 esta bloqueado por tu ISP o VPS, si estás en usado una IP marcada como residencial o DUHL, si no puedes configurar un DNS inverso o si el servidor no está directamente expuesto a internet y quieres utilizar otro servidor para enviar correos.",
"global_settings_setting_smtp_allow_ipv6": "Permitir el uso de IPv6 para enviar y recibir correo",
"domain_name_unknown": "Dominio «{domain}» desconocido",
"diagnosis_processes_killed_by_oom_reaper": "Algunos procesos fueron terminados por el sistema recientemente porque se quedó sin memoria. Típicamente es sintoma de falta de memoria o de un proceso que se adjudicó demasiada memoria.<br>Resumen de los procesos terminados:<br>\n{kills_summary}",
"diagnosis_http_nginx_conf_not_up_to_date_details": "Para arreglar este asunto, estudia las diferencias mediante el comando <cmd>yunohost tools regen-conf nginx --dry-run --with-diff</cmd> y si te parecen bien aplica los cambios mediante <cmd>yunohost tools regen-conf nginx --force</cmd>.",
"diagnosis_http_nginx_conf_not_up_to_date": "Parece que la configuración nginx de este dominio haya sido modificada manualmente, esto no deja que YunoHost pueda diagnosticar si es accesible mediante HTTP.",
"diagnosis_http_partially_unreachable": "El dominio {domain} parece que no es accesible mediante HTTP desde fuera de la red local mediante IPv{failed}, aunque si que funciona mediante IPv{passed}.",
"diagnosis_http_hairpinning_issue_details": "Esto quizás es debido a tu router o máquina en el ISP. Como resultado, la gente fuera de tu red local podrá acceder a tu servidor como es de esperar, pero no así las persona que estén dentro de la red local (como tu probablemente) o cuando usen el nombre de dominio o la IP global. Quizás puedes mejorar o arreglar esta situación leyendo <a href='https://yunohost.org/dns_local_network'>https://yunohost.org/dns_local_network</a>",
"diagnosis_http_hairpinning_issue": "Parece que tu red local no tiene la opción hairpinning activada.",
"diagnosis_ports_partially_unreachable": "El port {port} no es accesible desde el exterior mediante IPv{failed}.",
"diagnosis_mail_queue_too_big": "Demasiados correos electrónicos pendientes en la cola ({nb_pending} correos electrónicos)",
"diagnosis_mail_queue_unavailable_details": "Error: {error}",
"diagnosis_mail_queue_unavailable": "No se ha podido consultar el número de correos electrónicos pendientes en la cola",
"diagnosis_mail_queue_ok": "{nb_pending} correos esperando e la cola de correos electrónicos",
"diagnosis_mail_blacklist_website": "Cuando averigües y arregles el motivo por el que aprareces en la lista maligna, no dudes en solicitar que tu IP o dominio sea retirado de la {blacklist_website}",
"diagnosis_mail_blacklist_reason": "El motivo de estar en la lista maligna es: {reason}",
"diagnosis_mail_blacklist_listed_by": "Tu IP o dominio <code>{item}</code> está marcado como maligno en {blacklist_name}",
"diagnosis_mail_blacklist_ok": "Las IP y los dominios utilizados en este servidor no parece que estén en ningún listado maligno (blacklist)",
"diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "El DNS inverso actual es: <code>{rdns_domain}</code><br>Valor esperado: <code>{ehlo_domain}</code>",
"diagnosis_mail_fcrdns_different_from_ehlo_domain": "La resolución de DNS inverso no está correctamente configurada mediante IPv{ipversion}. Algunos correos pueden fallar al ser enviados o pueden ser marcados como basura.",
"diagnosis_mail_fcrdns_nok_alternatives_6": "Algunos proveedores no permiten configurar el DNS inverso (o su funcionalidad puede estar rota...). Si tu DNS inverso está configurado correctamente para IPv4, puedes intentar deshabilitarlo para IPv6 cuando envies correos mediante el comando <cmd>yunohost settings set smtp.allow_ipv6 -v off</cmd>. Nota: esta solución quiere decir que no podrás enviar ni recibir correos con los pocos servidores que utilizan exclusivamente IPv6.",
"diagnosis_mail_fcrdns_nok_alternatives_4": "Algunos proveedores no te permitirán que configures un DNS inverso (o puede que esta opción esté rota...). Si estás sufriendo problemas por este asunto, quizás te sirvan las siguientes soluciones:<br>- Algunos ISP proporcionan una alternativa mediante <a href='https://yunohost.org/#/smtp_relay'>el uso de un relay de servidor de correo</a> aunque esto implica que el relay podrá espiar tu tráfico de correo electrónico.<br>- Una solución amigable con la privacidad es utilizar una VPN con una *IP pública dedicada* para evitar este tipo de limitaciones. Mira en <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a><br>- Quizás tu solución sea <a href='https://yunohost.org/#/isp'>cambiar de proveedor de internet</a>",
"diagnosis_mail_fcrdns_nok_details": "Primero deberías intentar configurar el DNS inverso mediante <code>{ehlo_domain}</code> en la interfaz de internet de tu router o en la de tu proveedor de internet. (Algunos proveedores de internet en ocasiones necesitan que les solicites un ticket de soporte para ello).",
"diagnosis_mail_fcrdns_dns_missing": "No hay definida ninguna DNS inversa mediante IPv{ipversion}. Algunos correos puede que fallen al enviarse o puede que se marquen como basura.",
"diagnosis_mail_fcrdns_ok": "¡Las DNS inversas están bien configuradas!",
"diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}",
"diagnosis_mail_ehlo_could_not_diagnose": "No pudimos diagnosticar si el servidor de correo postfix es accesible desde el exterior utilizando IPv{ipversion}.",
"diagnosis_mail_ehlo_wrong_details": "El EHLO recibido por el diagnosticador remoto de IPv{ipversion} es diferente del dominio de tu servidor.<br>EHLO recibido: <code>{wrong_ehlo}</code><br>EHLO esperado: <code>{right_ehlo}</code><br> La causa más común de este error suele ser que el puerto 25 <a href='https://yunohost.org/isp_box_config'>no está correctamente enrutado hacia tu servidor</a>. Así mismo asegurate que ningún firewall ni reverse-proxy está interfiriendo.",
"diagnosis_mail_ehlo_wrong": "Un servidor diferente de SMTP está respondiendo mediante IPv{ipversion}. Es probable que tu servidor no pueda recibir correos.",
"diagnosis_mail_ehlo_bad_answer_details": "Podría ser debido a otra máquina en lugar de tu servidor.",
"diagnosis_mail_ehlo_bad_answer": "Un servicio que no es SMTP respondió en el puerto 25 mediante IPv{ipversion}",
"diagnosis_mail_ehlo_unreachable_details": "No pudo abrirse la conexión en el puerto 25 de tu servidor mediante IPv{ipversion}. Parece que no se puede contactar.<br>1. La causa más común en estos casos suele ser que el puerto 25 <a href='https://yunohost.org/isp_box_config'>no está correctamente redireccionado a tu servidor</a>.<br>2. También deberías asegurarte que el servicio postfix está en marcha.<br>3. En casos más complejos: asegurate que no estén interfiriendo ni el firewall ni el reverse-proxy.",
"diagnosis_mail_ehlo_unreachable": "El servidor de correo SMTP no puede contactarse desde el exterior mediante IPv{ipversion}. No puede recibir correos",
"diagnosis_mail_ehlo_ok": "¡El servidor de correo SMTP puede contactarse desde el exterior por lo que puede recibir correos!",
"diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.<br> - Algunos proporcionan una alternativa usando <a href='https://yunohost.org/#/smtp_relay'>un relay como servidor de correo</a> lo que implica que el relay podrá espiar tu trafico de correo.<br>- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a><br>- Otra alternativa es cambiar de proveedor de inteernet a <a href='https://yunohost.org/#/isp'>uno más amable con la Neutralidad de la Red</a>",
"diagnosis_backports_in_sources_list": "Parece que apt (el gestor de paquetes) está configurado para usar el repositorio backports. A menos que realmente sepas lo que estás haciendo, desaconsejamos absolutamente instalar paquetes desde backports, ya que pueden provocar comportamientos intestables o conflictos en el sistema.",
"diagnosis_basesystem_hardware_model": "El modelo de servidor es {model}",
"additional_urls_already_removed": "La URL adicional «{url:s}» ya se ha eliminado para el permiso «{permission:s}»",
"additional_urls_already_added": "La URL adicional «{url:s}» ya se ha añadido para el permiso «{permission:s}»"
}

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: {apps}. Veuillez dabord les désinstaller avant de supprimer ce domaine",
"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 …",
@ -129,7 +129,7 @@
"service_removed": "Le service « {service:s} » a été supprimé",
"service_start_failed": "Impossible de démarrer le service '{service:s}'\n\nJournaux historisés récents : {logs:s}",
"service_started": "Le service « {service:s} » a été démarré",
"service_stop_failed": "Impossible darrêter le service '{service:s}'\n\nJournaux historisés récents : {logs:s}",
"service_stop_failed": "Impossible darrêter le service '{service:s}'\n\nJournaux récents de service : {logs:s}",
"service_stopped": "Le service « {service:s} » a été arrêté",
"service_unknown": "Le service '{service:s}' est inconnu",
"ssowat_conf_generated": "La configuration de SSOwat a été regénérée",
@ -252,11 +252,11 @@
"app_upgrade_some_app_failed": "Certaines applications nont pas été mises à jour",
"dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.",
"dyndns_domain_not_provided": "Le fournisseur DynDNS {provider:s} ne peut pas fournir le domaine {domain:s}.",
"app_make_default_location_already_used": "Impossible de configurer lapplication '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par lapplication '{other_app}'",
"app_make_default_location_already_used": "Impossible de configurer lapplication '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par l'application '{other_app}'",
"app_upgrade_app_name": "Mise à jour de {app}...",
"backup_output_symlink_dir_broken": "Votre répertoire darchivage '{path:s}' est un lien symbolique brisé. Peut-être avez-vous oublié de re/monter ou de brancher le support de stockage sur lequel il pointe.",
"migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.",
"migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans linterface admin, ou lancer `yunohost tools migrations migrate`.",
"migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans linterface admin, ou lancer `yunohost tools migrations run`.",
"migrations_need_to_accept_disclaimer": "Pour lancer la migration {id}, vous devez accepter cet avertissement :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec loption --accept-disclaimer.",
"service_description_avahi-daemon": "Vous permet datteindre votre serveur en utilisant « yunohost.local » sur votre réseau local",
"service_description_dnsmasq": "Gère la résolution des noms de domaine (DNS)",
@ -277,10 +277,10 @@
"log_corrupted_md_file": "Le fichier YAML de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}",
"log_category_404": "Le journal de la catégorie '{category}' nexiste pas",
"log_link_to_log": "Journal complet de cette opération : '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\"> {desc} </a>'",
"log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log display {name}'",
"log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log show {name}{name}'",
"log_link_to_failed_log": "Lopération '{desc}' a échoué ! Pour obtenir de laide, merci de partager le journal de lopération en <a href=\"#/tools/logs/{name}\">cliquant ici</a>",
"backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge PHP 7, vous pourriez ne plus pouvoir restaurer vos applications PHP (cause : {error:s})",
"log_help_to_get_failed_log": "Lopération '{desc}' a échoué ! Pour obtenir de laide, merci de partager le journal de lopération en utilisant la commande 'yunohost log display {name} --share'",
"log_help_to_get_failed_log": "Lopération '{desc}' a échoué ! Pour obtenir de laide, merci de partager le journal de lopération en utilisant la commande 'yunohost log share {name}'",
"log_does_exists": "Il ny a pas de journal des opérations avec le nom '{log}', utilisez 'yunohost log list' pour voir tous les journaux dopérations disponibles",
"log_operation_unit_unclosed_properly": "Lopération ne sest pas terminée correctement",
"log_app_change_url": "Changer lURL de lapplication '{}'",
@ -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}...",
@ -419,7 +419,7 @@
"mailbox_disabled": "La boîte aux lettres est désactivée pour lutilisateur {user:s}",
"app_action_broke_system": "Cette action semble avoir cassé des services importants : {services}",
"apps_already_up_to_date": "Toutes les applications sont déjà à jour",
"migration_0011_create_group": "Création dun groupe pour chaque utilisateur…",
"migration_0011_create_group": "Création d'un groupe pour chaque utilisateur…",
"migration_0011_done": "Migration terminée. Vous êtes maintenant en mesure de gérer des groupes dutilisateurs.",
"migrations_must_provide_explicit_targets": "Vous devez fournir des cibles explicites lorsque vous utilisez '--skip' ou '--force-rerun'",
"migrations_no_such_migration": "Il ny a pas de migration appelée '{id}'",
@ -649,7 +649,7 @@
"global_settings_setting_backup_compress_tar_archives": "Compresser les archives (.tar.gz) au lieu des archives non-compressées lors de la création des backups. N.B. : activer cette option permet d'obtenir des sauvegardes plus légères, mais leur création sera significativement plus longue et plus gourmande en CPU.",
"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 legacy a échoué :",
"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": "É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 :(...",
@ -669,8 +669,8 @@
"diagnosis_package_installed_from_sury_details": "Certains paquets ont été installés par inadvertance à partir d'un dépôt tiers appelé Sury. L'équipe YunoHost a amélioré la stratégie de gestion de ces paquets, mais on s'attend à ce que certaines configurations qui ont installé des applications PHP7.3 tout en étant toujours sur Stretch présentent des incohérences. Pour résoudre cette situation, vous devez essayer d'exécuter la commande suivante : <cmd> {cmd_to_fix} </cmd>",
"app_argument_password_no_default": "Erreur lors de l'analyse de l'argument de mot de passe '{name}' : l'argument de mot de passe ne peut pas avoir de valeur par défaut pour des raisons de sécurité",
"pattern_email_forward": "Il doit s'agir d'une adresse électronique valide, le symbole '+' étant accepté (par exemples : johndoe@exemple.com ou bien johndoe+yunohost@exemple.com)",
"global_settings_setting_smtp_relay_password": "Mot de passe SMTP du serveur de courrier électronique",
"diagnosis_package_installed_from_sury": "Certains paquets du système devraient être rétrograder vers une version moins récente",
"global_settings_setting_smtp_relay_password": "Mot de passe du relais de l'hôte SMTP",
"diagnosis_package_installed_from_sury": "Des paquets du système devraient être rétrogradé de version",
"additional_urls_already_added": "URL supplémentaire '{url:s}' déjà ajoutée pour la permission '{permission:s}'",
"unknown_main_domain_path": "Domaine ou chemin inconnu pour '{app}'. Vous devez spécifier un domaine et un chemin pour pouvoir spécifier une URL pour l'autorisation.",
"show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, car l'URL de l'autorisation '{permission}' est une expression régulière",
@ -687,5 +687,14 @@
"invalid_regex": "Regex non valide : '{regex:s}'",
"domain_name_unknown": "Domaine '{domain}' inconnu",
"app_label_deprecated": "Cette commande est obsolète ! Veuillez utiliser la nouvelle commande 'yunohost user permission update' pour gérer l'étiquette de l'application.",
"additional_urls_already_removed": "URL supplémentaire '{url:s}' déjà supprimée pour la permission '{permission:s}'"
"additional_urls_already_removed": "URL supplémentaire '{url:s}' déjà supprimées pour la permission '{permission:s}'",
"migration_0019_rollback_success": "Retour à l'état antérieur du système.",
"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.",
"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

@ -9,22 +9,22 @@
"backup_created": "Backup completo",
"backup_invalid_archive": "Archivio di backup non valido",
"backup_output_directory_not_empty": "Dovresti scegliere una cartella di output vuota",
"domain_created": "Il dominio è stato creato",
"domain_exists": "Il dominio è già esistente",
"ldap_initialized": "LDAP è stato inizializzato",
"pattern_email": "L'indirizzo email deve essere valido (es. someone@domain.org)",
"domain_created": "Dominio creato",
"domain_exists": "Il dominio esiste già",
"ldap_initialized": "LDAP inizializzato",
"pattern_email": "L'indirizzo email deve essere valido, senza simboli '+' (es. tizio@dominio.com)",
"pattern_mailbox_quota": "La dimensione deve avere un suffisso b/k/M/G/T o 0 per disattivare la quota",
"port_already_opened": "La porta {port:d} è già aperta per {ip_version:s} connessioni",
"service_add_failed": "Impossibile aggiungere il servizio '{service:s}'",
"service_cmd_exec_failed": "Impossibile eseguire il comando '{command:s}'",
"service_disabled": "Il servizio '{service:s}' è stato disattivato",
"service_disabled": "Il servizio '{service:s}' non partirà più al boot di sistema.",
"service_remove_failed": "Impossibile rimuovere il servizio '{service:s}'",
"service_removed": "Il servizio '{service:s}' è stato rimosso",
"service_removed": "Servizio '{service:s}' rimosso",
"service_stop_failed": "Impossibile fermare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"system_username_exists": "il nome utente esiste già negli utenti del sistema",
"unrestore_app": "L'applicazione '{app:s}' non verrà ripristinata",
"upgrading_packages": "Aggiornamento dei pacchetti",
"user_deleted": "L'utente è stato cancellato",
"system_username_exists": "Il nome utente esiste già negli utenti del sistema",
"unrestore_app": "{app:s} non verrà ripristinata",
"upgrading_packages": "Aggiornamento dei pacchetti...",
"user_deleted": "Utente cancellato",
"admin_password": "Password dell'amministrazione",
"admin_password_change_failed": "Impossibile cambiare la password",
"admin_password_changed": "La password d'amministrazione è stata cambiata",
@ -63,54 +63,54 @@
"backup_output_directory_required": "Devi fornire una directory di output per il backup",
"backup_running_hooks": "Esecuzione degli hook di backup…",
"custom_app_url_required": "Devi fornire un URL per essere in grado di aggiornare l'applicazione personalizzata {app:s}",
"domain_creation_failed": "Impossibile creare un dominio",
"domain_deleted": "Il dominio è stato cancellato",
"domain_deletion_failed": "Impossibile cancellare il dominio",
"domain_creation_failed": "Impossibile creare il dominio {domain}: {error}",
"domain_deleted": "Dominio cancellato",
"domain_deletion_failed": "Impossibile cancellare il dominio {domain}: {error}",
"domain_dyndns_already_subscribed": "Hai già sottoscritto un dominio DynDNS",
"domain_dyndns_root_unknown": "Dominio radice DynDNS sconosciuto",
"domain_hostname_failed": "La definizione del nuovo hostname è fallita",
"domain_uninstall_app_first": "Una o più applicazioni sono installate su questo dominio. Disinstalla loro prima di procedere alla cancellazione di un dominio",
"domain_hostname_failed": "Impossibile impostare il nuovo hostname. Potrebbe causare problemi in futuro (o anche no).",
"domain_uninstall_app_first": "Queste applicazioni sono già installate su questo dominio:\n{apps}\n\nDisinstallale eseguendo 'yunohost app remove app_id' o spostale in un altro dominio eseguendo 'yunohost app change-url app_id' prima di procedere alla cancellazione del dominio",
"domain_unknown": "Dominio sconosciuto",
"done": "Terminato",
"domains_available": "Domini disponibili:",
"downloading": "Scaricamento…",
"dyndns_cron_installed": "Il cronjob DynDNS è stato installato",
"dyndns_cron_remove_failed": "Impossibile rimuovere il cronjob DynDNS",
"dyndns_cron_removed": "Il cronjob DynDNS è stato rimosso",
"dyndns_cron_installed": "Cronjob DynDNS creato",
"dyndns_cron_remove_failed": "Impossibile rimuovere il cronjob DynDNS perchè: {error}",
"dyndns_cron_removed": "Cronjob DynDNS rimosso",
"dyndns_ip_update_failed": "Impossibile aggiornare l'indirizzo IP in DynDNS",
"dyndns_ip_updated": "Il tuo indirizzo IP è stato aggiornato in DynDNS",
"dyndns_key_generating": "Si sta generando la chiave DNS, potrebbe richiedere del tempo…",
"dyndns_ip_updated": "Il tuo indirizzo IP è stato aggiornato su DynDNS",
"dyndns_key_generating": "Generando la chiave DNS... Potrebbe richiedere del tempo.",
"dyndns_key_not_found": "La chiave DNS non è stata trovata per il dominio",
"dyndns_no_domain_registered": "Nessuno dominio è stato registrato con DynDNS",
"dyndns_registered": "Il dominio DynDNS è stato registrato",
"dyndns_no_domain_registered": "Nessuno dominio registrato con DynDNS",
"dyndns_registered": "Dominio DynDNS registrato",
"dyndns_registration_failed": "Non è possibile registrare il dominio DynDNS: {error:s}",
"dyndns_unavailable": "Dominio {domain:s} non disponibile.",
"dyndns_unavailable": "Il dominio {domain:s} non disponibile.",
"executing_command": "Esecuzione del comando '{command:s}'…",
"executing_script": "Esecuzione dello script '{script:s}'…",
"extracting": "Estrazione",
"extracting": "Estrazione...",
"field_invalid": "Campo '{:s}' non valido",
"firewall_reload_failed": "Impossibile ricaricare il firewall",
"firewall_reloaded": "Il firewall è stato ricaricato",
"firewall_reloaded": "Firewall ricaricato",
"firewall_rules_cmd_failed": "Alcune regole del firewall sono fallite. Per ulteriori informazioni, vedi il registro.",
"hook_exec_failed": "L'esecuzione dello script è fallita: {path:s}",
"hook_exec_not_terminated": "L'esecuzione dello script non è stata terminata: {path:s}",
"hook_exec_failed": "Impossibile eseguire lo script: {path:s}",
"hook_exec_not_terminated": "Los script non è stato eseguito correttamente: {path:s}",
"hook_name_unknown": "Nome di hook '{name:s}' sconosciuto",
"installation_complete": "Installazione completata",
"installation_failed": "Installazione fallita",
"installation_failed": "Qualcosa è andato storto durante l'installazione",
"ip6tables_unavailable": "Non puoi giocare con ip6tables qui. O sei in un container o il tuo kernel non lo supporta",
"iptables_unavailable": "Non puoi giocare con iptables qui. O sei in un container o il tuo kernel non lo supporta",
"ldap_init_failed_to_create_admin": "L'inizializzazione LDAP non è riuscita a creare un utente admin",
"mail_alias_remove_failed": "Impossibile rimuovere l'alias mail '{mail:s}'",
"mail_domain_unknown": "Dominio d'indirizzo mail '{domain:s}' sconosciuto",
"mail_domain_unknown": "Indirizzo mail non valido per il dominio '{domain:s}'. Usa un dominio gestito da questo server.",
"mail_forward_remove_failed": "Impossibile rimuovere la mail inoltrata '{mail:s}'",
"mailbox_used_space_dovecot_down": "Il servizio di posta elettronica Dovecot deve essere attivato se vuoi riportare lo spazio usato dalla posta elettronica",
"mailbox_used_space_dovecot_down": "La casella di posta elettronica Dovecot deve essere attivato se vuoi recuperare lo spazio usato dalla posta elettronica",
"main_domain_change_failed": "Impossibile cambiare il dominio principale",
"main_domain_changed": "Il dominio principale è stato cambiato",
"no_internet_connection": "Il server non è collegato a Internet",
"not_enough_disk_space": "Non c'è abbastanza spazio libero in '{path:s}'",
"package_unknown": "Pacchetto '{pkgname}' sconosciuto",
"packages_upgrade_failed": "Impossibile aggiornare tutti i pacchetti",
"pattern_backup_archive_name": "Deve essere un nome di file valido con caratteri alfanumerici e -_. soli",
"pattern_backup_archive_name": "Deve essere un nome di file valido di massimo 30 caratteri di lunghezza, con caratteri alfanumerici e \"-_.\" come unica punteggiatura",
"pattern_domain": "Deve essere un nome di dominio valido (es. il-mio-dominio.org)",
"pattern_firstname": "Deve essere un nome valido",
"pattern_lastname": "Deve essere un cognome valido",
@ -119,50 +119,50 @@
"pattern_positive_number": "Deve essere un numero positivo",
"pattern_username": "Caratteri minuscoli alfanumerici o trattini bassi soli",
"port_already_closed": "La porta {port:d} è già chiusa per le connessioni {ip_version:s}",
"restore_already_installed_app": "Un'applicazione è già installata con l'identificativo '{app:s}'",
"restore_already_installed_app": "Un'applicazione con l'ID '{app:s}' è già installata",
"restore_app_failed": "Impossibile ripristinare l'applicazione '{app:s}'",
"restore_cleaning_failed": "Impossibile pulire la directory temporanea di ripristino",
"restore_complete": "Ripristino completo",
"restore_confirm_yunohost_installed": "Sei sicuro di volere ripristinare un sistema già installato? {answers:s}",
"restore_failed": "Impossibile ripristinare il sistema",
"user_update_failed": "Impossibile aggiornare l'utente",
"user_update_failed": "Impossibile aggiornare l'utente {user}: {error}",
"restore_hook_unavailable": "Lo script di ripristino per '{part:s}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio",
"restore_nothings_done": "Non è stato ripristinato nulla",
"restore_running_app_script": "Esecuzione dello script di ripristino dell'applicazione '{app:s}'…",
"restore_nothings_done": "Nulla è stato ripristinato",
"restore_running_app_script": "Ripristino dell'app '{app:s}'…",
"restore_running_hooks": "Esecuzione degli hook di ripristino…",
"service_added": "Il servizio '{service:s}' è stato aggiunto",
"service_already_started": "Il servizio '{service:s}' è già stato avviato",
"service_already_started": "Il servizio '{service:s}' è già avviato",
"service_already_stopped": "Il servizio '{service:s}' è già stato fermato",
"service_disable_failed": "Impossibile disabilitare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_enable_failed": "Impossibile abilitare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_enabled": "Il servizio '{service:s}' è stato attivato",
"service_disable_failed": "Impossibile disabilitare l'avvio al boot del servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_enable_failed": "Impossibile eseguire il servizio '{service:s}' al boot di sistema.\n\nRegistri di servizio recenti:{logs:s}",
"service_enabled": "Il servizio '{service:s}' si avvierà automaticamente al boot di sistema.",
"service_start_failed": "Impossibile eseguire il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_started": "Il servizio '{service:s}' è stato avviato",
"service_stopped": "Il servizio '{service:s}' è stato fermato",
"service_started": "Servizio '{service:s}' avviato",
"service_stopped": "Servizio '{service:s}' fermato",
"service_unknown": "Servizio '{service:s}' sconosciuto",
"ssowat_conf_generated": "La configurazione SSOwat è stata generata",
"ssowat_conf_updated": "La configurazione SSOwat è stata aggiornata",
"system_upgraded": "Il sistema è stato aggiornato",
"unbackup_app": "L'applicazione '{app:s}' non verrà salvata",
"unexpected_error": "Un'errore inaspettata si è verificata",
"ssowat_conf_generated": "La configurazione SSOwat rigenerata",
"ssowat_conf_updated": "Configurazione SSOwat aggiornata",
"system_upgraded": "Sistema aggiornato",
"unbackup_app": "{app:s} non verrà salvata",
"unexpected_error": "È successo qualcosa di inatteso: {error}",
"unlimit": "Nessuna quota",
"updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema",
"updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema...",
"upgrade_complete": "Aggiornamento completo",
"upnp_dev_not_found": "Nessuno supporto UPnP trovato",
"upnp_disabled": "UPnP è stato disattivato",
"upnp_enabled": "UPnP è stato attivato",
"upnp_port_open_failed": "Impossibile aprire le porte UPnP",
"user_created": "L'utente è stato creato",
"user_creation_failed": "Impossibile creare l'utente",
"user_deletion_failed": "Impossibile cancellare l'utente",
"user_home_creation_failed": "Impossibile creare la home directory del utente",
"upnp_port_open_failed": "Impossibile aprire le porte attraverso UPnP",
"user_created": "Utente creato",
"user_creation_failed": "Impossibile creare l'utente {user}: {error}",
"user_deletion_failed": "Impossibile cancellare l'utente {user}: {error}",
"user_home_creation_failed": "Impossibile creare la 'home' directory del utente",
"user_unknown": "Utente sconosciuto: {user:s}",
"user_updated": "L'utente è stato aggiornato",
"user_updated": "Info dell'utente cambiate",
"yunohost_already_installed": "YunoHost è già installato",
"yunohost_ca_creation_failed": "Impossibile creare una certificate authority",
"yunohost_configured": "YunoHost è stato configurato",
"yunohost_installing": "Installazione di YunoHost",
"yunohost_not_installed": "YunoHost non è o non corretamente installato. Esegui 'yunohost tools postinstall'",
"yunohost_configured": "YunoHost ora è configurato",
"yunohost_installing": "Installazione di YunoHost...",
"yunohost_not_installed": "YunoHost non è correttamente installato. Esegui 'yunohost tools postinstall'",
"domain_cert_gen_failed": "Impossibile generare il certificato",
"certmanager_attempt_to_replace_valid_cert": "Stai provando a sovrascrivere un certificato buono e valido per il dominio {domain:s}! (Usa --force per ignorare)",
"certmanager_domain_unknown": "Dominio {domain:s} sconosciuto",
@ -232,7 +232,7 @@
"password_too_simple_3": "La password deve essere lunga almeno 8 caratteri e contenere numeri, maiuscole e minuscole e simboli",
"password_too_simple_4": "La password deve essere lunga almeno 12 caratteri e contenere numeri, maiuscole e minuscole",
"users_available": "Utenti disponibili:",
"yunohost_ca_creation_success": "L'autorità di certificazione locale è stata creata.",
"yunohost_ca_creation_success": "Autorità di certificazione locale creata.",
"app_action_cannot_be_ran_because_required_services_down": "I seguenti servizi dovrebbero essere in funzione per completare questa azione: {services}. Prova a riavviarli per proseguire (e possibilmente cercare di capire come ma non funzionano più).",
"backup_output_symlink_dir_broken": "La tua cartella d'archivio '{path:s}' è un link simbolico interrotto. Probabilmente hai dimenticato di montare o montare nuovamente il supporto al quale punta il link.",
"certmanager_conflicting_nginx_file": "Impossibile preparare il dominio per il controllo ACME: il file di configurazione nginx {filepath:s} è in conflitto e dovrebbe essere prima rimosso",
@ -247,47 +247,47 @@
"confirm_app_install_warning": "Attenzione: Questa applicazione potrebbe funzionare, ma non è ben integrata in YunoHost. Alcune funzionalità come il single sign-on e il backup/ripristino potrebbero non essere disponibili. Installare comunque? [{answers:s}] ",
"confirm_app_install_danger": "ATTENZIONE! Questa applicazione è ancora sperimentale (se non esplicitamente dichiarata non funzionante)! Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio,digita '{answers:s}'",
"confirm_app_install_thirdparty": "PERICOLO! Quest'applicazione non fa parte del catalogo Yunohost. Installando app di terze parti potresti compromettere l'integrita e la sicurezza del tuo sistema. Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio, digita '{answers:s}'",
"dpkg_is_broken": "Non puoi eseguire questo ora perchè dpkg/apt (i gestori di pacchetti del sistema) sembrano essere in stato danneggiato... Puoi provare a risolvere il problema connettendoti via SSH ed eseguire `sudo dpkg --configure -a`.",
"domain_cannot_remove_main": "Non è possibile rimuovere il dominio principale ora. Prima imposta un nuovo dominio principale",
"domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra qual è la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.",
"dpkg_is_broken": "Non puoi eseguire questo ora perchè dpkg/APT (i gestori di pacchetti del sistema) sembrano essere in stato danneggiato... Puoi provare a risolvere il problema connettendoti via SSH ed eseguire `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.",
"domain_cannot_remove_main": "Non puoi rimuovere '{domain:s}' essendo il dominio principale, prima devi impostare un nuovo dominio principale con il comando 'yunohost domain main-domain -n <altro-dominio>'; ecco la lista dei domini candidati: {other_domains:s}",
"domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.",
"dyndns_could_not_check_provide": "Impossibile controllare se {provider:s} possano fornire {domain:s}.",
"dyndns_could_not_check_available": "Impossibile controllare se {domain:s} è disponibile su {provider:s}.",
"dyndns_domain_not_provided": "Il fornitore Dyndns {provider:s} non può fornire il dominio {domain:s}.",
"experimental_feature": "Attenzione: questa funzionalità è sperimentale e non è considerata stabile, non dovresti utilizzarla a meno che tu non sappia cosa stai facendo.",
"dyndns_domain_not_provided": "Il fornitore DynDNS {provider:s} non può fornire il dominio {domain:s}.",
"experimental_feature": "Attenzione: Questa funzionalità è sperimentale e non è considerata stabile, non dovresti utilizzarla a meno che tu non sappia cosa stai facendo.",
"file_does_not_exist": "Il file {path:s} non esiste.",
"global_settings_bad_choice_for_enum": "Scelta sbagliata per l'impostazione {setting:s}, ricevuta '{choice:s}' ma le scelte disponibili sono : {available_choices:s}",
"global_settings_bad_choice_for_enum": "Scelta sbagliata per l'impostazione {setting:s}, ricevuta '{choice:s}', ma le scelte disponibili sono: {available_choices:s}",
"global_settings_bad_type_for_setting": "Tipo errato per l'impostazione {setting:s}, ricevuto {received_type:s}, atteso {expected_type:s}",
"global_settings_cant_open_settings": "Apertura del file delle impostazioni non riuscita, motivo: {reason:s}",
"global_settings_cant_serialize_settings": "Serializzazione dei dati delle impostazioni non riuscita, motivo: {reason:s}",
"global_settings_cant_write_settings": "Scrittura del file delle impostazioni non riuscita, motivo: {reason:s}",
"global_settings_key_doesnt_exists": "La chiave '{settings_key:s}' non esiste nelle impostazioni globali, puoi vedere tutte le chiavi disponibili eseguendo 'yunohost settings list'",
"global_settings_reset_success": "Successo. Le tue impostazioni precedenti sono state salvate in {path:s}",
"global_settings_reset_success": "Le impostazioni precedenti sono state salvate in {path:s}",
"global_settings_setting_example_bool": "Esempio di opzione booleana",
"global_settings_setting_example_enum": "Esempio di opzione enum",
"already_up_to_date": "Niente da fare. Tutto è già aggiornato.",
"global_settings_setting_example_int": "Esempio di opzione int",
"global_settings_setting_example_string": "Esempio di opzione string",
"global_settings_setting_security_nginx_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server web nginx. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)",
"global_settings_setting_security_nginx_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server web NGIX. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)",
"global_settings_setting_security_password_admin_strength": "Complessità della password di amministratore",
"global_settings_setting_security_password_user_strength": "Complessità della password utente",
"global_settings_setting_security_ssh_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)",
"global_settings_unknown_setting_from_settings_file": "Chiave sconosciuta nelle impostazioni: '{setting_key:s}', scartata e salvata in /etc/yunohost/settings-unknown.json",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Consenti l'uso del (deprecato) hostkey DSA per la configurazione del demone SSH",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Consenti l'uso del hostkey DSA (deprecato) per la configurazione del demone SSH",
"global_settings_unknown_type": "Situazione inaspettata, l'impostazione {setting:s} sembra essere di tipo {unknown_type:s} ma non è un tipo supportato dal sistema.",
"good_practices_about_admin_password": "Stai per definire una nuova password di amministratore. La password deve essere almeno di 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una frase, una serie di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).",
"log_corrupted_md_file": "Il file dei metadati yaml associato con i registri è corrotto: '{md_file}'",
"log_corrupted_md_file": "Il file dei metadati YAML associato con i registri è danneggiato: '{md_file}'\nErrore: {error}",
"log_category_404": "La categoria di registrazione '{category}' non esiste",
"log_link_to_log": "Registro completo di questa operazione: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'",
"log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log display {name}'",
"log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log show {name}{name}'",
"global_settings_setting_security_postfix_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)",
"log_link_to_failed_log": "L'operazione '{desc}' è fallita! Per ottenere aiuto, per favore <a href=\"#/tools/logs/{name}\">fornisci il registro completo dell'operazione cliccando qui</a>",
"log_help_to_get_failed_log": "L'operazione '{desc}' è fallita! Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log display {name} --share'",
"log_link_to_failed_log": "Impossibile completare l'operazione '{desc}'! Per ricevere aiuto, per favore fornisci il registro completo dell'operazione <a href=\"#/tools/logs/{name}\">cliccando qui</a>",
"log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log share {name}'",
"log_does_exists": "Non esiste nessun registro delle operazioni chiamato '{log}', usa 'yunohost log list' per vedere tutti i registri delle operazioni disponibili",
"log_app_change_url": "Cambia l'url dell'applicazione '{}'",
"log_app_install": "Installa l'applicazione '{}'",
"log_app_remove": "Rimuovi l'applicazione '{}'",
"log_app_upgrade": "Aggiorna l'applicazione '{}'",
"log_app_makedefault": "Rendi predefinita l'applicazione '{}'",
"log_app_change_url": "Cambia l'URL dell'app '{}'",
"log_app_install": "Installa l'app '{}'",
"log_app_remove": "Rimuovi l'app '{}'",
"log_app_upgrade": "Aggiorna l'app '{}'",
"log_app_makedefault": "Rendi '{}' l'app predefinita",
"log_available_on_yunopaste": "Questo registro è ora disponibile via {url}",
"log_backup_restore_system": "Ripristina sistema da un archivio di backup",
"log_backup_restore_app": "Ripristina '{}' da un archivio di backup",
@ -296,16 +296,16 @@
"log_domain_add": "Aggiungi il dominio '{}' nella configurazione di sistema",
"log_domain_remove": "Rimuovi il dominio '{}' dalla configurazione di sistema",
"log_dyndns_subscribe": "Sottoscrivi un sottodominio YunoHost '{}'",
"log_dyndns_update": "Aggiorna l'ip associato con il tuo sottodominio YunoHost '{}'",
"log_dyndns_update": "Aggiorna l'IP associato con il tuo sottodominio YunoHost '{}'",
"log_letsencrypt_cert_install": "Installa un certificato Let's encrypt sul dominio '{}'",
"log_selfsigned_cert_install": "Installa un certificato autofirmato sul dominio '{}'",
"log_letsencrypt_cert_renew": "Rinnova il certificato Let's encrypt sul dominio '{}'",
"log_letsencrypt_cert_renew": "Rinnova il certificato Let's Encrypt sul dominio '{}'",
"log_regen_conf": "Rigenera configurazioni di sistema '{}'",
"log_user_create": "Aggiungi l'utente '{}'",
"log_user_delete": "Elimina l'utente '{}'",
"log_user_update": "Aggiornate le informazioni dell'utente '{}'",
"log_domain_main_domain": "Rendi '{}' dominio principale",
"log_tools_migrations_migrate_forward": "Migra avanti",
"log_user_update": "Aggiorna le informazioni dell'utente '{}'",
"log_domain_main_domain": "Rendi '{}' il dominio principale",
"log_tools_migrations_migrate_forward": "Esegui le migrazioni",
"log_tools_postinstall": "Postinstallazione del tuo server YunoHost",
"log_tools_upgrade": "Aggiornamento dei pacchetti di sistema",
"log_tools_shutdown": "Spegni il tuo server",
@ -334,12 +334,12 @@
"migration_0003_yunohost_upgrade": "Iniziando l'aggiornamento dei pacchetti yunohost… La migrazione terminerà, ma l'aggiornamento attuale avverrà subito dopo. Dopo che l'operazione sarà completata, probabilmente dovrai riaccedere all'interfaccia di amministrazione.",
"migration_0003_not_jessie": "La distribuzione attuale non è Jessie!",
"migration_0003_system_not_fully_up_to_date": "Il tuo sistema non è completamente aggiornato. Per favore prima esegui un aggiornamento normale prima di migrare a stretch.",
"this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/apt (i gestori di pacchetti del sistema)… Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo dpkg --configure -a`.",
"this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/APT (i gestori di pacchetti del sistema)... Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.",
"app_action_broke_system": "Questa azione sembra avere rotto questi servizi importanti: {services}",
"app_remove_after_failed_install": "Rimozione dell'applicazione a causa del fallimento dell'installazione...",
"app_install_script_failed": "Si è verificato un errore nello script di installazione dell'applicazione",
"app_install_failed": "Impossibile installare {app}:{error}",
"app_full_domain_unavailable": "Spiacente, questa app deve essere installata su un proprio dominio, ma altre applicazioni sono state installate sul dominio '{domain}'. Dovresti invece usare un sotto-dominio dedicato per questa app.",
"app_full_domain_unavailable": "Spiacente, questa app deve essere installata su un proprio dominio, ma altre applicazioni sono già installate sul dominio '{domain}'. Potresti usare invece un sotto-dominio dedicato per questa app.",
"app_upgrade_script_failed": "È stato trovato un errore nello script di aggiornamento dell'applicazione",
"apps_already_up_to_date": "Tutte le applicazioni sono aggiornate",
"apps_catalog_init_success": "Catalogo delle applicazioni inizializzato!",
@ -357,7 +357,7 @@
"app_manifest_install_ask_admin": "Scegli un utente amministratore per quest'applicazione",
"app_manifest_install_ask_password": "Scegli una password di amministrazione per quest'applicazione",
"app_manifest_install_ask_path": "Scegli il percorso dove installare quest'applicazione",
"app_manifest_install_ask_domain": "Scegli il dominio sotto il quale installare quest'applicazione",
"app_manifest_install_ask_domain": "Scegli il dominio dove installare quest'app",
"app_argument_password_no_default": "Errore durante il parsing dell'argomento '{name}': l'argomento password non può avere un valore di default per ragioni di sicurezza",
"additional_urls_already_added": "L'URL aggiuntivo '{url:s}' è già utilizzato come URL aggiuntivo per il permesso '{permission:s}'",
"diagnosis_basesystem_ynh_inconsistent_versions": "Stai eseguendo versioni incompatibili dei pacchetti YunoHost... probabilmente a causa di aggiornamenti falliti o parziali.",
@ -412,5 +412,271 @@
"diagnosis_failed_for_category": "Diagnosi fallita per la categoria '{category}:{error}",
"diagnosis_display_tip": "Per vedere i problemi rilevati, puoi andare alla sezione Diagnosi del amministratore, o eseguire 'yunohost diagnosis show --issues' dalla riga di comando.",
"diagnosis_package_installed_from_sury_details": "Alcuni pacchetti sono stati inavvertitamente installati da un repository di terze parti chiamato Sury. Il team di Yunohost ha migliorato la gestione di tali pacchetti, ma ci si aspetta che alcuni setup di app PHP7.3 abbiano delle incompatibilità anche se sono ancora in Stretch. Per sistemare questa situazione, dovresti provare a lanciare il seguente comando: <cmd>{cmd_to_fix}</cmd>",
"diagnosis_package_installed_from_sury": "Alcuni pacchetti di sistema dovrebbero fare il downgrade"
"diagnosis_package_installed_from_sury": "Alcuni pacchetti di sistema dovrebbero fare il downgrade",
"diagnosis_mail_ehlo_bad_answer": "Un servizio diverso da SMTP ha risposto sulla porta 25 su IPv{ipversion}",
"diagnosis_mail_ehlo_unreachable_details": "Impossibile aprire una connessione sulla porta 25 sul tuo server su IPv{ipversion}. Sembra irraggiungibile.<br>1. La causa più probabile di questo problema è la porta 25 <a href='https://yunohost.org/isp_box_config'>non correttamente inoltrata al tuo server</a>.<br>2. Dovresti esser sicuro che il servizio postfix sia attivo.<br>3. Su setup complessi: assicuratu che nessun firewall o reverse-proxy stia interferendo.",
"diagnosis_mail_ehlo_unreachable": "Il server SMTP non è raggiungibile dall'esterno su IPv{ipversion}. Non potrà ricevere email.",
"diagnosis_mail_ehlo_ok": "Il server SMTP è raggiungibile dall'esterno e quindi può ricevere email!",
"diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Alcuni provider non ti permettono di aprire la porta 25 in uscita perché non gli importa della Net Neutrality.<br> - Alcuni mettono a disposizione un alternativa <a href='https://yunohost.org/#/smtp_relay'>attraverso un mail server relay</a> anche se implica che il relay ha la capacità di leggere il vostro traffico email.<br> - Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a><br> - Puoi anche prendere in considerazione di cambiare <a href='https://yunohost.org/#/isp'>per un provider pro Net Neutrality</a>",
"diagnosis_mail_outgoing_port_25_blocked_details": "Come prima cosa dovresti sbloccare la porta 25 in uscita dall'interfaccia del tuo router internet o del tuo hosting provider. (Alcuni hosting provider potrebbero richiedere l'invio di un ticket di supporto per la richiesta).",
"diagnosis_mail_outgoing_port_25_blocked": "Il server SMTP non può inviare email ad altri server perché la porta 25 è bloccata in uscita su IPv{ipversion}.",
"diagnosis_mail_outgoing_port_25_ok": "Il server SMTP è abile all'invio delle email (porta 25 in uscita non bloccata).",
"diagnosis_swap_tip": "Attenzione. Sii consapevole che se il server ha lo swap su di una memoria SD o un disco SSD, potrebbe drasticamente ridurre la durata di vita del dispositivo.",
"diagnosis_swap_ok": "Il sistema ha {total} di memoria swap!",
"diagnosis_swap_notsomuch": "Il sistema ha solo {total} di swap. Dovresti considerare almeno di aggiungere {recommended} di memoria swap per evitare situazioni dove il sistema esaurisce la memoria.",
"diagnosis_swap_none": "Il sistema non ha lo swap. Dovresti considerare almeno di aggiungere {recommended} di memoria swap per evitare situazioni dove il sistema esaurisce la memoria.",
"diagnosis_ram_ok": "Il sistema ha ancora {available} ({available_percent}%) di RAM disponibile su {total}.",
"diagnosis_ram_low": "Il sistema ha solo {available} ({available_percent}%) di RAM disponibile (su {total}). Fa attenzione.",
"diagnosis_ram_verylow": "Il sistema ha solo {available} ({available_percent}%) di RAM disponibile (su {total})",
"diagnosis_diskusage_ok": "Lo storage <code>{mountpoint}</code> (nel device <code>{device}</code> ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total})!",
"diagnosis_diskusage_low": "Lo storage <code>{mountpoint}</code> (nel device <code>{device}</code> ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total}). Fa attenzione.",
"diagnosis_diskusage_verylow": "Lo storage <code>{mountpoint}</code> (nel device <code>{device}</code> ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total}). Dovresti seriamente considerare di fare un po' di pulizia!",
"diagnosis_mail_fcrdns_nok_details": "Dovresti prima configurare il DNS inverso con <code>{ehlo_domain}</code> nell'interfaccia del tuo router internet o del tuo hosting provider. (Alcuni hosting provider potrebbero richiedere l'invio di un ticket di supporto per la richiesta).",
"diagnosis_mail_fcrdns_dns_missing": "Nessun DNS inverso è configurato per IPv{ipversion}. Alcune email potrebbero non essere inviate o segnalate come spam.",
"diagnosis_mail_fcrdns_ok": "Il tuo DNS inverso è configurato correttamente!",
"diagnosis_mail_ehlo_could_not_diagnose_details": "Errore: {error}",
"diagnosis_mail_ehlo_could_not_diagnose": "Non è possibile verificare se il server mail postfix è raggiungibile dall'esterno su IPv{ipversion}.",
"diagnosis_mail_ehlo_wrong": "Un server mail SMTP diverso sta rispondendo su IPv{ipversion}. Probabilmente il tuo server non può ricevere email.",
"diagnosis_mail_ehlo_bad_answer_details": "Potrebbe essere un'altra macchina a rispondere al posto del tuo server.",
"diagnosis_mail_fcrdns_nok_alternatives_4": "Alcuni provider non ti permettono di configurare un DNS inverso (o la loro configurazione non funziona...). Se stai avendo problemi a causa di ciò, considera le seguenti soluzioni:<br> - Alcuni ISP mettono a disposizione un alternativa <a href='https://yunohost.org/#/smtp_relay'>attraverso un mail server relay</a> anche se implica che il relay ha la capacità di leggere il vostro traffico email.<br> - Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a><br> - Puoi anche prendere in considerazione di cambiare <a href='https://yunohost.org/#/isp'>internet provider</a>",
"diagnosis_mail_ehlo_wrong_details": "L'EHLO ricevuto dalla diagnostica remota su IPv{ipversion} è differente dal dominio del tuo server.<br>EHLO ricevuto: <code>{wrong_ehlo}</code><br>EHLO atteso: <code>{right_ehlo}</code><br>La causa più comune di questo problema è la porta 25 <a href='https://yunohost.org/isp_box_config'>non correttamente inoltrata al tuo server</a>. Oppure assicurati che nessun firewall o reverse-proxy stia interferendo.",
"diagnosis_mail_blacklist_ok": "Gli IP e i domini utilizzati da questo server non sembrano essere nelle blacklist",
"diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS invero corrente: <code>{rdns_domain}</code><br>Valore atteso: <code>{ehlo_domain}</code>",
"diagnosis_mail_fcrdns_different_from_ehlo_domain": "Il DNS inverso non è correttamente configurato su IPv{ipversion}. Alcune email potrebbero non essere spedite o segnalate come SPAM.",
"diagnosis_mail_fcrdns_nok_alternatives_6": "Alcuni provider non permettono di configurare un DNS inverso (o non è configurato bene...). Se il tuo DNS inverso è correttamente configurato per IPv4, puoi provare a disabilitare l'utilizzo di IPv6 durante l'invio mail eseguendo <cmd>yunohost settings set smtp.allow_ipv6 -v off</cmd>. NB: se esegui il comando non sarà più possibile inviare o ricevere email da i pochi IPv6-only server mail esistenti.",
"yunohost_postinstall_end_tip": "La post-installazione è completata! Per rifinire il tuo setup, considera di:\n\t- aggiungere il primo utente nella sezione 'Utenti' del webadmin (o eseguendo da terminale 'yunohost user create <username>');\n\t- eseguire una diagnosi alla ricerca di problemi nella sezione 'Diagnosi' del webadmin (o eseguendo da terminale 'yunohost diagnosis run');\n\t- leggere 'Finalizing your setup' e 'Getting to know Yunohost' nella documentazione admin: https://yunohost.org/admindoc.",
"user_already_exists": "L'utente '{user}' esiste già",
"update_apt_cache_warning": "Qualcosa è andato storto mentre eseguivo l'aggiornamento della cache APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}",
"update_apt_cache_failed": "Impossibile aggiornare la cache di APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}",
"unknown_main_domain_path": "Percorso o dominio sconosciuto per '{app}'. Devi specificare un dominio e un percorso per poter specificare un URL per il permesso.",
"tools_upgrade_special_packages_completed": "Aggiornamento pacchetti YunoHost completato.\nPremi [Invio] per tornare al terminale",
"tools_upgrade_special_packages_explanation": "L'aggiornamento speciale continuerà in background. Per favore non iniziare nessun'altra azione sul tuo server per i prossimi ~10 minuti (dipende dalla velocità hardware). Dopo questo, dovrai ri-loggarti nel webadmin. Il registro di aggiornamento sarà disponibile in Strumenti → Log/Registri (nel webadmin) o dalla linea di comando eseguendo 'yunohost log list'.",
"tools_upgrade_special_packages": "Adesso aggiorno i pacchetti 'speciali' (correlati a yunohost)…",
"tools_upgrade_regular_packages_failed": "Impossibile aggiornare i pacchetti: {packages_list}",
"tools_upgrade_regular_packages": "Adesso aggiorno i pacchetti 'normali' (non correlati a yunohost)…",
"tools_upgrade_cant_unhold_critical_packages": "Impossibile annullare il blocco dei pacchetti critici/importanti…",
"tools_upgrade_cant_hold_critical_packages": "Impossibile bloccare i pacchetti critici/importanti…",
"tools_upgrade_cant_both": "Impossibile aggiornare sia il sistema e le app nello stesso momento",
"tools_upgrade_at_least_one": "Specifica '--apps', o '--system'",
"show_tile_cant_be_enabled_for_regex": "Non puoi abilitare 'show_tile' in questo momento, perché l'URL del permesso '{permission}' è una regex",
"show_tile_cant_be_enabled_for_url_not_defined": "Non puoi abilitare 'show_tile' in questo momento, devi prima definire un URL per il permesso '{permission}'",
"service_reloaded_or_restarted": "Il servizio '{service:s}' è stato ricaricato o riavviato",
"service_reload_or_restart_failed": "Impossibile ricaricare o riavviare il servizio '{service:s}'\n\nUltimi registri del servizio: {logs:s}",
"service_restarted": "Servizio '{service:s}' riavviato",
"service_restart_failed": "Impossibile riavviare il servizio '{service:s}'\n\nUltimi registri del servizio: {logs:s}",
"service_reloaded": "Servizio '{service:s}' ricaricato",
"service_reload_failed": "Impossibile ricaricare il servizio '{service:s}'\n\nUltimi registri del servizio: {logs:s}",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' è obsoleto! Per favore usa 'yunohost tools regen-conf' al suo posto.",
"service_description_yunohost-firewall": "Gestisce l'apertura e la chiusura delle porte ai servizi",
"service_description_yunohost-api": "Gestisce l'interazione tra l'interfaccia web YunoHost ed il sistema",
"service_description_ssh": "Ti consente di accedere da remoto al tuo server attraverso il terminale (protocollo SSH)",
"service_description_slapd": "Memorizza utenti, domini e info correlate",
"service_description_rspamd": "Filtra SPAM, e altre funzionalità legate alle mail",
"service_description_redis-server": "Un database specializzato usato per un veloce accesso ai dati, task queue, e comunicazioni tra programmi",
"service_description_postfix": "Usato per inviare e ricevere email",
"service_description_php7.3-fpm": "Esegue app scritte in PHP con NGINX",
"service_description_nginx": "Serve o permette l'accesso a tutti i siti pubblicati sul tuo server",
"service_description_mysql": "Memorizza i dati delle app (database SQL)",
"service_description_metronome": "Gestisce gli account di messaggistica instantanea XMPP",
"service_description_fail2ban": "Ti protegge dal brute-force e altri tipi di attacchi da Internet",
"service_description_dovecot": "Consente ai client mail di accedere/recuperare le email (via IMAP e POP3)",
"service_description_dnsmasq": "Gestisce la risoluzione dei domini (DNS)",
"service_description_avahi-daemon": "Consente di raggiungere il tuo server eseguendo 'yunohost.local' sulla tua LAN",
"server_reboot_confirm": "Il server si riavvierà immediatamente, sei sicuro? [{answers:s}]",
"server_reboot": "Il server si riavvierà",
"server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers:s}]",
"server_shutdown": "Il server si spegnerà",
"root_password_replaced_by_admin_password": "La tua password di root è stata sostituita dalla tua password d'amministratore.",
"root_password_desynchronized": "La password d'amministratore è stata cambiata, ma YunoHost non ha potuto propagarla alla password di root!",
"restore_system_part_failed": "Impossibile ripristinare la sezione di sistema '{part:s}'",
"restore_removing_tmp_dir_failed": "Impossibile rimuovere una vecchia directory temporanea",
"restore_not_enough_disk_space": "Spazio libero insufficiente (spazio: {free_space:d}B, necessario: {needed_space:d}B, margine di sicurezza: {margin:d}B)",
"restore_may_be_not_enough_disk_space": "Il tuo sistema non sembra avere abbastanza spazio (libero: {free_space:d}B, necessario: {needed_space:d}B, margine di sicurezza: {margin:d}B)",
"restore_extracting": "Sto estraendo i file necessari dall'archivio…",
"restore_already_installed_apps": "Le seguenti app non possono essere ripristinate perché sono già installate: {apps}",
"regex_with_only_domain": "Non puoi usare una regex per il dominio, solo per i percorsi",
"regex_incompatible_with_tile": "/!\\ Packagers! Il permesso '{permission}' ha show_tile impostato su 'true' e perciò non è possibile definire un URL regex per l'URL principale",
"regenconf_need_to_explicitly_specify_ssh": "La configurazione ssh è stata modificata manualmente, ma devi specificare la categoria 'ssh' con --force per applicare le modifiche.",
"regenconf_pending_applying": "Applico le configurazioni in attesa per la categoria '{category}'...",
"regenconf_failed": "Impossibile rigenerare la configurazione per le categorie: {categories}",
"regenconf_dry_pending_applying": "Controllo configurazioni in attesa che potrebbero essere applicate alla categoria '{category}'…",
"regenconf_would_be_updated": "La configurazione sarebbe stata aggiornata per la categoria '{category}'",
"regenconf_updated": "Configurazione aggiornata per '{category}'",
"regenconf_up_to_date": "Il file di configurazione è già aggiornato per la categoria '{category}'",
"regenconf_now_managed_by_yunohost": "Il file di configurazione '{conf}' da adesso è gestito da YunoHost (categoria {category}).",
"regenconf_file_updated": "File di configurazione '{conf}' aggiornato",
"regenconf_file_removed": "File di configurazione '{conf}' rimosso",
"regenconf_file_remove_failed": "Impossibile rimuovere il file di configurazione '{conf}'",
"regenconf_file_manually_removed": "Il file di configurazione '{conf}' è stato rimosso manualmente, e non sarà generato",
"regenconf_file_manually_modified": "Il file di configurazione '{conf}' è stato modificato manualmente e non sarà aggiornato",
"regenconf_file_kept_back": "Il file di configurazione '{conf}' dovrebbe esser stato cancellato da regen-conf (categoria {category}), ma non è così.",
"regenconf_file_copy_failed": "Impossibile copiare il nuovo file di configurazione da '{new}' a '{conf}'",
"regenconf_file_backed_up": "File di configurazione '{conf}' salvato in '{backup}'",
"permission_require_account": "Il permesso {permission} ha senso solo per gli utenti con un account, quindi non può essere attivato per i visitatori.",
"permission_protected": "Il permesso {permission} è protetto. Non puoi aggiungere o rimuovere il gruppo visitatori dal permesso.",
"permission_updated": "Permesso '{permission:s}' aggiornato",
"permission_update_failed": "Impossibile aggiornare il permesso '{permission}': {error}",
"permission_not_found": "Permesso '{permission:s}' non trovato",
"permission_deletion_failed": "Impossibile cancellare il permesso '{permission}': {error}",
"permission_deleted": "Permesso '{permission:s}' cancellato",
"permission_currently_allowed_for_all_users": "Il permesso è attualmente garantito a tutti gli utenti oltre gli altri gruppi. Probabilmente vuoi o rimuovere il permesso 'all_user' o rimuovere gli altri gruppi per cui è garantito attualmente.",
"permission_creation_failed": "Impossibile creare i permesso '{permission}': {error}",
"permission_created": "Permesso '{permission:s}' creato",
"permission_cannot_remove_main": "Non è possibile rimuovere un permesso principale",
"permission_already_up_to_date": "Il permesso non è stato aggiornato perché la richiesta di aggiunta/rimozione è già coerente con lo stato attuale.",
"permission_already_exist": "Permesso '{permission}' esiste già",
"permission_already_disallowed": "Il gruppo '{group}' ha già il permesso '{permission}' disabilitato",
"permission_already_allowed": "Il gruppo '{group}' ha già il permesso '{permission}' abilitato",
"pattern_password_app": "Mi spiace, le password non possono contenere i seguenti caratteri: {forbidden_chars}",
"pattern_email_forward": "Dev'essere un indirizzo mail valido, simbolo '+' accettato (es: tizio+tag@example.com)",
"operation_interrupted": "L'operazione è stata interrotta manualmente?",
"invalid_number": "Dev'essere un numero",
"migrations_to_be_ran_manually": "Migrazione {id} dev'essere eseguita manualmente. Vai in Strumenti → Migrazioni nella pagina webadmin, o esegui `yunohost tools migrations run`.",
"migrations_success_forward": "Migrazione {id} completata",
"migrations_skip_migration": "Salto migrazione {id}...",
"migrations_running_forward": "Eseguo migrazione {id}...",
"migrations_pending_cant_rerun": "Queste migrazioni sono ancora in attesa, quindi non possono essere eseguite nuovamente: {ids}",
"migrations_not_pending_cant_skip": "Queste migrazioni non sono in attesa, quindi non possono essere saltate: {ids}",
"migrations_no_such_migration": "Non esiste una migrazione chiamata '{id}'",
"migrations_no_migrations_to_run": "Nessuna migrazione da eseguire",
"migrations_need_to_accept_disclaimer": "Per eseguire la migrazione {id}, devi accettare il disclaimer seguente:\n---\n{disclaimer}\n---\nSe accetti di eseguire la migrazione, per favore reinserisci il comando con l'opzione '--accept-disclaimer'.",
"migrations_must_provide_explicit_targets": "Devi specificare i target quando utilizzi '--skip' o '--force-rerun'",
"migrations_migration_has_failed": "Migrazione {id} non completata, annullamento. Errore: {exception}",
"migrations_loading_migration": "Caricamento migrazione {id}...",
"migrations_list_conflict_pending_done": "Non puoi usare sia '--previous' e '--done' allo stesso tempo.",
"migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' sono opzioni che si escludono a vicenda.",
"migrations_failed_to_load_migration": "Impossibile caricare la migrazione {id}: {error}",
"migrations_dependencies_not_satisfied": "Esegui queste migrazioni: '{dependencies_id}', prima di {id}.",
"migrations_cant_reach_migration_file": "Impossibile accedere ai file di migrazione nel path '%s'",
"migrations_already_ran": "Migrazioni già effettuate: {ids}",
"migration_0019_slapd_config_will_be_overwritten": "Sembra che tu abbia modificato manualmente la configurazione slapd. Per questa importante migrazione, YunoHost deve forzare l'aggiornamento della configurazione slapd. I file originali verranno back-uppati in {conf_backup_folder}.",
"migration_0019_rollback_success": "Sistema ripristinato.",
"migration_0019_migration_failed_trying_to_rollback": "Impossibile migrare... sto cercando di ripristinare il sistema.",
"migration_0019_can_not_backup_before_migration": "Il backup del sistema non è stato completato prima della migrazione. Errore: {error:s}",
"migration_0019_backup_before_migration": "Creando un backup del database LDAP e delle impostazioni delle app prima dell'effettiva migrazione.",
"migration_0019_add_new_attributes_in_ldap": "Aggiungi nuovi attributi ai permessi nel database LDAP",
"migration_0018_failed_to_reset_legacy_rules": "Impossibile resettare le regole iptables legacy: {error}",
"migration_0018_failed_to_migrate_iptables_rules": "Migrazione fallita delle iptables legacy a nftables: {error}",
"migration_0017_not_enough_space": "Libera abbastanza spazio in {path} per eseguire la migrazione.",
"migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 è installato, ma non PostgreSQL 11 ?! Qualcosa di strano potrebbe esser successo al tuo sistema :'( ...",
"migration_0017_postgresql_96_not_installed": "PostgreSQL non è stato installato sul tuo sistema. Nulla da fare.",
"migration_0015_weak_certs": "I seguenti certificati utilizzano ancora un algoritmo di firma debole e dovrebbero essere aggiornati per essere compatibili con la prossima versione di nginx: {certs}",
"migration_0015_cleaning_up": "Sto pulendo la cache e i pacchetti non più utili...",
"migration_0015_specific_upgrade": "Inizio l'aggiornamento dei pacchetti di sistema che necessitano di essere aggiornati da soli...",
"migration_0015_modified_files": "Attenzioni, i seguenti file sembrano esser stati modificati manualmente, e potrebbero essere sovrascritti dopo l'aggiornamento: {manually_modified_files}",
"migration_0015_problematic_apps_warning": "Alcune applicazioni potenzialmente problematiche sono state rilevate nel sistema. Sembra che non siano state installate attraverso il catalogo app YunoHost, o non erano flaggate come 'working'/'funzionanti'. Di conseguenza, non è possibile garantire che funzioneranno ancora dopo l'aggiornamento: {problematic_apps}",
"migration_0015_general_warning": "Attenzione, sappi che questa migrazione è un'operazione delicata. Il team YunoHost ha fatto del suo meglio nel controllarla e testarla, ma le probabilità che il sistema e/o qualche app si danneggi non sono nulle.\n\nPerciò, ti raccomandiamo di:\n\t- Effettuare un backup di tutti i dati e app importanti. Maggiori informazioni su https://yunohost.org/backup;\n\t- Sii paziente dopo aver lanciato l'operazione: in base alla tua connessione internet e al tuo hardware, potrebbero volerci alcune ore per aggiornare tutto.",
"migration_0015_system_not_fully_up_to_date": "Il tuo sistema non è completamente aggiornato. Esegui un aggiornamento classico prima di lanciare la migrazione a Buster.",
"migration_0015_not_enough_free_space": "Poco spazio libero disponibile in /var/! Dovresti avere almeno 1GB libero per effettuare questa migrazione.",
"migration_0015_not_stretch": "La distribuzione Debian corrente non è Stretch!",
"migration_0015_yunohost_upgrade": "Inizio l'aggiornamento del core di YunoHost...",
"migration_0015_still_on_stretch_after_main_upgrade": "Qualcosa è andato storto durante l'aggiornamento principale, il sistema sembra essere ancora su Debian Stretch",
"migration_0015_main_upgrade": "Inizio l'aggiornamento principale...",
"migration_0015_patching_sources_list": "Applico le patch a sources.lists...",
"migration_0015_start": "Inizio migrazione a Buster",
"migration_0011_failed_to_remove_stale_object": "Impossibile rimuovere l'oggetto {dn}: {error}",
"migration_0011_update_LDAP_schema": "Aggiornado lo schema LDAP...",
"migration_0011_update_LDAP_database": "Aggiornando il database LDAP...",
"migration_0011_migrate_permission": "Migrando permessi dalle impostazioni delle app a LDAP...",
"migration_0011_LDAP_update_failed": "Impossibile aggiornare LDAP. Errore: {error:s}",
"migration_0011_create_group": "Sto creando un gruppo per ogni utente...",
"migration_description_0019_extend_permissions_features": "Estendi il sistema di gestione dei permessi app",
"migration_description_0018_xtable_to_nftable": "Migra le vecchie regole di traffico network sul nuovo sistema nftable",
"migration_description_0017_postgresql_9p6_to_11": "Migra i database da PostgreSQL 9.6 a 11",
"migration_description_0016_php70_to_php73_pools": "MIgra i file di configurazione 'pool' di php7.0-fpm su php7.3",
"migration_description_0015_migrate_to_buster": "Aggiorna il sistema a Debian Buster e YunoHost 4.X",
"migrating_legacy_permission_settings": "Impostando le impostazioni legacy dei permessi..",
"mailbox_disabled": "E-mail disabilitate per l'utente {user:s}",
"log_user_permission_reset": "Resetta il permesso '{}'",
"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_permission_url": "Aggiorna l'URL collegato al permesso '{}'",
"log_permission_delete": "Cancella permesso '{}'",
"log_permission_create": "Crea permesso '{}'",
"log_app_config_apply": "Applica la configurazione all'app '{}'",
"log_app_config_show_panel": "Mostra il pannello di configurazione dell'app '{}'",
"log_app_action_run": "Esegui l'azione dell'app '{}'",
"log_operation_unit_unclosed_properly": "Operazion unit non è stata chiusa correttamente",
"invalid_regex": "Regex invalida:'{regex:s}'",
"hook_list_by_invalid": "Questa proprietà non può essere usata per listare gli hooks",
"hook_json_return_error": "Impossibile leggere la risposta del hook {path:s}. Errore: {msg:s}. Contenuto raw: {raw_content}",
"group_user_not_in_group": "L'utente {user} non è nel gruppo {group}",
"group_user_already_in_group": "L'utente {user} è già nel gruppo {group}",
"group_update_failed": "Impossibile aggiornare il gruppo '{group}': {error}",
"group_updated": "Gruppo '{group}' aggiornato",
"group_unknown": "Gruppo '{group:s}' sconosciuto",
"group_deletion_failed": "Impossibile cancellare il gruppo '{group}': {error}",
"group_deleted": "Gruppo '{group}' cancellato",
"group_cannot_be_deleted": "Il gruppo {group} non può essere eliminato manualmente.",
"group_cannot_edit_primary_group": "Il gruppo '{group}' non può essere modificato manualmente. È il gruppo principale con lo scopo di contenere solamente uno specifico utente.",
"group_cannot_edit_visitors": "Il gruppo 'visitatori' non può essere modificato manualmente. È un gruppo speciale che rappresenta i visitatori anonimi",
"group_cannot_edit_all_users": "Il gruppo 'all_users' non può essere modificato manualmente. È un gruppo speciale che contiene tutti gli utenti registrati in YunoHost",
"group_creation_failed": "Impossibile creare il gruppo '{group}': {error}",
"group_created": "Gruppo '{group}' creato",
"group_already_exist_on_system_but_removing_it": "Il gruppo {group} esiste già tra i gruppi di sistema, ma YunoHost lo cancellerà...",
"group_already_exist_on_system": "Il gruppo {group} esiste già tra i gruppi di sistema",
"group_already_exist": "Il gruppo {group} esiste già",
"global_settings_setting_backup_compress_tar_archives": "Quando creo nuovi backup, usa un archivio (.tar.gz) al posto di un archivio non compresso (.tar). NB: abilitare quest'opzione significa create backup più leggeri, ma la procedura durerà di più e il carico CPU sarà maggiore.",
"global_settings_setting_smtp_relay_password": "Password del relay SMTP",
"global_settings_setting_smtp_relay_user": "User account del relay SMTP",
"global_settings_setting_smtp_relay_port": "Porta del relay SMTP",
"global_settings_setting_smtp_relay_host": "Utilizza SMTP relay per inviare mail al posto di questa instanza yunohost. Utile se sei in una di queste situazioni: la tua porta 25 è bloccata dal tuo provider ISP o VPS; hai un IP residenziale listato su DUHL; non sei puoi configurare il DNS inverso; oppure questo server non è direttamente esposto a Internet e vuoi usarne un'altro per spedire email.",
"global_settings_setting_smtp_allow_ipv6": "Permetti l'utilizzo di IPv6 per ricevere e inviare mail",
"global_settings_setting_pop3_enabled": "Abilita il protocollo POP3 per il server mail",
"dyndns_provider_unreachable": "Incapace di raggiungere il provider DynDNS {provider}: o il tuo YunoHost non è connesso ad internet o il server dynette è down.",
"dpkg_lock_not_available": "Impossibile eseguire il comando in questo momento perché un altro programma sta bloccando dpkg (il package manager di sistema)",
"domain_name_unknown": "Dominio '{domain}' sconosciuto",
"domain_cannot_remove_main_add_new_one": "Non puoi rimuovere '{domain:s}' visto che è il dominio principale nonché il tuo unico dominio, devi prima aggiungere un altro dominio eseguendo 'yunohost domain add <altro-dominio.com>', impostarlo come dominio principale con 'yunohost domain main-domain n <altro-dominio.com>', e solo allora potrai rimuovere il dominio '{domain:s}' eseguendo 'yunohost domain remove {domain:s}'.'",
"domain_cannot_add_xmpp_upload": "Non puoi aggiungere domini che iniziano per 'xmpp-upload.'. Questo tipo di nome è riservato per la funzionalità di upload XMPP integrata in YunoHost.",
"diagnosis_processes_killed_by_oom_reaper": "Alcuni processi sono stati terminati dal sistema che era a corto di memoria. Questo è un sintomo di insufficienza di memoria nel sistema o di un processo che richiede troppa memoria. Lista dei processi terminati:\n{kills_summary}",
"diagnosis_never_ran_yet": "Sembra che questo server sia stato impostato recentemente e non è presente nessun report di diagnostica. Dovresti partire eseguendo una diagnostica completa, da webadmin o da terminale con il comando 'yunohost diagnosis run'.",
"diagnosis_unknown_categories": "Le seguenti categorie sono sconosciute: {categories}",
"diagnosis_http_nginx_conf_not_up_to_date_details": "Per sistemare, ispeziona le differenze nel terminale eseguendo <cmd>yunohost tools regen-conf nginx --dry-run --with-diff</cmd> e se ti va bene, applica le modifiche con <cmd>yunohost tools regen-conf ngix --force</cmd>.",
"diagnosis_http_nginx_conf_not_up_to_date": "La configurazione nginx di questo dominio sembra esser stato modificato manualmente, e impedisce a YunoHost di controlalre se è raggiungibile su HTTP.",
"diagnosis_http_partially_unreachable": "Il dominio {domain} sembra irraggiungibile attraverso HTTP dall'esterno della tua LAN su IPv{failed}, anche se funziona su IPv{passed}.",
"diagnosis_http_unreachable": "Il dominio {domain} sembra irraggiungibile attraverso HTTP dall'esterno della tua LAN.",
"diagnosis_http_bad_status_code": "Sembra che un altro dispositivo (forse il tuo router internet) abbia risposto al posto del tuo server<br> 1. La causa più comune è la porta 80 (e 443) <a href='https://yunohost.org/isp_box_config'>non correttamente inoltrata al tuo server</a>.<br> 2. Su setup più complessi: assicurati che nessun firewall o reverse-proxy stia interferendo.",
"diagnosis_http_connection_error": "Errore connessione: impossibile connettersi al dominio richiesto, probabilmente è irraggiungibile.",
"diagnosis_http_timeout": "Andato in time-out cercando di contattare il server dall'esterno. Sembra essere irraggiungibile.<br> 1. La causa più comune è la porta 80 (e 443) <a href='https://yunohost.org/isp_box_config'>non correttamente inoltrata al tuo server</a>.<br> 2. Dovresti accertarti che il servizio nginx sia attivo.<br> 3. Su setup più complessi: assicurati che nessun firewall o reverse-proxy stia interferendo.",
"diagnosis_http_ok": "Il dominio {domain} è raggiungibile attraverso HTTP al di fuori della tua LAN.",
"diagnosis_http_could_not_diagnose_details": "Errore: {error}",
"diagnosis_http_could_not_diagnose": "Non posso controllare se i domini sono raggiungibili dall'esterno su IPv{ipversion}.",
"diagnosis_http_hairpinning_issue_details": "Questo probabilmente è causato dal tuo ISP router. Come conseguenza, persone al di fuori della tua LAN saranno in grado di accedere al tuo server come atteso, ma non le persone all'interno della LAN (tipo te, immagino) utilizzando il dominio internet o l'IP globale. Dovresti essere in grado di migliorare la situazione visitando <a href='https://yunohost.org/dns_local_network'>https://yunohost.org/dns_local_network</a>",
"diagnosis_http_hairpinning_issue": "La tua rete locale sembra non avere \"hairpinning\" abilitato.",
"diagnosis_ports_forwarding_tip": "Per sistemare questo problema, probabilmente dovresti configurare l'inoltro della porta sul tuo router internet come descritto qui <a href='https://yunohost.org/isp_box_config'>https://yunohost.org/isp_box_config</a>",
"diagnosis_ports_needed_by": "Esporre questa porta è necessario per le feature di {category} (servizio {service})",
"diagnosis_ports_ok": "La porta {port} è raggiungibile dall'esterno.",
"diagnosis_ports_partially_unreachable": "La porta {port} non è raggiungibile dall'esterno su IPv{failed}.",
"diagnosis_ports_unreachable": "La porta {port} non è raggiungibile dall'esterno.",
"diagnosis_ports_could_not_diagnose_details": "Errore: {error}",
"diagnosis_ports_could_not_diagnose": "Impossibile diagnosticare se le porte sono raggiungibili dall'esterno su IPv{ipversion}.",
"diagnosis_description_regenconf": "Configurazioni sistema",
"diagnosis_description_mail": "Email",
"diagnosis_description_web": "Web",
"diagnosis_description_ports": "Esposizione porte",
"diagnosis_description_systemresources": "Risorse di sistema",
"diagnosis_description_services": "Check stato servizi",
"diagnosis_description_dnsrecords": "Record DNS",
"diagnosis_description_ip": "Connettività internet",
"diagnosis_description_basesystem": "Sistema base",
"diagnosis_security_vulnerable_to_meltdown_details": "Per sistemare, dovresti aggiornare il tuo sistema e fare il reboot per caricare il nuovo kernel linux (o contatta il tuo server provider se non funziona). Visita https://meltdownattack.com/ per maggiori info.",
"diagnosis_security_vulnerable_to_meltdown": "Sembra che tu sia vulnerabile alla vulnerabilità di sicurezza critica \"Meltdown\"",
"diagnosis_regenconf_manually_modified_details": "Questo è probabilmente OK se sai cosa stai facendo! YunoHost smetterà di aggiornare automaticamente questo file... Ma sappi che gli aggiornamenti di YunoHost potrebbero contenere importanti cambiamenti. Se vuoi, puoi controllare le differente con <cmd>yunohost tools regen-conf {category} --dry-run --with-diff</cmd> e forzare il reset della configurazione raccomandata con <cmd>yunohost tools regen-conf {category} --force</cmd>",
"diagnosis_regenconf_manually_modified": "Il file di configurazione <code>{file}</code> sembra esser stato modificato manualmente.",
"diagnosis_regenconf_allgood": "Tutti i file di configurazione sono allineati con le configurazioni raccomandate!",
"diagnosis_mail_queue_too_big": "Troppe email in attesa nella coda ({nb_pending} emails)",
"diagnosis_mail_queue_unavailable_details": "Errore: {error}",
"diagnosis_mail_queue_unavailable": "Impossibile consultare il numero di email in attesa",
"diagnosis_mail_queue_ok": "{nb_pending} emails in attesa nelle code",
"diagnosis_mail_blacklist_website": "Dopo aver identificato il motivo e averlo risolto, sentiti libero di chiedere di rimuovere il tuo IP o dominio da {blacklist_website}",
"diagnosis_mail_blacklist_reason": "Il motivo della blacklist è: {reason}",
"diagnosis_mail_blacklist_listed_by": "Il tuo IP o dominio <code>{item}</code> è nella blacklist {blacklist_name}",
"diagnosis_backports_in_sources_list": "Sembra che apt (il package manager) sia configurato per utilizzare le backport del repository. A meno che tu non sappia quello che stai facendo, scoraggiamo fortemente di installare pacchetti tramite esse, perché ci sono alte probabilità di creare conflitti con il tuo sistema.",
"diagnosis_basesystem_hardware_model": "Modello server: {model}",
"postinstall_low_rootfsspace": "La radice del filesystem ha uno spazio totale inferiore ai 10 GB, ed è piuttosto preoccupante! Consumerai tutta la memoria molto velocemente! Raccomandiamo di avere almeno 16 GB per la radice del filesystem. Se vuoi installare YunoHost ignorando questo avviso, esegui nuovamente il postinstall con l'argomento --force-diskspace",
"domain_remove_confirm_apps_removal": "Rimuovere questo dominio rimuoverà anche le seguenti applicazioni:\n{apps}\n\nSei sicuro di voler continuare? [{answers}]",
"diagnosis_rootfstotalspace_critical": "La radice del filesystem ha un totale di solo {space}, ed è piuttosto preoccupante! Probabilmente consumerai tutta la memoria molto velocemente! Raccomandiamo di avere almeno 16 GB per la radice del filesystem.",
"diagnosis_rootfstotalspace_warning": "La radice del filesystem ha un totale di solo {space}. Potrebbe non essere un problema, ma stai attento perché potresti consumare tutta la memoria velocemente... Raccomandiamo di avere almeno 16 GB per la radice del filesystem."
}

View file

@ -132,7 +132,7 @@
"domain_dyndns_already_subscribed": "Du har allerede abonnement på et DynDNS-domene",
"log_category_404": "Loggkategorien '{category}' finnes ikke",
"log_link_to_log": "Full logg for denne operasjonen: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'",
"log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log display {name}'",
"log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}{name}'",
"log_user_create": "Legg til '{}' bruker",
"app_change_url_success": "{app:s} nettadressen er nå {domain:s}{path:s}",
"app_install_failed": "Kunne ikke installere {app}: {error}"

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

@ -281,7 +281,7 @@
"migration_0003_problematic_apps_warning": "Notatz que las aplicacions seguentas, saique problematicas, son estadas desactivadas. Semblan daver estadas installadas duna lista daplicacions o que son pas marcadas coma «working ». En consequéncia, podèm pas assegurar que tendràn de foncionar aprèp la mesa a nivèl: {problematic_apps}",
"migrations_migration_has_failed": "La migracion {id} a pas capitat, abandon. Error : {exception}",
"migrations_skip_migration": "Passatge de la migracion {id}…",
"migrations_to_be_ran_manually": "La migracion {id} deu èsser lançada manualament. Mercés danar a Aisinas > Migracion dins linterfàcia admin, o lançar «yunohost tools migrations migrate ».",
"migrations_to_be_ran_manually": "La migracion {id} deu èsser lançada manualament. Mercés danar a Aisinas > Migracion dins linterfàcia admin, o lançar «yunohost tools migrations run ».",
"migrations_need_to_accept_disclaimer": "Per lançar la migracion {id} , avètz dacceptar aquesta clausa de non-responsabilitat:\n---\n{disclaimer}\n---\nSacceptatz de lançar la migracion, mercés de tornar executar la comanda amb lopcion accept-disclaimer.",
"pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »",
"service_description_dovecot": "permet als clients de messatjariá daccedir/recuperar los corrièls (via IMAP e POP3)",
@ -300,10 +300,10 @@
"log_corrupted_md_file": "Lo fichièr YAML de metadonadas ligat als jornals daudit es damatjat: « {md_file} »\nError: {error:s}",
"log_category_404": "La categoria de jornals daudit « {category} » existís pas",
"log_link_to_log": "Jornal complèt daquesta operacion: <a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>",
"log_help_to_get_log": "Per veire lo jornal daquesta operacion « {desc} », utilizatz la comanda «yunohost log display {name} »",
"log_help_to_get_log": "Per veire lo jornal daquesta operacion « {desc} », utilizatz la comanda «yunohost log show {name}{name} »",
"backup_php5_to_php7_migration_may_fail": "Impossible de convertir vòstre archiu per prendre en carga PHP 7, la restauracion de vòstras aplicacions PHP pòt reüssir pas a restaurar vòstras aplicacions PHP (rason: {error:s})",
"log_link_to_failed_log": "Loperacion « {desc} » a pas capitat! Per obténer dajuda, mercés <a href=\"#/tools/logs/{name}\"> de fornir lo jornal complèt de loperacion</a>",
"log_help_to_get_failed_log": "Loperacion « {desc} » a pas reüssit! Per obténer dajuda, mercés de partejar lo jornal daudit complèt daquesta operacion en utilizant la comanda «yunohost log display {name} --share »",
"log_help_to_get_failed_log": "Loperacion « {desc} » a pas reüssit! Per obténer dajuda, mercés de partejar lo jornal daudit complèt daquesta operacion en utilizant la comanda «yunohost log share {name} »",
"log_does_exists": "I a pas cap de jornal daudit per loperacion amb lo nom « {log} », utilizatz «yunohost log list» per veire totes los jornals doperacion disponibles",
"log_operation_unit_unclosed_properly": "Loperacion a pas acabat corrèctament",
"log_app_change_url": "Cambiar lURL de laplicacion « {} »",
@ -327,7 +327,7 @@
"log_user_delete": "Levar lutilizaire « {} »",
"log_user_update": "Actualizar las informacions de lutilizaire « {} »",
"log_domain_main_domain": "Far venir « {} » lo domeni màger",
"log_tools_migrations_migrate_forward": "Migrar",
"log_tools_migrations_migrate_forward": "Executar las migracions",
"log_tools_postinstall": "Realizar la post installacion del servidor YunoHost",
"log_tools_upgrade": "Actualizacion dels paquets sistèma",
"log_tools_shutdown": "Atudar lo servidor",
@ -340,8 +340,8 @@
"migration_0005_not_enough_space": "I a pas pro despaci disponible sus {path} per lançar la migracion daquela passa :(.",
"service_description_php7.0-fpm": "executa daplicacions escrichas en PHP amb nginx",
"users_available": "Lista dels utilizaires disponibles:",
"good_practices_about_admin_password": "Sètz per definir un nòu senhal per ladministracion. Lo senhal deu almens conténer 8 caractèrs - encara que siá de bon far dutilizar un senhal mai long quaquò (ex. una passafrasa) e/o dutilizar mantun tipes de caractèrs (majuscula, minuscula, nombre e caractèrs especials).",
"good_practices_about_user_password": "Sètz a mand de definir un nòu senhal dutilizaire. Lo nòu senhal deu conténer almens 8 caractèrs, es de bon far dutilizar un senhal mai long (es a dire una frasa de senhal) e/o utilizar mantuns tipes de caractèrs (majusculas, minusculas, nombres e caractèrs especials).",
"good_practices_about_admin_password": "Sètz per definir un nòu senhal per ladministracion. Lo senhal deu almens conténer 8 caractèrs - encara que siá de bon far dutilizar un senhal mai long quaquò (ex. una passafrasa) e/o dutilizar mantun tipe de caractèrs (majuscula, minuscula, nombre e caractèrs especials).",
"good_practices_about_user_password": "Sètz a mand de definir un nòu senhal dutilizaire. Lo nòu senhal deu conténer almens 8 caractèrs, es de bon far dutilizar un senhal mai long (es a dire una frasa de senhal) e/o utilizar mantun tipe de caractèrs (majusculas, minusculas, nombres e caractèrs especials).",
"migration_description_0006_sync_admin_and_root_passwords": "Sincronizar los senhals admin e root",
"migration_0006_disclaimer": "Ara YunoHost sespèra que los senhals admin e root sián sincronizats. En lançant aquesta migracion, vòstre senhal root serà remplaçat pel senhal admin.",
"password_listed": "Aqueste senhal es un dels mai utilizats al monde. Se vos plai utilizatz-ne un mai unic.",
@ -590,5 +590,22 @@
"app_manifest_install_ask_password": "Causissètz lo senhal administrator per aquesta aplicacion",
"app_manifest_install_ask_path": "Causissètz lo camin ont volètz installar aquesta aplicacion",
"app_manifest_install_ask_domain": "Causissètz lo domeni ont volètz installar aquesta aplicacion",
"app_argument_password_no_default": "Error pendent lanalisi de largument del senhal « {name} » : largument de senhal pòt pas aver de valor per defaut per de rason de seguretat"
"app_argument_password_no_default": "Error pendent lanalisi de largument del senhal « {name} » : largument de senhal pòt pas aver de valor per defaut per de rason de seguretat",
"app_label_deprecated": "Aquesta comanda es estada renduda obsolèta. Mercés d'utilizar lo nòva \"yunohost user permission update\" per gerir letiquetada de l'aplication",
"additional_urls_already_removed": "URL addicionala {url:s} es ja estada elimida per la permission «#permission:s»",
"additional_urls_already_added": "URL addicionadal «{url:s}'» es ja estada aponduda per la permission «{permission:s}»",
"migration_0015_yunohost_upgrade": "Aviada de la mesa a jorn de YunoHost...",
"migration_0015_main_upgrade": "Aviada de la mesa a nivèl generala...",
"migration_0015_patching_sources_list": "Mesa a jorn del fichièr sources.lists...",
"migration_0015_start": "Aviar la migracion cap a Buster",
"migration_description_0017_postgresql_9p6_to_11": "Migrar las basas de donadas de PostgreSQL 9.6 cap a 11",
"migration_description_0016_php70_to_php73_pools": "Migrar los fichièrs de configuracion php7.0 cap a php7.3",
"migration_description_0015_migrate_to_buster": "Mesa a nivèl dels sistèmas Debian Buster e YunoHost 4.x",
"migrating_legacy_permission_settings": "Migracion dels paramètres de permission ancians...",
"log_app_config_apply": "Aplicar la configuracion a laplicacion « {} »",
"log_app_config_show_panel": "Mostrar lo panèl de configuracion de laplicacion « {} »",
"log_app_action_run": "Executar laccion de laplicacion « {} »",
"diagnosis_basesystem_hardware_model": "Lo modèl del servidor es {model}",
"backup_archive_cant_retrieve_info_json": "Obtencion impossibla de las informacions de larchiu « {archive} »... Se pòt pas recuperar lo fichièr info.json (o es pas un fichièr json valid).",
"app_packaging_format_not_supported": "Se pòt pas installar aquesta aplicacion pramor que son format es pas pres en carga per vòstra version de YunoHost. Deuriatz considerar actualizar lo sistèma."
}

View file

@ -1,3 +1,12 @@
{
"password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków"
}
"password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków",
"app_already_up_to_date": "{app:s} jest obecnie aktualna",
"app_already_installed": "{app:s} jest już zainstalowane",
"already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.",
"admin_password_too_long": "Proszę wybrać hasło krótsze niż 127 znaków",
"admin_password_changed": "Hasło administratora zostało zmienione",
"admin_password_change_failed": "Nie można zmienić hasła",
"admin_password": "Hasło administratora",
"action_invalid": "Nieprawidłowa operacja '{action:s}'",
"aborting": "Przerywanie."
}

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,128 +78,84 @@ 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)
if not os.path.isdir(logdir):
os.makedirs(logdir, 0o750)
# ####################################################################### #
logging_configuration = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'console': {
'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
'tty-debug': {
'format': '%(relativeCreated)-4d %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'cli': {
'level': 'DEBUG' if debug else 'INFO',
'class': 'moulinette.interfaces.cli.TTYHandler',
'formatter': 'tty-debug' if debug else '',
},
'api': {
'level': 'DEBUG' if debug else 'INFO',
'class': 'moulinette.interfaces.api.APIQueueHandler',
},
'file': {
'class': 'logging.FileHandler',
'formatter': 'precise',
'filename': logfile,
'filters': ['action'],
},
},
'loggers': {
'yunohost': {
'level': 'DEBUG',
'handlers': ['file', interface] if not quiet else ['file'],
'propagate': False,
},
'moulinette': {
'level': 'DEBUG',
'handlers': ['file', interface] if not quiet else ['file'],
'propagate': False,
},
},
'root': {
'level': 'DEBUG',
'handlers': ['file', interface] if debug else ['file'],
},
}
# Logging configuration for CLI (or any other interface than api...) #
# ####################################################################### #
if interface != "api":
configure_logging({
'version': 1,
'main_logger': "yunohost",
'disable_existing_loggers': True,
'formatters': {
'tty-debug': {
'format': '%(relativeCreated)-4d %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'tty': {
'level': 'DEBUG' if debug else 'INFO',
'class': 'moulinette.interfaces.cli.TTYHandler',
'formatter': 'tty-debug' if debug else '',
},
'file': {
'class': 'logging.FileHandler',
'formatter': 'precise',
'filename': logfile,
'filters': ['action'],
},
},
'loggers': {
'yunohost': {
'level': 'DEBUG',
'handlers': ['file', 'tty'] if not quiet else ['file'],
'propagate': False,
},
'moulinette': {
'level': 'DEBUG',
'handlers': [],
'propagate': True,
},
'moulinette.interface': {
'level': 'DEBUG',
'handlers': ['file', 'tty'] if not quiet else ['file'],
'propagate': False,
},
},
'root': {
'level': 'DEBUG',
'handlers': ['file', 'tty'] if debug else ['file'],
},
})
# ####################################################################### #
configure_logging(logging_configuration)
# Logging configuration for API #
# ####################################################################### #
else:
configure_logging({
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'console': {
'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'api': {
'level': 'DEBUG' if debug else 'INFO',
'class': 'moulinette.interfaces.api.APIQueueHandler',
},
'file': {
'class': 'logging.handlers.WatchedFileHandler',
'formatter': 'precise',
'filename': logfile,
'filters': ['action'],
},
'console': {
'class': 'logging.StreamHandler',
'formatter': 'console',
'stream': 'ext://sys.stdout',
'filters': ['action'],
},
},
'loggers': {
'yunohost': {
'level': 'DEBUG',
'handlers': ['file', 'api'] + (['console'] if debug else []),
'propagate': False,
},
'moulinette': {
'level': 'DEBUG',
'handlers': [],
'propagate': True,
},
},
'root': {
'level': 'DEBUG',
'handlers': ['file'] + (['console'] if debug else []),
},
})
# We use a WatchedFileHandler instead of regular FileHandler to possibly support log rotation etc
logging_configuration["handlers"]["file"]["class"] = 'logging.handlers.WatchedFileHandler'
# This is for when launching yunohost-api in debug mode, we want to display stuff in the console
if debug:
logging_configuration["loggers"]["yunohost"]["handlers"].append("cli")
logging_configuration["loggers"]["moulinette"]["handlers"].append("cli")
logging_configuration["root"]["handlers"].append("cli")
configure_logging(logging_configuration)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -37,7 +37,7 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file
from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate
from yunohost.utils.error import YunohostError
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.network import get_public_ip
from yunohost.diagnosis import Diagnoser
@ -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,14 +54,14 @@ 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
VALIDITY_LIMIT = 15 # days
# For tests
STAGING_CERTIFICATION_AUTHORITY = "https://acme-staging.api.letsencrypt.org"
STAGING_CERTIFICATION_AUTHORITY = "https://acme-staging-v02.api.letsencrypt.org"
# For prod
PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v02.api.letsencrypt.org"
@ -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 YunohostValidationError("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 YunohostValidationError(
"certmanager_attempt_to_replace_valid_cert", domain=domain
)
operation_logger.start()
@ -185,13 +192,16 @@ 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()
out = out.decode("utf-8")
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 +227,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 +256,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 +267,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 YunohostValidationError("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 YunohostValidationError(
"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 +293,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 +343,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 +356,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 +371,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 YunohostValidationError("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 YunohostValidationError(
"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 YunohostValidationError(
"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 YunohostValidationError(
"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,24 +412,34 @@ 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:
_fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks)
except Exception as e:
import traceback
from StringIO import StringIO
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 +447,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 +503,12 @@ investigate :
-- Certificate Manager
""" % (domain, exception_message, stack, logs)
""" % (
domain,
exception_message,
stack,
logs,
)
message = """\
From: %s
@ -462,9 +516,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 +563,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 +582,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 +614,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 +646,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 +661,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", False, "DNS:" + subdomain)])
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)
@ -615,7 +703,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder):
csr_file = output_folder + domain + ".csr"
logger.debug("Saving to %s.", csr_file)
with open(csr_file, "w") as f:
with open(csr_file, "wb") as f:
f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr))
@ -624,29 +712,38 @@ 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
valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ")
organization_name = cert.get_issuer().O
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",
}
elif cert_issuer.startswith("Let's Encrypt") or cert_issuer == "R3":
elif organization_name == "Let's Encrypt":
CA_type = {
"code": "lets-encrypt",
"verbose": "Let's Encrypt",
@ -709,6 +806,7 @@ def _get_status(domain):
"summary": status_summary,
}
#
# Misc small stuff ... #
#
@ -722,10 +820,11 @@ 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, "w") as f:
with open(destination_path, "wb") as f:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
@ -760,15 +859,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 +884,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 YunohostValidationError("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 YunohostValidationError(
"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 YunohostValidationError("certmanager_domain_http_not_working", domain=domain)
# FIXME / TODO : ideally this should not be needed. There should be a proper
@ -817,11 +934,11 @@ def _regen_dnsmasq_if_needed():
for domainconf in domainsconf:
# Look for the IP, it's in the lines with this format :
# address=/the.domain.tld/11.22.33.44
# host-record=the.domain.tld,11.22.33.44
for line in open(domainconf).readlines():
if not line.startswith("address"):
if not line.startswith("host-record"):
continue
ip = line.strip().split("/")[2]
ip = line.strip().split(",")[-1]
# Compared found IP to current IPv4 / IPv6
# IPv6 IPv4
@ -840,7 +957,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 +967,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

@ -1,13 +1,13 @@
import subprocess
from moulinette import m18n
from yunohost.utils.error import YunohostError
from yunohost.utils.error import YunohostError, YunohostValidationError
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):
@ -23,43 +23,60 @@ class MyMigration(Migration):
return
if not self.package_is_installed("postgresql-11"):
raise YunohostError("migration_0017_postgresql_11_not_installed")
raise YunohostValidationError("migration_0017_postgresql_11_not_installed")
# Make sure there's a 9.6 cluster
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 YunohostValidationError(
"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")
@ -98,13 +111,18 @@ class MyMigration(Migration):
# Migrate old settings
migrate_legacy_permission_settings()
except Exception as e:
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.utils.error import YunohostError, YunohostValidationError
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 YunohostValidationError("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 YunohostValidationError(
"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 YunohostValidationError(
"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 YunohostValidationError(
"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 YunohostValidationError(
"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)
raise YunohostValidationError("%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 YunohostValidationError(
"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
@ -287,7 +331,7 @@ def diagnosis_ignore(add_filter=None, remove_filter=None, list=False):
configuration["ignore_filters"][category] = []
if criterias not in configuration["ignore_filters"][category]:
raise YunohostError("This filter does not exists.")
raise YunohostValidationError("This filter does not exists.")
configuration["ignore_filters"][category].remove(criterias)
_diagnosis_write_configuration(configuration)
@ -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))
@ -449,9 +551,8 @@ class Diagnoser():
@staticmethod
def get_description(id_):
key = "diagnosis_description_" + id_
descr = m18n.n(key)
# If no description available, fallback to id
return descr if descr.decode('utf-8') != key else id_
return m18n.n(key) if m18n.key_exists(key) else id_
@staticmethod
def i18n(report, force_remove_html_tags=False):
@ -476,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):
@ -486,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"])
@ -512,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
@ -558,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"
@ -581,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 yunohost.utils.error import YunohostError, YunohostValidationError
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:
@ -62,25 +73,17 @@ def domain_list(exclude_subdomains=False):
result_list.append(domain)
def cmp_domain(domain1, domain2):
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']
domain1 = domain1.split('.')
domain2 = domain2.split('.')
domain1[-1] = domain1[-2] + domain1.pop()
domain2[-1] = domain2[-2] + domain2.pop()
domain1 = list(reversed(domain1))
domain2 = list(reversed(domain2))
return cmp(domain1, domain2)
domain = domain.split(".")
domain[-1] = domain[-2] + domain.pop()
domain = list(reversed(domain))
return domain
result_list = sorted(result_list, cmp_domain)
result_list = sorted(result_list, key=cmp_domain)
return {'domains': result_list}
return {
'domains': result_list,
'main': _get_maindomain()
}
return {"domains": result_list, "main": _get_maindomain()}
@is_unit_operation()
@ -98,16 +101,14 @@ def domain_add(operation_logger, domain, dyndns=False):
from yunohost.utils.ldap import _get_ldap_interface
if domain.startswith("xmpp-upload."):
raise YunohostError("domain_cannot_add_xmpp_upload")
raise YunohostValidationError("domain_cannot_add_xmpp_upload")
ldap = _get_ldap_interface()
try:
ldap.validate_uniqueness({'virtualdomain': domain})
ldap.validate_uniqueness({"virtualdomain": domain})
except MoulinetteError:
raise YunohostError('domain_exists')
operation_logger.start()
raise YunohostValidationError("domain_exists")
# Lower domain to avoid some edge cases issues
# See: https://forum.yunohost.org/t/invalid-domain-causes-diagnosis-web-to-fail-fr-on-demand/11765
@ -116,36 +117,41 @@ 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_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 YunohostValidationError('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 YunohostValidationError("domain_dyndns_root_unknown")
operation_logger.start()
if dyndns:
from yunohost.dyndns import dyndns_subscribe
# 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
@ -157,38 +163,43 @@ 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)
except:
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
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)
raise YunohostValidationError('domain_name_unknown', domain=domain)
# Check domain is not the main domain
if domain == _get_maindomain():
@ -196,30 +207,49 @@ 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 YunohostValidationError(
"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 YunohostValidationError("domain_cannot_remove_main_add_new_one", domain=domain)
# Check if apps are installed on the domain
apps_on_that_domain = []
for app in _installed_apps():
settings = _get_app_settings(app)
label = app_info(app)["name"]
if settings.get("domain") == domain:
apps_on_that_domain.append("%s (on https://%s%s)" % (app, 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=", ".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 YunohostValidationError('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 ...
@ -238,14 +268,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):
@ -258,6 +290,9 @@ def domain_dns_conf(domain, ttl=None):
"""
if domain not in domain_list()["domains"]:
raise YunohostValidationError("domain_name_unknown", domain=domain)
ttl = 3600 if ttl is None else ttl
dns_conf = _build_dns_conf(domain, ttl)
@ -290,7 +325,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
@ -309,63 +344,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 YunohostValidationError("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):
@ -381,16 +411,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
@ -499,10 +524,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
],
}
##################
@ -511,7 +548,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
@ -527,18 +564,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)
@ -547,7 +594,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)
@ -573,19 +620,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:
@ -593,16 +648,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

@ -35,23 +35,21 @@ from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import write_to_file, read_file
from moulinette.utils.network import download_json
from moulinette.utils.process import check_output
from yunohost.utils.error import YunohostError
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.domain import _get_maindomain, _build_dns_conf
from yunohost.utils.network import get_public_ip
from yunohost.utils.network import get_public_ip, 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$"
)
@ -72,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
@ -94,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 == u"Domain %s is available" % domain
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
@ -119,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'):
raise YunohostError('domain_dyndns_already_subscribed')
if _guess_current_dyndns_domain(subscribe_host) != (None, None):
raise YunohostValidationError('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 YunohostValidationError(
"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 YunohostValidationError("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
@ -195,29 +229,68 @@ 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 YunohostValidationError('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 YunohostValidationError("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,
]
old_ipv4 = check_output("dig @%s +short %s" % (dyn_host, domain)) or None
old_ipv6 = check_output("dig @%s +short aaaa %s" % (dyn_host, domain)) or None
def resolve_domain(domain, rdtype):
# FIXME make this work for IPv6-only hosts too..
ok, result = dig(dyn_host, "A")
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)
)
else:
return None
logger.debug("Falling back to external resolvers")
ok, result = dig(domain, rdtype, resolvers="force_external")
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)
)
else:
return None
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")
# Get current IPv4 and IPv6
ipv4_ = get_public_ip()
@ -237,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...")
@ -270,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...")
@ -290,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):
@ -335,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..)
@ -353,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

@ -28,19 +28,20 @@ import yaml
import miniupnpc
from moulinette import m18n
from yunohost.utils.error import YunohostError
from yunohost.utils.error import YunohostError, YunohostValidationError
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,10 +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 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)
@ -89,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
@ -106,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 = []
@ -135,10 +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 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)
@ -163,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
@ -197,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",
@ -218,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",
@ -237,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",
@ -247,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",
@ -263,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
@ -275,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
@ -295,113 +327,131 @@ 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 YunohostValidationError("action_invalid", action=action)
# Refresh port mapping using UPnP
if not no_refresh:
upnpc = miniupnpc.UPnP()
upnpc = miniupnpc.UPnP(localport=1)
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']:
for port in firewall['uPnP'][protocol]:
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 Exception:
pass
firewall["uPnP"][protocol + "_TO_CLOSE"] = []
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
if enabled != firewall['uPnP']['enabled']:
firewall = firewall_list(raw=True)
firewall['uPnP']['enabled'] = enabled
_update_firewall_file(firewall)
# Make a backup and update firewall file
os.system("cp {0} {0}.old".format(FIREWALL_FILE))
with open(FIREWALL_FILE, 'w') as f:
yaml.safe_dump(firewall, f, default_flow_style=False)
if enabled != firewall["uPnP"]["enabled"]:
firewall = firewall_list(raw=True)
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():
@ -412,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")
@ -423,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):
@ -433,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
@ -446,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

@ -32,14 +32,14 @@ from glob import iglob
from importlib import import_module
from moulinette import m18n, msettings
from yunohost.utils.error import YunohostError
from yunohost.utils.error import YunohostError, YunohostValidationError
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 YunohostValidationError("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,63 +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 YunohostValidationError("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"):
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:
@ -200,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, no_trace=False, 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
@ -220,7 +241,6 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
action -- Action name
hooks -- List of hooks names to execute
args -- Ordered list of arguments to pass to the scripts
no_trace -- Do not print each command that will be executed
chdir -- The directory from where the scripts will be executed
env -- Dictionnary of environment variables to export
pre_callback -- An object to call before each script execution with
@ -235,11 +255,9 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, 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, no_trace=False, 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, no_trace=False, chdir=None,
try:
hl = hooks_names[n]
except KeyError:
raise YunohostError('hook_name_unknown', n)
raise YunohostValidationError("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,
no_trace=no_trace, 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, no_trace=False,
chdir=None, env=None, user="root", 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
@ -306,18 +329,15 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
path -- Path of the script to execute
args -- Ordered list of arguments to pass to the script
raise_on_error -- Raise if the script returns a non-zero exit code
no_trace -- Do not print each command that will be executed
chdir -- The directory from where the script will be executed
env -- Dictionnary of environment variables to export
user -- User with which to run the command
"""
# 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):
@ -331,108 +351,100 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
r"invalid value for trace file descriptor",
r"Creating config file .* with new version",
r"Created symlink /etc/systemd",
r"dpkg: warning: while removing .* not empty so not removed"
r"dpkg: warning: while removing .* not empty so not removed",
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, no_trace, chdir, env, user, 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
def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, loggers):
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')
stdinfo = os.path.join(tempfile.mkdtemp(), "stdinfo")
env['YNH_STDINFO'] = stdinfo
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
# Construct command to execute
if user == "root":
command = ['sh', '-c']
else:
command = ['sudo', '-n', '-u', user, '-H', 'sh', '-c']
# use xtrace on fd 7 which is redirected to stdout
env["BASH_XTRACEFD"] = "7"
cmd = '/bin/bash -x "{script}" {args} 7>&1'
cmd = cmd.format(script=cmd_script, args=cmd_args)
if no_trace:
cmd = '/bin/bash "{script}" {args}'
else:
# use xtrace on fd 7 which is redirected to stdout
cmd = 'BASH_XTRACEFD=7 /bin/bash -x "{script}" {args} 7>&1'
logger.debug("Executing command '%s'" % cmd)
# prepend environment variables
cmd = '{0} {1}'.format(
' '.join(['{0}={1}'.format(k, shell_quote(v))
for k, v in env.items()]), cmd)
command.append(cmd.format(script=cmd_script, args=cmd_args))
_env = os.environ.copy()
_env.update(env)
logger.debug("Executing command '%s'" % ' '.join(command))
returncode = call_async_output(
command, loggers, shell=False, cwd=chdir,
stdinfo=stdinfo
)
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"):
@ -441,7 +453,10 @@ def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, logge
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)
@ -461,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
@ -484,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

@ -35,18 +35,18 @@ from logging import FileHandler, getLogger, Formatter
from moulinette import m18n, msettings
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from yunohost.utils.error import YunohostError, YunohostValidationError
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):
@ -65,8 +65,7 @@ def log_list(limit=None, with_details=False, with_suboperations=False):
operations = {}
logs = filter(lambda x: x.endswith(METADATA_FILE_EXT),
os.listdir(OPERATIONS_PATH))
logs = [x for x in os.listdir(OPERATIONS_PATH) if x.endswith(METADATA_FILE_EXT)]
logs = list(reversed(sorted(logs)))
if limit is not None:
@ -74,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 = {
@ -89,14 +88,16 @@ def log_list(limit=None, with_details=False, with_suboperations=False):
pass
try:
metadata = read_yaml(md_path)
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:
entry["success"] = metadata.get("success", "?") if metadata else "?"
entry["success"] = metadata.get("success", "?")
entry["parent"] = metadata.get("parent")
if with_suboperations:
@ -124,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_display(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.
@ -157,7 +160,7 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su
r"args_array=.*$",
r"local -A args_array$",
r"ynh_handle_getopts_args",
r"ynh_script_progression"
r"ynh_script_progression",
]
else:
filters = []
@ -165,19 +168,21 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su
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)
@ -186,17 +191,18 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su
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 YunohostValidationError("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)
@ -208,7 +214,7 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su
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
@ -218,17 +224,17 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su
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:
@ -249,19 +255,25 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su
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.get("parent") == base_filename:
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())
@ -269,6 +281,7 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su
# 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:
@ -278,14 +291,21 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su
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 is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'],
exclude=['password'], operation_key=None):
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,
):
"""
Configure quickly a unit operation
@ -307,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
@ -320,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 = ()
@ -337,7 +359,7 @@ def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'],
entity_type = entity
if entity in kwargs and kwargs[entity] is not None:
if isinstance(kwargs[entity], basestring):
if isinstance(kwargs[entity], str):
related_to.append((entity_type, kwargs[entity]))
else:
for x in kwargs[entity]:
@ -362,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
@ -376,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):
@ -387,11 +414,13 @@ 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())
match = re.search(r'(pwd|pass|password|secret\w*|\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):
@ -460,13 +489,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]
@ -512,10 +547,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):
@ -527,7 +564,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
@ -551,7 +588,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
@ -561,19 +598,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
@ -594,10 +631,19 @@ class OperationLogger(object):
"""
Close properly the unit operation
"""
# When the error happen's in the is_unit_operation try/except,
# we want to inject the log ref in the exception, such that it may be
# transmitted to the webadmin which can then redirect to the appropriate
# log page
if isinstance(error, Exception) and not isinstance(error, YunohostValidationError):
error.log_ref = self.name
if self.ended_at is not None or self.started_at is None:
return
if error is not None and not isinstance(error, basestring):
if error is not None and not isinstance(error, str):
error = str(error)
self.ended_at = datetime.utcnow()
self._error = error
self._success = error is None
@ -606,21 +652,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
@ -634,7 +682,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

@ -31,10 +31,10 @@ import random
from moulinette import m18n
from moulinette.utils.log import getActionLogger
from yunohost.utils.error import YunohostError
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.log import is_unit_operation
logger = getActionLogger('yunohost.user')
logger = getActionLogger("yunohost.user")
SYSTEM_PERMS = ["mail", "xmpp", "sftp", "ssh"]
@ -45,40 +45,60 @@ 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
"""
# Fetch relevant informations
from yunohost.app import app_setting, app_list
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 = [app["id"] for app in app_list()["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 = 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")
}
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,34 +107,52 @@ 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"]
infos["label"] = "%s (%s)" % (main_perm_label, infos["label"])
if short:
permissions = permissions.keys()
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 YunohostValidationError("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 YunohostValidationError("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 YunohostValidationError("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 YunohostValidationError("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 YunohostValidationError("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 YunohostValidationError("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 YunohostValidationError("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
@ -668,48 +837,53 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app):
For example:
re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$
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$
"""
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 YunohostValidationError("invalid_regex", regex=regex)
if url.startswith('re:'):
if url.startswith("re:"):
# regex without domain
if url.startswith('re:/'):
validate_regex(url[4:])
# we check for the first char after 're:'
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 YunohostValidationError("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 YunohostValidationError("domain_name_unknown", domain=domain)
validate_regex(path)
return 're:' + domain + path
return "re:" + domain + path
#
# "Regular" URIs
@ -717,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)
@ -740,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 YunohostValidationError("domain_name_unknown", domain=domain)
_assert_no_conflicting_apps(domain, path, ignore_app=app)

View file

@ -21,7 +21,6 @@
import os
import yaml
import subprocess
import shutil
import hashlib
@ -30,24 +29,31 @@ from datetime import datetime
from moulinette import m18n
from moulinette.utils import log, filesystem
from moulinette.utils.process import check_output
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 = subprocess.check_output("dpkg-query -W -f='${Conffiles}\n' '*' \
| awk 'OFS=\" \"{print $2,$1}' \
| md5sum -c 2>/dev/null \
| awk -F': ' '$2 !~ /OK/{print $1}'", shell=True)
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}'"
)
files = files.strip().split("\n")
if ignore_handled_by_regenconf:

View file

@ -34,16 +34,27 @@ from glob import glob
from datetime import datetime
from moulinette import m18n
from yunohost.utils.error import YunohostError
from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils.process import check_output
from moulinette.utils.log import getActionLogger
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
@ -68,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
@ -86,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
@ -100,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
@ -112,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):
@ -128,16 +145,16 @@ def service_remove(name):
services = _get_services()
if name not in services:
raise YunohostError('service_unknown', service=name)
raise YunohostValidationError("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):
@ -152,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):
@ -171,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):
@ -190,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):
@ -208,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):
@ -226,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):
@ -244,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):
@ -261,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=[]):
@ -286,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 YunohostValidationError("service_unknown", service=name)
# Filter only requested servivces
services = {k: v for k, v in services.items() if k in names}
@ -299,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]]
@ -312,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
@ -337,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
@ -351,46 +397,54 @@ def _get_and_format_service_status(service, infos):
# If no description was there, try to get it from the .json locales
if not description:
translation_key = "service_description_%s" % service
description = m18n.n(translation_key)
# If descrption is still equal to the translation key,
# that mean that we don't have a translation for this string
# that's the only way to test for that for now
# if we don't have it, uses the one provided by systemd
if description.decode('utf-8') == translation_key:
translation_key = "service_description_%s" % service
if m18n.key_exists(translation_key):
description = m18n.n(translation_key)
else:
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
@ -399,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:
@ -428,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 YunohostValidationError("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]
@ -471,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()
@ -486,14 +545,15 @@ 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 YunohostValidationError("service_unknown", service=name)
if names is []:
names = services.keys()
names = list(services.keys())
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)
@ -508,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 YunohostValidationError("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"
@ -534,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:
@ -563,16 +639,16 @@ def _give_lock(action, service, p):
while son_PID == 0 and p.poll() is None:
# Call systemctl to get the PID
# Output of the command is e.g. ControlPID=1234
son_PID = subprocess.check_output(cmd_get_son_PID.split()) \
.strip().split("=")[1]
son_PID = check_output(cmd_get_son_PID).split("=")[1]
son_PID = int(son_PID)
time.sleep(1)
# 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
@ -583,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():
@ -592,19 +668,21 @@ 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
# filter this
for key, value in services.items():
for key, value in list(services.items()):
if value is None:
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])]
@ -636,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
@ -657,6 +735,7 @@ def _tail(file, n):
try:
if file.endswith(".gz"):
import gzip
f = gzip.open(file)
lines = f.read().splitlines()
else:
@ -697,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
@ -720,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 subprocess.check_output("journalctl --no-hostname --no-pager -u {0} -n{1}".format(systemd_service, number), shell=True)
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()
)

Some files were not shown because too many files have changed in this diff Show more