Merge branch 'dev' into enh-config-panel-file

This commit is contained in:
ljf 2021-08-30 13:21:53 +02:00
commit 8e2cf58862
58 changed files with 1381 additions and 448 deletions

View file

@ -181,3 +181,12 @@ test-service:
only: only:
changes: changes:
- src/yunohost/service.py - src/yunohost/service.py
test-ldapauth:
extends: .test-stage
script:
- cd src/yunohost
- python3 -m pytest tests/test_ldapauth.py
only:
changes:
- src/yunohost/authenticators/*.py

173
bin/yunomdns Executable file
View file

@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""
Pythonic declaration of mDNS .local domains for YunoHost
"""
import subprocess
import re
import sys
import yaml
import socket
from time import sleep
from typing import List, Dict
from zeroconf import Zeroconf, ServiceInfo
# Helper command taken from Moulinette
def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs):
"""Run command with arguments and return its output as a byte string
Overwrite some of the arguments to capture standard error in the result
and use shell by default before calling subprocess.check_output.
"""
return (
subprocess.check_output(args, stderr=stderr, shell=shell, **kwargs)
.decode("utf-8")
.strip()
)
# Helper command taken from Moulinette
def _extract_inet(string, skip_netmask=False, skip_loopback=True):
"""
Extract IP addresses (v4 and/or v6) from a string limited to one
address by protocol
Keyword argument:
string -- String to search in
skip_netmask -- True to skip subnet mask extraction
skip_loopback -- False to include addresses reserved for the
loopback interface
Returns:
A dict of {protocol: address} with protocol one of 'ipv4' or 'ipv6'
"""
ip4_pattern = (
r"((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}"
)
ip6_pattern = r"(((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::?((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)"
ip4_pattern += r"/[0-9]{1,2})" if not skip_netmask else ")"
ip6_pattern += r"/[0-9]{1,3})" if not skip_netmask else ")"
result = {}
for m in re.finditer(ip4_pattern, string):
addr = m.group(1)
if skip_loopback and addr.startswith("127."):
continue
# Limit to only one result
result["ipv4"] = addr
break
for m in re.finditer(ip6_pattern, string):
addr = m.group(1)
if skip_loopback and addr == "::1":
continue
# Limit to only one result
result["ipv6"] = addr
break
return result
# Helper command taken from Moulinette
def get_network_interfaces():
# Get network devices and their addresses (raw infos from 'ip addr')
devices_raw = {}
output = check_output("ip --brief a").split("\n")
for line in output:
line = line.split()
iname = line[0]
ips = ' '.join(line[2:])
devices_raw[iname] = ips
# Parse relevant informations for each of them
devices = {
name: _extract_inet(addrs)
for name, addrs in devices_raw.items()
if name != "lo"
}
return devices
if __name__ == '__main__':
###
# CONFIG
###
with open('/etc/yunohost/mdns.yml', 'r') as f:
config = yaml.safe_load(f) or {}
updated = False
required_fields = ["interfaces", "domains"]
missing_fields = [field for field in required_fields if field not in config]
if missing_fields:
print("The fields %s are required" % ', '.join(missing_fields))
if config['interfaces'] is None:
print('No interface listed for broadcast.')
sys.exit(0)
if 'yunohost.local' not in config['domains']:
config['domains'].append('yunohost.local')
zcs = {}
interfaces = get_network_interfaces()
for interface in config['interfaces']:
infos = [] # List of ServiceInfo objects, to feed Zeroconf
ips = [] # Human-readable IPs
b_ips = [] # Binary-convered IPs
ipv4 = interfaces[interface]['ipv4'].split('/')[0]
if ipv4:
ips.append(ipv4)
b_ips.append(socket.inet_pton(socket.AF_INET, ipv4))
ipv6 = interfaces[interface]['ipv6'].split('/')[0]
if ipv6:
ips.append(ipv6)
b_ips.append(socket.inet_pton(socket.AF_INET6, ipv6))
# If at least one IP is listed
if ips:
# Create a Zeroconf object, and store the ServiceInfos
zc = Zeroconf(interfaces=ips)
zcs[zc]=[]
for d in config['domains']:
d_domain=d.replace('.local','')
if '.' in d_domain:
print(d_domain+'.local: subdomains are not supported.')
else:
# Create a ServiceInfo object for each .local domain
zcs[zc].append(ServiceInfo(
type_='_device-info._tcp.local.',
name=interface+': '+d_domain+'._device-info._tcp.local.',
addresses=b_ips,
port=80,
server=d+'.',
))
print('Adding '+d+' with addresses '+str(ips)+' on interface '+interface)
# Run registration
print("Registering...")
for zc, infos in zcs.items():
for info in infos:
zc.register_service(info)
try:
print("Registered. Press Ctrl+C or stop service to stop.")
while True:
sleep(1)
except KeyboardInterrupt:
pass
finally:
print("Unregistering...")
for zc, infos in zcs.items():
for info in infos:
zc.unregister_service(info)
zc.close()

View file

@ -33,18 +33,10 @@
# Global parameters # # Global parameters #
############################# #############################
_global: _global:
configuration: name: yunohost.admin
authenticate: authentication:
- api api: ldap_admin
authenticator: cli: null
default:
vendor: ldap
help: admin_password
parameters:
uri: ldap://localhost:389
base_dn: dc=yunohost,dc=org
user_rdn: cn=admin,dc=yunohost,dc=org
argument_auth: false
arguments: arguments:
-v: -v:
full: --version full: --version
@ -673,7 +665,11 @@ app:
api: DELETE /apps/<app> api: DELETE /apps/<app>
arguments: arguments:
app: app:
help: App to delete help: App to remove
-p:
full: --purge
help: Also remove all application data
action: store_true
### app_upgrade() ### app_upgrade()
upgrade: upgrade:
@ -693,6 +689,10 @@ app:
full: --force full: --force
help: Force the update, even though the app is up to date help: Force the update, even though the app is up to date
action: store_true action: store_true
-b:
full: --no-safety-backup
help: Disable the safety backup during upgrade
action: store_true
### app_change_url() ### app_change_url()
change-url: change-url:
@ -1417,9 +1417,9 @@ tools:
postinstall: postinstall:
action_help: YunoHost post-install action_help: YunoHost post-install
api: POST /postinstall api: POST /postinstall
configuration: authentication:
# We need to be able to run the postinstall without being authenticated, otherwise we can't run the postinstall # We need to be able to run the postinstall without being authenticated, otherwise we can't run the postinstall
authenticate: false api: null
arguments: arguments:
-d: -d:
full: --domain full: --domain

View file

@ -32,7 +32,7 @@ def get_dict_actions(OPTION_SUBTREE, category):
with open(ACTIONSMAP_FILE, "r") as stream: with open(ACTIONSMAP_FILE, "r") as stream:
# Getting the dictionary containning what actions are possible per category # Getting the dictionary containning what actions are possible per category
OPTION_TREE = yaml.load(stream) OPTION_TREE = yaml.safe_load(stream)
CATEGORY = [ CATEGORY = [
category for category in OPTION_TREE.keys() if not category.startswith("_") category for category in OPTION_TREE.keys() if not category.startswith("_")

View file

@ -31,7 +31,11 @@ ynh_multimedia_build_main_dir() {
mkdir -p "$MEDIA_DIRECTORY/$user/eBook" mkdir -p "$MEDIA_DIRECTORY/$user/eBook"
ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share" ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share"
# Création du lien symbolique dans le home de l'utilisateur. # Création du lien symbolique dans le home de l'utilisateur.
ln -sfn "$MEDIA_DIRECTORY/$user" "/home/$user/Multimedia" #link will only be created if the home directory of the user exists and if it's located in '/home' folder
local user_home="$(getent passwd $user | cut -d: -f6 | grep '^/home/')"
if [[ -d "$user_home" ]]; then
ln -sfn "$MEDIA_DIRECTORY/$user" "$user_home/Multimedia"
fi
# Propriétaires des dossiers utilisateurs. # Propriétaires des dossiers utilisateurs.
chown -R $user "$MEDIA_DIRECTORY/$user" chown -R $user "$MEDIA_DIRECTORY/$user"
done done

View file

@ -86,7 +86,7 @@ key, value = os.environ['KEY'], os.environ.get('VALUE', None)
setting_file = "/etc/yunohost/apps/%s/settings.yml" % app setting_file = "/etc/yunohost/apps/%s/settings.yml" % app
assert os.path.exists(setting_file), "Setting file %s does not exists ?" % setting_file assert os.path.exists(setting_file), "Setting file %s does not exists ?" % setting_file
with open(setting_file) as f: with open(setting_file) as f:
settings = yaml.load(f) settings = yaml.safe_load(f)
if action == "get": if action == "get":
if key in settings: if key in settings:
print(settings[key]) print(settings[key])
@ -96,7 +96,7 @@ else:
del settings[key] del settings[key]
elif action == "set": elif action == "set":
if key in ['redirected_urls', 'redirected_regex']: if key in ['redirected_urls', 'redirected_regex']:
value = yaml.load(value) value = yaml.safe_load(value)
settings[key] = value settings[key] = value
else: else:
raise ValueError("action should either be get, set or delete") raise ValueError("action should either be get, set or delete")

View file

@ -2,8 +2,6 @@
set -e set -e
services_path="/etc/yunohost/services.yml"
do_init_regen() { do_init_regen() {
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
echo "You must be root to run this script" 1>&2 echo "You must be root to run this script" 1>&2
@ -19,8 +17,6 @@ do_init_regen() {
|| echo "yunohost.org" > /etc/yunohost/current_host || echo "yunohost.org" > /etc/yunohost/current_host
# copy default services and firewall # copy default services and firewall
[[ -f $services_path ]] \
|| cp services.yml "$services_path"
[[ -f /etc/yunohost/firewall.yml ]] \ [[ -f /etc/yunohost/firewall.yml ]] \
|| cp firewall.yml /etc/yunohost/firewall.yml || cp firewall.yml /etc/yunohost/firewall.yml
@ -49,6 +45,9 @@ do_init_regen() {
chmod 644 /etc/ssowat/conf.json.persistent chmod 644 /etc/ssowat/conf.json.persistent
chown root:root /etc/ssowat/conf.json.persistent chown root:root /etc/ssowat/conf.json.persistent
# Empty service conf
touch /etc/yunohost/services.yml
mkdir -p /var/cache/yunohost/repo mkdir -p /var/cache/yunohost/repo
chown root:root /var/cache/yunohost chown root:root /var/cache/yunohost
chmod 700 /var/cache/yunohost chmod 700 /var/cache/yunohost
@ -59,25 +58,9 @@ do_pre_regen() {
cd /usr/share/yunohost/templates/yunohost cd /usr/share/yunohost/templates/yunohost
# update services.yml # Legacy code that can be removed once on bullseye
if [[ -f $services_path ]]; then touch /etc/yunohost/services.yml
tmp_services_path="${services_path}-tmp" yunohost tools shell -c "from yunohost.service import _get_services, _save_services; _save_services(_get_services())"
new_services_path="${services_path}-new"
cp "$services_path" "$tmp_services_path"
_update_services "$new_services_path" || {
mv "$tmp_services_path" "$services_path"
exit 1
}
if [[ -f $new_services_path ]]; then
# replace services.yml with new one
mv "$new_services_path" "$services_path"
mv "$tmp_services_path" "${services_path}-old"
else
rm -f "$tmp_services_path"
fi
else
cp services.yml /etc/yunohost/services.yml
fi
mkdir -p $pending_dir/etc/cron.d/ mkdir -p $pending_dir/etc/cron.d/
mkdir -p $pending_dir/etc/cron.daily/ mkdir -p $pending_dir/etc/cron.daily/
@ -144,6 +127,14 @@ HandleLidSwitch=ignore
HandleLidSwitchDocked=ignore HandleLidSwitchDocked=ignore
HandleLidSwitchExternalPower=ignore HandleLidSwitchExternalPower=ignore
EOF EOF
mkdir -p ${pending_dir}/etc/systemd/
if [[ "$(yunohost settings get 'security.experimental.enabled')" == "True" ]]
then
cp proc-hidepid.service ${pending_dir}/etc/systemd/system/proc-hidepid.service
else
touch ${pending_dir}/etc/systemd/system/proc-hidepid.service
fi
} }
@ -204,65 +195,13 @@ do_post_regen() {
# Propagates changes in systemd service config overrides # Propagates changes in systemd service config overrides
[[ ! "$regen_conf_files" =~ "ntp.service.d/ynh-override.conf" ]] || { systemctl daemon-reload; systemctl restart ntp; } [[ ! "$regen_conf_files" =~ "ntp.service.d/ynh-override.conf" ]] || { systemctl daemon-reload; systemctl restart ntp; }
[[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload [[ ! "$regen_conf_files" =~ "nftables.service.d/ynh-override.conf" ]] || systemctl daemon-reload
} [[ ! "$regen_conf_files" =~ "login.conf.d/ynh-override.conf" ]] || systemctl daemon-reload
if [[ "$regen_conf_files" =~ "proc-hidepid.service" ]]
_update_services() { then
python3 - << EOF systemctl daemon-reload
import yaml action=$([[ -e /etc/systemd/system/proc-hidepid.service ]] && echo 'enable' || echo 'disable')
systemctl $action proc-hidepid --quiet --now
fi
with open('services.yml') as f:
new_services = yaml.load(f)
with open('/etc/yunohost/services.yml') as f:
services = yaml.load(f) or {}
updated = False
for service, conf in new_services.items():
# remove service with empty conf
if conf is None:
if service in services:
print("removing '{0}' from services".format(service))
del services[service]
updated = True
# add new service
elif not services.get(service, None):
print("adding '{0}' to services".format(service))
services[service] = conf
updated = True
# update service conf
else:
conffiles = services[service].pop('conffiles', {})
# status need to be removed
if "status" not in conf and "status" in services[service]:
print("update '{0}' service status access".format(service))
del services[service]["status"]
updated = True
if services[service] != conf:
print("update '{0}' service".format(service))
services[service].update(conf)
updated = True
if conffiles:
services[service]['conffiles'] = conffiles
# Remove legacy /var/log/daemon.log and /var/log/syslog from log entries
# because they are too general. Instead, now the journalctl log is
# returned by default which is more relevant.
if "log" in services[service]:
if services[service]["log"] in ["/var/log/syslog", "/var/log/daemon.log"]:
del services[service]["log"]
if updated:
with open('/etc/yunohost/services.yml-new', 'w') as f:
yaml.safe_dump(services, f, default_flow_style=False)
EOF
} }
FORCE=${2:-0} FORCE=${2:-0}

View file

@ -2,7 +2,7 @@
set -e set -e
tmp_backup_dir_file="/tmp/slapd-backup-dir.txt" tmp_backup_dir_file="/root/slapd-backup-dir.txt"
config="/usr/share/yunohost/templates/slapd/config.ldif" config="/usr/share/yunohost/templates/slapd/config.ldif"
db_init="/usr/share/yunohost/templates/slapd/db_init.ldif" db_init="/usr/share/yunohost/templates/slapd/db_init.ldif"

View file

@ -17,19 +17,16 @@ Pin-Priority: -1" >> "${pending_dir}/etc/apt/preferences.d/extra_php_version"
done done
echo " echo "
# Yes !
# This is what's preventing you from installing apache2 ! # PLEASE READ THIS WARNING AND DON'T EDIT THIS FILE
#
# Maybe take two fucking minutes to realize that if you try to install # You are probably reading this file because you tried to install apache2 or
# apache2, this will break nginx and break the entire YunoHost ecosystem. # bind9. These 2 packages conflict with YunoHost.
# on your server.
# # Installing apache2 will break nginx and break the entire YunoHost ecosystem
# So, *NO* # on your server, therefore don't remove those lines!
# DO NOT do this.
# DO NOT remove these lines. # You have been warned.
#
# I warned you. I WARNED YOU! But did you listen to me?
# Oooooh, noooo. You knew it all, didn't you?
Package: apache2 Package: apache2
Pin: release * Pin: release *
@ -39,9 +36,9 @@ Package: apache2-bin
Pin: release * Pin: release *
Pin-Priority: -1 Pin-Priority: -1
# Also yes, bind9 will conflict with dnsmasq. # Also bind9 will conflict with dnsmasq.
# Same story than for apache2. # Same story as for apache2.
# Don't fucking install it. # Don't install it, don't remove those lines.
Package: bind9 Package: bind9
Pin: release * Pin: release *

View file

@ -61,6 +61,7 @@ do_pre_regen() {
# Support different strategy for security configurations # Support different strategy for security configurations
export compatibility="$(yunohost settings get 'security.nginx.compatibility')" export compatibility="$(yunohost settings get 'security.nginx.compatibility')"
export experimental="$(yunohost settings get 'security.experimental.enabled')"
ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc"
cert_status=$(yunohost domain cert-status --json) cert_status=$(yunohost domain cert-status --json)

View file

@ -1,37 +0,0 @@
#!/bin/bash
set -e
do_pre_regen() {
pending_dir=$1
cd /usr/share/yunohost/templates/avahi-daemon
install -D -m 644 avahi-daemon.conf \
"${pending_dir}/etc/avahi/avahi-daemon.conf"
}
do_post_regen() {
regen_conf_files=$1
[[ -z "$regen_conf_files" ]] \
|| systemctl restart avahi-daemon
}
FORCE=${2:-0}
DRY_RUN=${3:-0}
case "$1" in
pre)
do_pre_regen $4
;;
post)
do_post_regen $4
;;
*)
echo "hook called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

83
data/hooks/conf_regen/37-mdns Executable file
View file

@ -0,0 +1,83 @@
#!/bin/bash
set -e
_generate_config() {
echo "domains:"
echo " - yunohost.local"
for domain in $YNH_DOMAINS
do
# Only keep .local domains (don't keep
[[ "$domain" =~ [^.]+\.[^.]+\.local$ ]] && echo "Subdomain $domain cannot be handled by Bonjour/Zeroconf/mDNS" >&2
[[ "$domain" =~ ^[^.]+\.local$ ]] || continue
echo " - $domain"
done
echo "interfaces:"
local_network_interfaces="$(ip --brief a | grep ' 10\.\| 192\.168\.' | awk '{print $1}')"
for interface in $local_network_interfaces
do
echo " - $interface"
done
}
do_init_regen() {
do_pre_regen
do_post_regen /etc/systemd/system/yunomdns.service
systemctl enable yunomdns
}
do_pre_regen() {
pending_dir="$1"
cd /usr/share/yunohost/templates/mdns
mkdir -p ${pending_dir}/etc/systemd/system/
cp yunomdns.service ${pending_dir}/etc/systemd/system/
getent passwd mdns &>/dev/null || useradd --no-create-home --shell /usr/sbin/nologin --system --user-group mdns
mkdir -p ${pending_dir}/etc/yunohost
_generate_config > ${pending_dir}/etc/yunohost/mdns.yml
}
do_post_regen() {
regen_conf_files="$1"
chown mdns:mdns /etc/yunohost/mdns.yml
# If we changed the systemd ynh-override conf
if echo "$regen_conf_files" | sed 's/,/\n/g' | grep -q "^/etc/systemd/system/yunomdns.service$"
then
systemctl daemon-reload
fi
# Legacy stuff to enable the new yunomdns service on legacy systems
if [[ -e /etc/avahi/avahi-daemon.conf ]] && grep -q 'yunohost' /etc/avahi/avahi-daemon.conf
then
systemctl enable yunomdns
fi
[[ -z "$regen_conf_files" ]] \
|| systemctl restart yunomdns
}
FORCE=${2:-0}
DRY_RUN=${3:-0}
case "$1" in
pre)
do_pre_regen $4
;;
post)
do_post_regen $4
;;
init)
do_init_regen
;;
*)
echo "hook called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

View file

@ -13,6 +13,7 @@ from yunohost.diagnosis import Diagnoser
from yunohost.domain import domain_list, _build_dns_conf, _get_maindomain 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"]
SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"]
class DNSRecordsDiagnoser(Diagnoser): class DNSRecordsDiagnoser(Diagnoser):
@ -29,8 +30,14 @@ class DNSRecordsDiagnoser(Diagnoser):
for domain in all_domains: for domain in all_domains:
self.logger_debug("Diagnosing DNS conf for %s" % domain) self.logger_debug("Diagnosing DNS conf for %s" % domain)
is_subdomain = domain.split(".", 1)[1] in all_domains is_subdomain = domain.split(".", 1)[1] in all_domains
is_specialusedomain = any(
domain.endswith("." + tld) for tld in SPECIAL_USE_TLDS
)
for report in self.check_domain( for report in self.check_domain(
domain, domain == main_domain, is_subdomain=is_subdomain domain,
domain == main_domain,
is_subdomain=is_subdomain,
is_specialusedomain=is_specialusedomain,
): ):
yield report yield report
@ -48,7 +55,7 @@ class DNSRecordsDiagnoser(Diagnoser):
for report in self.check_expiration_date(domains_from_registrar): for report in self.check_expiration_date(domains_from_registrar):
yield report yield report
def check_domain(self, domain, is_main_domain, is_subdomain): def check_domain(self, domain, is_main_domain, is_subdomain, is_specialusedomain):
expected_configuration = _build_dns_conf( expected_configuration = _build_dns_conf(
domain, include_empty_AAAA_if_no_ipv6=True domain, include_empty_AAAA_if_no_ipv6=True
@ -59,6 +66,15 @@ class DNSRecordsDiagnoser(Diagnoser):
if is_subdomain: if is_subdomain:
categories = ["basic"] categories = ["basic"]
if is_specialusedomain:
categories = []
yield dict(
meta={"domain": domain},
data={},
status="INFO",
summary="diagnosis_dns_specialusedomain",
)
for category in categories: for category in categories:
records = expected_configuration[category] records = expected_configuration[category]

View file

@ -34,6 +34,12 @@ class WebDiagnoser(Diagnoser):
summary="diagnosis_http_nginx_conf_not_up_to_date", summary="diagnosis_http_nginx_conf_not_up_to_date",
details=["diagnosis_http_nginx_conf_not_up_to_date_details"], details=["diagnosis_http_nginx_conf_not_up_to_date_details"],
) )
elif domain.endswith(".local"):
yield dict(
meta={"domain": domain},
status="INFO",
summary="diagnosis_http_localdomain",
)
else: else:
domains_to_check.append(domain) domains_to_check.append(domain)

View file

@ -0,0 +1,76 @@
#!/usr/bin/env python
import os
from yunohost.app import app_list
from yunohost.diagnosis import Diagnoser
class AppDiagnoser(Diagnoser):
id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
cache_duration = 300
dependencies = []
def run(self):
apps = app_list(full=True)["apps"]
for app in apps:
app["issues"] = list(self.issues(app))
if not any(app["issues"] for app in apps):
yield dict(
meta={"test": "apps"},
status="SUCCESS",
summary="diagnosis_apps_allgood",
)
else:
for app in apps:
if not app["issues"]:
continue
level = "ERROR" if any(issue[0] == "error" for issue in app["issues"]) else "WARNING"
yield dict(
meta={"test": "apps", "app": app["name"]},
status=level,
summary="diagnosis_apps_issue",
details=[issue[1] for issue in app["issues"]]
)
def issues(self, app):
# Check quality level in catalog
if not app.get("from_catalog") or app["from_catalog"].get("state") != "working":
yield ("error", "diagnosis_apps_not_in_app_catalog")
elif not isinstance(app["from_catalog"].get("level"), int) or app["from_catalog"]["level"] == 0:
yield ("error", "diagnosis_apps_broken")
elif app["from_catalog"]["level"] <= 4:
yield ("warning", "diagnosis_apps_bad_quality")
# Check for super old, deprecated practices
yunohost_version_req = app["manifest"].get("requirements", {}).get("yunohost", "").strip(">= ")
if yunohost_version_req.startswith("2."):
yield ("error", "diagnosis_apps_outdated_ynh_requirement")
deprecated_helpers = [
"yunohost app setting",
"yunohost app checkurl",
"yunohost app checkport",
"yunohost app initdb",
"yunohost tools port-available",
]
for deprecated_helper in deprecated_helpers:
if os.system(f"grep -nr -q '{deprecated_helper}' {app['setting_path']}/scripts/") == 0:
yield ("error", "diagnosis_apps_deprecated_practices")
old_arg_regex = r'^domain=\${?[0-9]'
if os.system(f"grep -q '{old_arg_regex}' {app['setting_path']}/scripts/install") == 0:
yield ("error", "diagnosis_apps_deprecated_practices")
def main(args, env, loggers):
return AppDiagnoser(args, env, loggers).diagnose()

View file

@ -15,7 +15,11 @@ mkdir -p "$MEDIA_DIRECTORY/$user/Video"
mkdir -p "$MEDIA_DIRECTORY/$user/eBook" mkdir -p "$MEDIA_DIRECTORY/$user/eBook"
ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share" ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share"
# Création du lien symbolique dans le home de l'utilisateur. # Création du lien symbolique dans le home de l'utilisateur.
ln -sfn "$MEDIA_DIRECTORY/$user" "/home/$user/Multimedia" #link will only be created if the home directory of the user exists and if it's located in '/home' folder
user_home="$(getent passwd $user | cut -d: -f6 | grep '^/home/')"
if [[ -d "$user_home" ]]; then
ln -sfn "$MEDIA_DIRECTORY/$user" "$user_home/Multimedia"
fi
# Propriétaires des dossiers utilisateurs. # Propriétaires des dossiers utilisateurs.
chown -R $user "$MEDIA_DIRECTORY/$user" chown -R $user "$MEDIA_DIRECTORY/$user"

View file

@ -1,68 +0,0 @@
# This file is part of avahi.
#
# avahi is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# avahi is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with avahi; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA.
# See avahi-daemon.conf(5) for more information on this configuration
# file!
[server]
host-name=yunohost
domain-name=local
#browse-domains=0pointer.de, zeroconf.org
use-ipv4=yes
use-ipv6=yes
#allow-interfaces=eth0
#deny-interfaces=eth1
#check-response-ttl=no
#use-iff-running=no
#enable-dbus=yes
#disallow-other-stacks=no
#allow-point-to-point=no
#cache-entries-max=4096
#clients-max=4096
#objects-per-client-max=1024
#entries-per-entry-group-max=32
ratelimit-interval-usec=1000000
ratelimit-burst=1000
[wide-area]
enable-wide-area=yes
[publish]
#disable-publishing=no
#disable-user-service-publishing=no
#add-service-cookie=no
#publish-addresses=yes
#publish-hinfo=yes
#publish-workstation=yes
#publish-domain=yes
#publish-dns-servers=192.168.50.1, 192.168.50.2
#publish-resolv-conf-dns-servers=yes
#publish-aaaa-on-ipv4=yes
#publish-a-on-ipv6=no
[reflector]
#enable-reflector=no
#reflect-ipv=no
[rlimits]
#rlimit-as=
rlimit-core=0
rlimit-data=4194304
rlimit-fsize=0
rlimit-nofile=768
rlimit-stack=4194304
rlimit-nproc=3

View file

@ -0,0 +1,13 @@
[Unit]
Description=YunoHost mDNS service
After=network.target
[Service]
User=mdns
Group=mdns
Type=simple
ExecStart=/usr/bin/yunomdns
StandardOutput=syslog
[Install]
WantedBy=default.target

View file

@ -30,7 +30,7 @@ skip-external-locking
key_buffer_size = 16K key_buffer_size = 16K
max_allowed_packet = 16M max_allowed_packet = 16M
table_open_cache = 4 table_open_cache = 4
sort_buffer_size = 64K sort_buffer_size = 256K
read_buffer_size = 256K read_buffer_size = 256K
read_rnd_buffer_size = 256K read_rnd_buffer_size = 256K
net_buffer_length = 2K net_buffer_length = 2K

View file

@ -25,7 +25,11 @@ ssl_dhparam /usr/share/yunohost/other/ffdhe2048.pem;
# Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners # Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners
# https://wiki.mozilla.org/Security/Guidelines/Web_Security # https://wiki.mozilla.org/Security/Guidelines/Web_Security
# https://observatory.mozilla.org/ # https://observatory.mozilla.org/
{% if experimental == "True" %}
more_set_headers "Content-Security-Policy : upgrade-insecure-requests; default-src https: data:";
{% else %}
more_set_headers "Content-Security-Policy : upgrade-insecure-requests"; more_set_headers "Content-Security-Policy : upgrade-insecure-requests";
{% endif %}
more_set_headers "Content-Security-Policy-Report-Only : default-src https: data: 'unsafe-inline' 'unsafe-eval' "; more_set_headers "Content-Security-Policy-Report-Only : default-src https: data: 'unsafe-inline' 'unsafe-eval' ";
more_set_headers "X-Content-Type-Options : nosniff"; more_set_headers "X-Content-Type-Options : nosniff";
more_set_headers "X-XSS-Protection : 1; mode=block"; more_set_headers "X-XSS-Protection : 1; mode=block";
@ -34,7 +38,13 @@ more_set_headers "X-Permitted-Cross-Domain-Policies : none";
more_set_headers "X-Frame-Options : SAMEORIGIN"; more_set_headers "X-Frame-Options : SAMEORIGIN";
# Disable the disaster privacy thing that is FLoC # Disable the disaster privacy thing that is FLoC
{% if experimental == "True" %}
more_set_headers "Permissions-Policy : fullscreen=(), geolocation=(), payment=(), accelerometer=(), battery=(), magnetometer=(), usb=(), interest-cohort=()";
# Force HTTPOnly and Secure for all cookies
proxy_cookie_path ~$ "; HTTPOnly; Secure;";
{% else %}
more_set_headers "Permissions-Policy : interest-cohort=()"; more_set_headers "Permissions-Policy : interest-cohort=()";
{% endif %}
# Disable gzip to protect against BREACH # Disable gzip to protect against BREACH
# Read https://trac.nginx.org/nginx/ticket/1720 (text/html cannot be disabled!) # Read https://trac.nginx.org/nginx/ticket/1720 (text/html cannot be disabled!)

View file

@ -0,0 +1,14 @@
[Unit]
Description=Mounts /proc with hidepid=2
DefaultDependencies=no
Before=sysinit.target
Requires=local-fs.target
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/bin/mount -o remount,nosuid,nodev,noexec,hidepid=2 /proc
RemainAfterExit=yes
[Install]
WantedBy=sysinit.target

View file

@ -1,4 +1,3 @@
avahi-daemon: {}
dnsmasq: dnsmasq:
test_conf: dnsmasq --test test_conf: dnsmasq --test
dovecot: dovecot:
@ -52,6 +51,8 @@ yunohost-firewall:
need_lock: true need_lock: true
test_status: iptables -S | grep "^-A INPUT" | grep " --dport" | grep -q ACCEPT test_status: iptables -S | grep "^-A INPUT" | grep " --dport" | grep -q ACCEPT
category: security category: security
yunomdns:
category: mdns
glances: null glances: null
nsswitch: null nsswitch: null
ssl: null ssl: null
@ -68,3 +69,4 @@ rmilter: null
php5-fpm: null php5-fpm: null
php7.0-fpm: null php7.0-fpm: null
nslcd: null nslcd: null
avahi-daemon: null

21
debian/changelog vendored
View file

@ -1,3 +1,24 @@
yunohost (4.2.8.1) stable; urgency=low
- [fix] Safer location for slapd backup during hdb/mdb migration (3c646b3d)
Thanks to all contributors <3 ! (ljf)
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 27 Aug 2021 01:32:16 +0200
yunohost (4.2.8) stable; urgency=low
- [fix] ynh_permission_has_user not behaving properly when checking if a group is allowed (f0590907)
- [enh] use yaml safeloader everywhere ([#1287](https://github.com/YunoHost/yunohost/pull/1287))
- [enh] Add --no-safety-backup option to "yunohost app upgrade" ([#1286](https://github.com/YunoHost/yunohost/pull/1286))
- [enh] Add --purge option to "yunohost app remove" ([#1285](https://github.com/YunoHost/yunohost/pull/1285))
- [enh] Multimedia helper: check that home folder exists ([#1255](https://github.com/YunoHost/yunohost/pull/1255))
- [i18n] Translations updated for French, Galician, German, Portuguese
Thanks to all contributors <3 ! (José M, Kay0u, Krakinou, ljf, Luca, mifegui, ppr, sagessylu)
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 19 Aug 2021 19:11:19 +0200
yunohost (4.2.7) stable; urgency=low yunohost (4.2.7) stable; urgency=low
Notable changes: Notable changes:

5
debian/control vendored
View file

@ -13,14 +13,15 @@ Depends: ${python3:Depends}, ${misc:Depends}
, moulinette (>= 4.2), ssowat (>= 4.0) , moulinette (>= 4.2), ssowat (>= 4.0)
, python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-psutil, python3-requests, python3-dnspython, python3-openssl
, python3-miniupnpc, python3-dbus, python3-jinja2 , python3-miniupnpc, python3-dbus, python3-jinja2
, python3-toml, python3-packaging, python3-publicsuffix , python3-toml, python3-packaging, python3-publicsuffix,
, python3-ldap, python3-zeroconf,
, apt, apt-transport-https, apt-utils, dirmngr , apt, apt-transport-https, apt-utils, dirmngr
, php7.3-common, php7.3-fpm, php7.3-ldap, php7.3-intl , php7.3-common, php7.3-fpm, php7.3-ldap, php7.3-intl
, mariadb-server, php7.3-mysql , mariadb-server, php7.3-mysql
, openssh-server, iptables, fail2ban, dnsutils, bind9utils , openssh-server, iptables, fail2ban, dnsutils, bind9utils
, openssl, ca-certificates, netcat-openbsd, iproute2 , openssl, ca-certificates, netcat-openbsd, iproute2
, slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd
, dnsmasq, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname , dnsmasq, resolvconf, libnss-myhostname
, postfix, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre , postfix, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre
, dovecot-core, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, dovecot-antispam , dovecot-core, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, dovecot-antispam
, rspamd, opendkim-tools, postsrsd, procmail, mailutils , rspamd, opendkim-tools, postsrsd, procmail, mailutils

1
debian/postinst vendored
View file

@ -18,6 +18,7 @@ do_configure() {
bash /usr/share/yunohost/hooks/conf_regen/46-nsswitch 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/06-slapd init
bash /usr/share/yunohost/hooks/conf_regen/15-nginx init bash /usr/share/yunohost/hooks/conf_regen/15-nginx init
bash /usr/share/yunohost/hooks/conf_regen/37-mdns init
fi fi
else else
echo "Regenerating configuration, this might take a while..." echo "Regenerating configuration, this might take a while..."

View file

@ -26,7 +26,7 @@ ACTIONSMAP_FILE = os.path.join(THIS_SCRIPT_DIR, "../data/actionsmap/yunohost.yml
def ordered_yaml_load(stream): def ordered_yaml_load(stream):
class OrderedLoader(yaml.Loader): class OrderedLoader(yaml.SafeLoader):
pass pass
OrderedLoader.add_constructor( OrderedLoader.add_constructor(

View file

@ -83,7 +83,6 @@
"yunohost_installing": "عملية تنصيب يونوهوست جارية …", "yunohost_installing": "عملية تنصيب يونوهوست جارية …",
"yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب أو هو مثبت حاليا بشكل خاطئ. قم بتنفيذ الأمر 'yunohost tools postinstall'", "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب أو هو مثبت حاليا بشكل خاطئ. قم بتنفيذ الأمر 'yunohost tools postinstall'",
"migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.", "migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.",
"service_description_avahi-daemon": "يسمح لك بالنفاذ إلى خادومك عبر الشبكة المحلية باستخدام yunohost.local",
"service_description_metronome": "يُدير حسابات الدردشة الفورية XMPP", "service_description_metronome": "يُدير حسابات الدردشة الفورية XMPP",
"service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك", "service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك",
"service_description_postfix": "يقوم بإرسال و تلقي الرسائل البريدية الإلكترونية", "service_description_postfix": "يقوم بإرسال و تلقي الرسائل البريدية الإلكترونية",

View file

@ -283,7 +283,6 @@
"service_already_started": "El servei «{service}» ja està funcionant", "service_already_started": "El servei «{service}» ja està funcionant",
"service_already_stopped": "Ja s'ha aturat el servei «{service}»", "service_already_stopped": "Ja s'ha aturat el servei «{service}»",
"service_cmd_exec_failed": "No s'ha pogut executar l'ordre «{command}»", "service_cmd_exec_failed": "No s'ha pogut executar l'ordre «{command}»",
"service_description_avahi-daemon": "Permet accedir al servidor via «yunohost.local» en la xarxa local",
"service_description_dnsmasq": "Gestiona la resolució del nom de domini (DNS)", "service_description_dnsmasq": "Gestiona la resolució del nom de domini (DNS)",
"service_description_dovecot": "Permet als clients de correu accedir/recuperar correus (via IMAP i POP3)", "service_description_dovecot": "Permet als clients de correu accedir/recuperar correus (via IMAP i POP3)",
"service_description_fail2ban": "Protegeix contra els atacs de força bruta i a altres atacs provinents d'Internet", "service_description_fail2ban": "Protegeix contra els atacs de força bruta i a altres atacs provinents d'Internet",

View file

@ -124,7 +124,7 @@
"upnp_dev_not_found": "Es konnten keine UPnP Geräte gefunden werden", "upnp_dev_not_found": "Es konnten keine UPnP Geräte gefunden werden",
"upnp_disabled": "UPnP deaktiviert", "upnp_disabled": "UPnP deaktiviert",
"upnp_enabled": "UPnP aktiviert", "upnp_enabled": "UPnP aktiviert",
"upnp_port_open_failed": "UPnP Port konnte nicht geöffnet werden.", "upnp_port_open_failed": "Port konnte nicht via UPnP geöffnet werden",
"user_created": "Benutzer erstellt", "user_created": "Benutzer erstellt",
"user_creation_failed": "Benutzer konnte nicht erstellt werden {user}: {error}", "user_creation_failed": "Benutzer konnte nicht erstellt werden {user}: {error}",
"user_deleted": "Benutzer gelöscht", "user_deleted": "Benutzer gelöscht",
@ -528,7 +528,7 @@
"migrations_no_such_migration": "Es existiert keine Migration genannt '{id}'", "migrations_no_such_migration": "Es existiert keine Migration genannt '{id}'",
"migrations_running_forward": "Durchführen der Migrationen {id}...", "migrations_running_forward": "Durchführen der Migrationen {id}...",
"migrations_skip_migration": "Überspringe 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_too_simple_2": "Das Passwort muss mindestens 8 Zeichen lang sein und Gross- sowie Kleinbuchstaben enthalten",
"password_listed": "Dieses Passwort zählt zu den meistgenutzten Passwörtern der Welt. Bitte wähle ein anderes, einzigartigeres Passwort.", "password_listed": "Dieses Passwort zählt zu den meistgenutzten Passwörtern der Welt. Bitte wähle ein anderes, einzigartigeres Passwort.",
"operation_interrupted": "Wurde die Operation manuell unterbrochen?", "operation_interrupted": "Wurde die Operation manuell unterbrochen?",
"invalid_number": "Muss eine Zahl sein", "invalid_number": "Muss eine Zahl sein",
@ -539,8 +539,8 @@
"permission_already_allowed": "Die Gruppe '{group}' hat die Berechtigung '{permission}' bereits erhalten", "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_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)", "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_4": "Das Passwort muss mindestens 12 Zeichen lang sein und Grossbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten",
"password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Zahlen, Klein- und Grossbuchstaben und Sonderzeichen enthalten", "password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Grossbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten",
"regenconf_file_manually_removed": "Die Konfigurationsdatei '{conf}' wurde manuell gelöscht und wird nicht erstellt", "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_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_kept_back": "Die Konfigurationsdatei '{conf}' sollte von \"regen-conf\" (Kategorie {category}) gelöscht werden, wurde aber beibehalten.",
@ -597,7 +597,6 @@
"service_description_fail2ban": "Schützt gegen Brute-Force-Angriffe und andere Angriffe aus dem Internet", "service_description_fail2ban": "Schützt gegen Brute-Force-Angriffe und andere Angriffe aus dem Internet",
"service_description_dovecot": "Ermöglicht es E-Mail-Clients auf Konten zuzugreifen (IMAP und POP3)", "service_description_dovecot": "Ermöglicht es E-Mail-Clients auf Konten zuzugreifen (IMAP und POP3)",
"service_description_dnsmasq": "Verarbeitet die Auflösung des Domainnamens (DNS)", "service_description_dnsmasq": "Verarbeitet die Auflösung des Domainnamens (DNS)",
"service_description_avahi-daemon": "Erlaubt, den Server im lokalen Netz über 'yunohost.local' zu erreichen",
"restore_backup_too_old": "Dieses Backup kann nicht wieder hergestellt werden, weil es von einer zu alten YunoHost Version stammt.", "restore_backup_too_old": "Dieses Backup kann nicht wieder hergestellt werden, weil es von einer zu alten YunoHost Version stammt.",
"service_description_slapd": "Speichert Benutzer, Domains und verbundene Informationen", "service_description_slapd": "Speichert Benutzer, Domains und verbundene Informationen",
"service_description_rspamd": "Spamfilter und andere E-Mail-Merkmale", "service_description_rspamd": "Spamfilter und andere E-Mail-Merkmale",
@ -631,5 +630,9 @@
"unknown_main_domain_path": "Unbekannte:r Domain oder Pfad für '{app}'. Du musst eine Domain und einen Pfad setzen, um die URL für Berechtigungen zu setzen.", "unknown_main_domain_path": "Unbekannte:r Domain oder Pfad für '{app}'. Du musst eine Domain und einen Pfad setzen, um die URL für Berechtigungen zu setzen.",
"yunohost_postinstall_end_tip": "Post-install ist fertig! Um das Setup abzuschliessen, wird empfohlen:\n - einen ersten Benutzer über den Bereich 'Benutzer*in' im Adminbereich hinzuzufügen (oder mit 'yunohost user create <username>' in der Kommandezeile);\n - mögliche Fehler zu diagnostizieren über den Bereich 'Diagnose' im Adminbereich (oder mit 'yunohost diagnosis run' in der Kommandozeile;\n - Die Abschnitte 'Install YunoHost' und 'Geführte Tour' im Administratorenhandbuch zu lesen: https://yunohost.org/admindoc.", "yunohost_postinstall_end_tip": "Post-install ist fertig! Um das Setup abzuschliessen, wird empfohlen:\n - einen ersten Benutzer über den Bereich 'Benutzer*in' im Adminbereich hinzuzufügen (oder mit 'yunohost user create <username>' in der Kommandezeile);\n - mögliche Fehler zu diagnostizieren über den Bereich 'Diagnose' im Adminbereich (oder mit 'yunohost diagnosis run' in der Kommandozeile;\n - Die Abschnitte 'Install YunoHost' und 'Geführte Tour' im Administratorenhandbuch zu lesen: https://yunohost.org/admindoc.",
"user_already_exists": "Der Benutzer '{user}' ist bereits vorhanden", "user_already_exists": "Der Benutzer '{user}' ist bereits vorhanden",
"update_apt_cache_warning": "Beim Versuch den Cache für APT (Debians Paketmanager) zu aktualisieren, ist etwas schief gelaufen. Hier ist ein Dump der Zeilen aus sources.list, die Ihnen vielleicht dabei helfen, das Problem zu identifizieren:\n{sourceslist}" "update_apt_cache_warning": "Beim Versuch den Cache für APT (Debians Paketmanager) zu aktualisieren, ist etwas schief gelaufen. Hier ist ein Dump der Zeilen aus sources.list, die Ihnen vielleicht dabei helfen, das Problem zu identifizieren:\n{sourceslist}",
"global_settings_setting_security_webadmin_allowlist": "IP-Adressen, die auf die Verwaltungsseite zugreifen dürfen. Kommasepariert.",
"global_settings_setting_security_webadmin_allowlist_enabled": "Erlaube nur bestimmten IP-Adressen den Zugriff auf die Verwaltungsseite.",
"disk_space_not_sufficient_update": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu aktuallisieren",
"disk_space_not_sufficient_install": "Es ist nicht genügend Speicherplatz frei, um diese Applikation zu installieren"
} }

View file

@ -32,7 +32,7 @@
"app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps}", "app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps}",
"app_manifest_invalid": "Something is wrong with the app manifest: {error}", "app_manifest_invalid": "Something is wrong with the app manifest: {error}",
"app_manifest_install_ask_domain": "Choose the domain where this app should be installed", "app_manifest_install_ask_domain": "Choose the domain where this app should be installed",
"app_manifest_install_ask_path": "Choose the path where this app should be installed", "app_manifest_install_ask_path": "Choose the URL path (after the domain) where this app should be installed",
"app_manifest_install_ask_password": "Choose an administration password for this app", "app_manifest_install_ask_password": "Choose an administration password for this app",
"app_manifest_install_ask_admin": "Choose an administrator user for this app", "app_manifest_install_ask_admin": "Choose an administrator user for this app",
"app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?", "app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?",
@ -184,6 +184,7 @@
"diagnosis_dns_discrepancy": "The following DNS record does not seem to follow the recommended configuration:<br>Type: <code>{type}</code><br>Name: <code>{name}</code><br>Current value: <code>{current}</code><br>Expected value: <code>{value}</code>", "diagnosis_dns_discrepancy": "The following DNS record does not seem to follow the recommended configuration:<br>Type: <code>{type}</code><br>Name: <code>{name}</code><br>Current value: <code>{current}</code><br>Expected value: <code>{value}</code>",
"diagnosis_dns_point_to_doc": "Please check the documentation at <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> if you need help about configuring DNS records.", "diagnosis_dns_point_to_doc": "Please check the documentation at <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> if you need help about configuring DNS records.",
"diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using <cmd>yunohost dyndns update --force</cmd>.", "diagnosis_dns_try_dyndns_update_force": "This domain's DNS configuration should automatically be managed by YunoHost. If that's not the case, you can try to force an update using <cmd>yunohost dyndns update --force</cmd>.",
"diagnosis_dns_specialusedomain": "Domain {domain} is based on a special-use top-level domain (TLD) and is therefore not expected to have actual DNS records.",
"diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains", "diagnosis_domain_expiration_not_found": "Unable to check the expiration date for some domains",
"diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!", "diagnosis_domain_not_found_details": "The domain {domain} doesn't exist in WHOIS database or is expired!",
"diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?", "diagnosis_domain_expiration_not_found_details": "The WHOIS information for domain {domain} doesn't seem to contain the information about the expiration date?",
@ -249,6 +250,14 @@
"diagnosis_description_web": "Web", "diagnosis_description_web": "Web",
"diagnosis_description_mail": "Email", "diagnosis_description_mail": "Email",
"diagnosis_description_regenconf": "System configurations", "diagnosis_description_regenconf": "System configurations",
"diagnosis_description_apps": "Applications",
"diagnosis_apps_allgood": "All installed apps respect basic packaging practices",
"diagnosis_apps_issue": "An issue was found for app {app}",
"diagnosis_apps_not_in_app_catalog": "This application is not in YunoHost's application catalog. If it was in the past and got removed, you should consider uninstalling this app as it won't receive upgrade, and may compromise the integrity and security of your system.",
"diagnosis_apps_broken": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.",
"diagnosis_apps_bad_quality": "This application is currently flagged as broken on YunoHost's application catalog. This may be a temporary issue while the maintainers attempt to fix the issue. In the meantime, upgrading this app is disabled.",
"diagnosis_apps_outdated_ynh_requirement": "This app's installed version only requires yunohost >= 2.x, which tends to indicate that it's not up to date with recommended packaging practices and helpers. You should really consider upgrading it.",
"diagnosis_apps_deprecated_practices": "This app's installed version still uses some super-old deprecated packaging practices. You should really consider upgrading it.",
"diagnosis_ports_could_not_diagnose": "Could not diagnose if ports are reachable from outside in IPv{ipversion}.", "diagnosis_ports_could_not_diagnose": "Could not diagnose if ports are reachable from outside in IPv{ipversion}.",
"diagnosis_ports_could_not_diagnose_details": "Error: {error}", "diagnosis_ports_could_not_diagnose_details": "Error: {error}",
"diagnosis_ports_unreachable": "Port {port} is not reachable from outside.", "diagnosis_ports_unreachable": "Port {port} is not reachable from outside.",
@ -260,6 +269,7 @@
"diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at <a href='https://yunohost.org/dns_local_network'>https://yunohost.org/dns_local_network</a>", "diagnosis_http_hairpinning_issue_details": "This is probably because of your ISP box / router. As a result, people from outside your local network will be able to access your server as expected, but not people from inside the local network (like you, probably?) when using the domain name or global IP. You may be able to improve the situation by having a look at <a href='https://yunohost.org/dns_local_network'>https://yunohost.org/dns_local_network</a>",
"diagnosis_http_could_not_diagnose": "Could not diagnose if domains are reachable from outside in IPv{ipversion}.", "diagnosis_http_could_not_diagnose": "Could not diagnose if domains are reachable from outside in IPv{ipversion}.",
"diagnosis_http_could_not_diagnose_details": "Error: {error}", "diagnosis_http_could_not_diagnose_details": "Error: {error}",
"diagnosis_http_localdomain": "Domain {domain}, with a .local TLD, is not expected to be reached from outside the local network.",
"diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.", "diagnosis_http_ok": "Domain {domain} is reachable through HTTP from outside the local network.",
"diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.<br>1. The most common cause for this issue is that port 80 (and 443) <a href='https://yunohost.org/isp_box_config'>are not correctly forwarded to your server</a>.<br>2. You should also make sure that the service nginx is running<br>3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.", "diagnosis_http_timeout": "Timed-out while trying to contact your server from outside. It appears to be unreachable.<br>1. The most common cause for this issue is that port 80 (and 443) <a href='https://yunohost.org/isp_box_config'>are not correctly forwarded to your server</a>.<br>2. You should also make sure that the service nginx is running<br>3. On more complex setups: make sure that no firewall or reverse-proxy is interfering.",
"diagnosis_http_connection_error": "Connection error: could not connect to the requested domain, it's very likely unreachable.", "diagnosis_http_connection_error": "Connection error: could not connect to the requested domain, it's very likely unreachable.",
@ -341,6 +351,7 @@
"global_settings_setting_smtp_relay_password": "SMTP relay host password", "global_settings_setting_smtp_relay_password": "SMTP relay host password",
"global_settings_setting_security_webadmin_allowlist_enabled": "Allow only some IPs to access the webadmin.", "global_settings_setting_security_webadmin_allowlist_enabled": "Allow only some IPs to access the webadmin.",
"global_settings_setting_security_webadmin_allowlist": "IP adresses allowed to access the webadmin. Comma-separated.", "global_settings_setting_security_webadmin_allowlist": "IP adresses allowed to access the webadmin. Comma-separated.",
"global_settings_setting_security_experimental_enabled": "Enable experimental security features (don't enable this if you don't know what you're doing!)",
"global_settings_setting_backup_compress_tar_archives": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", "global_settings_setting_backup_compress_tar_archives": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.",
"global_settings_unknown_type": "Unexpected situation, the setting {setting} appears to have the type {unknown_type} but it is not a type supported by the system.", "global_settings_unknown_type": "Unexpected situation, the setting {setting} appears to have the type {unknown_type} but it is not a type supported by the system.",
"good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).", "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).",
@ -370,6 +381,8 @@
"invalid_regex": "Invalid regex:'{regex}'", "invalid_regex": "Invalid regex:'{regex}'",
"ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it",
"iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it",
"ldap_server_down": "Unable to reach LDAP server",
"ldap_server_is_down_restart_it": "The LDAP service is down, attempt to restart it...",
"log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'", "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_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 show {name}{name}'", "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}{name}'",
@ -399,7 +412,7 @@
"log_letsencrypt_cert_install": "Install a Let's Encrypt certificate on '{}' domain", "log_letsencrypt_cert_install": "Install a Let's Encrypt certificate on '{}' domain",
"log_permission_create": "Create permission '{}'", "log_permission_create": "Create permission '{}'",
"log_permission_delete": "Delete permission '{}'", "log_permission_delete": "Delete permission '{}'",
"log_permission_url": "Update url related to permission '{}'", "log_permission_url": "Update URL related to permission '{}'",
"log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain", "log_selfsigned_cert_install": "Install self-signed certificate on '{}' domain",
"log_letsencrypt_cert_renew": "Renew '{}' Let's Encrypt certificate", "log_letsencrypt_cert_renew": "Renew '{}' Let's Encrypt certificate",
"log_regen_conf": "Regenerate system configurations '{}'", "log_regen_conf": "Regenerate system configurations '{}'",
@ -478,6 +491,7 @@
"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`.", "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}'", "not_enough_disk_space": "Not enough free space on '{path}'",
"invalid_number": "Must be a number", "invalid_number": "Must be a number",
"invalid_password": "Invalid password",
"operation_interrupted": "The operation was manually interrupted?", "operation_interrupted": "The operation was manually interrupted?",
"packages_upgrade_failed": "Could not upgrade all the packages", "packages_upgrade_failed": "Could not upgrade all the packages",
"password_listed": "This password is among the most used passwords in the world. Please choose something more unique.", "password_listed": "This password is among the most used passwords in the world. Please choose something more unique.",
@ -561,7 +575,7 @@
"service_already_started": "The service '{service}' is running already", "service_already_started": "The service '{service}' is running already",
"service_already_stopped": "The service '{service}' has already been stopped", "service_already_stopped": "The service '{service}' has already been stopped",
"service_cmd_exec_failed": "Could not execute the command '{command}'", "service_cmd_exec_failed": "Could not execute the command '{command}'",
"service_description_avahi-daemon": "Allows you to reach your server using 'yunohost.local' in your local network", "service_description_yunomdns": "Allows you to reach your server using 'yunohost.local' in your local network",
"service_description_dnsmasq": "Handles domain name resolution (DNS)", "service_description_dnsmasq": "Handles domain name resolution (DNS)",
"service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)", "service_description_dovecot": "Allows e-mail clients to access/fetch email (via IMAP and POP3)",
"service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet", "service_description_fail2ban": "Protects against brute-force and other kinds of attacks from the Internet",

View file

@ -332,7 +332,6 @@
"hook_exec_failed": "Ne povis funkcii skripto: {path}", "hook_exec_failed": "Ne povis funkcii skripto: {path}",
"global_settings_cant_open_settings": "Ne eblis malfermi agordojn, tial: {reason}", "global_settings_cant_open_settings": "Ne eblis malfermi agordojn, tial: {reason}",
"user_created": "Uzanto kreita", "user_created": "Uzanto kreita",
"service_description_avahi-daemon": "Permesas al vi atingi vian servilon uzante 'yunohost.local' en via loka reto",
"certmanager_attempt_to_replace_valid_cert": "Vi provas anstataŭigi bonan kaj validan atestilon por domajno {domain}! (Uzu --forte pretervidi)", "certmanager_attempt_to_replace_valid_cert": "Vi provas anstataŭigi bonan kaj validan atestilon por domajno {domain}! (Uzu --forte pretervidi)",
"regenconf_updated": "Agordo ĝisdatigita por '{category}'", "regenconf_updated": "Agordo ĝisdatigita por '{category}'",
"update_apt_cache_warning": "Io iris malbone dum la ĝisdatigo de la kaŝmemoro de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}", "update_apt_cache_warning": "Io iris malbone dum la ĝisdatigo de la kaŝmemoro de APT (paka administranto de Debian). Jen rubujo de la sources.list-linioj, kiuj povus helpi identigi problemajn liniojn:\n{sourceslist}",

View file

@ -238,7 +238,6 @@
"service_description_fail2ban": "Protege contra ataques de fuerza bruta y otras clases de ataques desde Internet", "service_description_fail2ban": "Protege contra ataques de fuerza bruta y otras clases de ataques desde Internet",
"service_description_dovecot": "Permite a los clientes de correo acceder/obtener correo (vía IMAP y POP3)", "service_description_dovecot": "Permite a los clientes de correo acceder/obtener correo (vía IMAP y POP3)",
"service_description_dnsmasq": "Maneja la resolución de nombres de dominio (DNS)", "service_description_dnsmasq": "Maneja la resolución de nombres de dominio (DNS)",
"service_description_avahi-daemon": "Permite acceder a su servidor usando «yunohost.local» en su red local",
"server_reboot_confirm": "El servidor se reiniciará inmediatamente ¿está seguro? [{answers}]", "server_reboot_confirm": "El servidor se reiniciará inmediatamente ¿está seguro? [{answers}]",
"server_reboot": "El servidor se reiniciará", "server_reboot": "El servidor se reiniciará",
"server_shutdown_confirm": "El servidor se apagará inmediatamente ¿está seguro? [{answers}]", "server_shutdown_confirm": "El servidor se apagará inmediatamente ¿está seguro? [{answers}]",

View file

@ -156,7 +156,7 @@
"certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain} nest pas émis par Lets Encrypt. Impossible de le renouveler automatiquement !", "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain} nest pas émis par Lets Encrypt. Impossible de le renouveler automatiquement !",
"certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain} nest pas sur le point dexpirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)", "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain} nest pas sur le point dexpirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)",
"certmanager_domain_http_not_working": "Le domaine {domain} ne semble pas être accessible via HTTP. Merci de vérifier la catégorie 'Web' dans le diagnostic pour plus d'informations. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)", "certmanager_domain_http_not_working": "Le domaine {domain} ne semble pas être accessible via HTTP. Merci de vérifier la catégorie 'Web' dans le diagnostic pour plus d'informations. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)",
"certmanager_domain_dns_ip_differs_from_public_ip": "L'enregistrement DNS du domaine '{domain}' est différent de ladresse IP de ce serveur. Pour plus d'informations, veuillez consulter la catégorie \"Enregistrements DNS\" dans la section diagnostic. Si vous avez récemment modifié votre enregistrement A, veuillez attendre sa propagation (des vérificateurs de propagation DNS sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver ces contrôles)", "certmanager_domain_dns_ip_differs_from_public_ip": "Les enregistrements DNS du domaine '{domain}' sont différents de ladresse IP de ce serveur. Pour plus d'informations, veuillez consulter la catégorie \"Enregistrements DNS\" dans la section diagnostic. Si vous avez récemment modifié votre enregistrement A, veuillez attendre sa propagation (des vérificateurs de propagation DNS sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver ces contrôles)",
"certmanager_cannot_read_cert": "Quelque chose sest mal passé lors de la tentative douverture du certificat actuel pour le domaine {domain} (fichier : {file}), la cause est : {reason}", "certmanager_cannot_read_cert": "Quelque chose sest mal passé lors de la tentative douverture du certificat actuel pour le domaine {domain} (fichier : {file}), la cause est : {reason}",
"certmanager_cert_install_success_selfsigned": "Le certificat auto-signé est maintenant installé pour le domaine « {domain} »", "certmanager_cert_install_success_selfsigned": "Le certificat auto-signé est maintenant installé pour le domaine « {domain} »",
"certmanager_cert_install_success": "Le certificat Lets Encrypt est maintenant installé pour le domaine « {domain} »", "certmanager_cert_install_success": "Le certificat Lets Encrypt est maintenant installé pour le domaine « {domain} »",
@ -217,10 +217,10 @@
"backup_couldnt_bind": "Impossible de lier {src} avec {dest}.", "backup_couldnt_bind": "Impossible de lier {src} avec {dest}.",
"domain_dns_conf_is_just_a_recommendation": "Cette commande vous montre la configuration *recommandée*. Elle ne configure pas le DNS pour vous. Il est de votre ressort de configurer votre zone DNS chez votre registrar/fournisseur conformément à cette recommandation.", "domain_dns_conf_is_just_a_recommendation": "Cette commande vous montre la configuration *recommandée*. Elle ne configure pas le DNS pour vous. Il est de votre ressort de configurer votre zone DNS chez votre registrar/fournisseur conformément à cette recommandation.",
"migrations_cant_reach_migration_file": "Impossible daccéder aux fichiers de migration via le chemin '%s'", "migrations_cant_reach_migration_file": "Impossible daccéder aux fichiers de migration via le chemin '%s'",
"migrations_loading_migration": "Chargement de la migration {id}...", "migrations_loading_migration": "Chargement de la migration {id} ...",
"migrations_migration_has_failed": "La migration {id} a échoué avec lexception {exception} : annulation", "migrations_migration_has_failed": "La migration {id} a échoué avec lexception {exception} : annulation",
"migrations_no_migrations_to_run": "Aucune migration à lancer", "migrations_no_migrations_to_run": "Aucune migration à lancer",
"migrations_skip_migration": "Ignorer et passer la migration {id}...", "migrations_skip_migration": "Ignorer et passer la migration {id} ...",
"server_shutdown": "Le serveur va séteindre", "server_shutdown": "Le serveur va séteindre",
"server_shutdown_confirm": "Le serveur va être éteint immédiatement, le voulez-vous vraiment ? [{answers}]", "server_shutdown_confirm": "Le serveur va être éteint immédiatement, le voulez-vous vraiment ? [{answers}]",
"server_reboot": "Le serveur va redémarrer", "server_reboot": "Le serveur va redémarrer",
@ -234,7 +234,7 @@
"migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", "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 run`.", "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.", "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_yunomdns": "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)", "service_description_dnsmasq": "Gère la résolution des noms de domaine (DNS)",
"service_description_dovecot": "Permet aux clients de messagerie daccéder/récupérer les courriels (via IMAP et POP3)", "service_description_dovecot": "Permet aux clients de messagerie daccéder/récupérer les courriels (via IMAP et POP3)",
"service_description_fail2ban": "Protège contre les attaques brute-force et autres types dattaques venant dInternet", "service_description_fail2ban": "Protège contre les attaques brute-force et autres types dattaques venant dInternet",
@ -285,7 +285,7 @@
"mail_unavailable": "Cette adresse de courriel est réservée et doit être automatiquement attribuée au tout premier utilisateur", "mail_unavailable": "Cette adresse de courriel est réservée et doit être automatiquement attribuée au tout premier utilisateur",
"good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe d'administration. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou d'utiliser une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "good_practices_about_admin_password": "Vous êtes sur le point de définir un nouveau mot de passe d'administration. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou d'utiliser une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).",
"good_practices_about_user_password": "Vous êtes sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "good_practices_about_user_password": "Vous êtes sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères, bien qu'il soit recommandé d'utiliser un mot de passe plus long (c'est-à-dire une phrase secrète) et/ou une combinaison de caractères (majuscules, minuscules, chiffres et caractères spéciaux).",
"password_listed": "Ce mot de passe fait partie des mots de passe les plus utilisés dans le monde. Veuillez en choisir un autre moins commun et plus fort.", "password_listed": "Ce mot de passe fait partie des mots de passe les plus utilisés dans le monde. Veuillez en choisir un autre moins commun et plus robuste.",
"password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères", "password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères",
"password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules et des minuscules", "password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules et des minuscules",
"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_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",
@ -342,7 +342,7 @@
"regenconf_would_be_updated": "La configuration aurait dû être mise à jour pour la catégorie '{category}'", "regenconf_would_be_updated": "La configuration aurait dû être mise à jour pour la catégorie '{category}'",
"regenconf_dry_pending_applying": "Vérification de la configuration en attente qui aurait été appliquée pour la catégorie '{category}'…", "regenconf_dry_pending_applying": "Vérification de la configuration en attente qui aurait été appliquée pour la catégorie '{category}'…",
"regenconf_failed": "Impossible de régénérer la configuration pour la ou les catégorie(s) : '{categories}'", "regenconf_failed": "Impossible de régénérer la configuration pour la ou les catégorie(s) : '{categories}'",
"regenconf_pending_applying": "Applique la configuration en attente pour la catégorie '{category}'...", "regenconf_pending_applying": "Applique la configuration en attente pour la catégorie '{category}' ...",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' est obsolète ! Veuillez plutôt utiliser 'yunohost tools regen-conf' à la place.", "service_regen_conf_is_deprecated": "'yunohost service regen-conf' est obsolète ! Veuillez plutôt utiliser 'yunohost tools regen-conf' à la place.",
"tools_upgrade_at_least_one": "Veuillez spécifier '--apps' ou '--system'", "tools_upgrade_at_least_one": "Veuillez spécifier '--apps' ou '--system'",
"tools_upgrade_cant_both": "Impossible de mettre à niveau le système et les applications en même temps", "tools_upgrade_cant_both": "Impossible de mettre à niveau le système et les applications en même temps",
@ -381,7 +381,7 @@
"migrations_already_ran": "Ces migrations sont déjà effectuées : {ids}", "migrations_already_ran": "Ces migrations sont déjà effectuées : {ids}",
"migrations_dependencies_not_satisfied": "Exécutez ces migrations : '{dependencies_id}', avant migration {id}.", "migrations_dependencies_not_satisfied": "Exécutez ces migrations : '{dependencies_id}', avant migration {id}.",
"migrations_failed_to_load_migration": "Impossible de charger la migration {id} : {error}", "migrations_failed_to_load_migration": "Impossible de charger la migration {id} : {error}",
"migrations_running_forward": "Exécution de la migration {id}...", "migrations_running_forward": "Exécution de la migration {id} ...",
"migrations_success_forward": "Migration {id} terminée", "migrations_success_forward": "Migration {id} terminée",
"operation_interrupted": "L'opération a-t-elle été interrompue manuellement ?", "operation_interrupted": "L'opération a-t-elle été interrompue manuellement ?",
"permission_already_exist": "Lautorisation '{permission}' existe déjà", "permission_already_exist": "Lautorisation '{permission}' existe déjà",
@ -554,24 +554,24 @@
"diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement lespérance de vie du périphérique.", "diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement lespérance de vie du périphérique.",
"restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}", "restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}",
"regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.", "regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.",
"migration_0015_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus nécessaires...", "migration_0015_cleaning_up": "Nettoyage du cache et des paquets qui ne sont plus nécessaires ...",
"migration_0015_specific_upgrade": "Démarrage de la mise à jour des paquets du système qui doivent être mis à jour séparément...", "migration_0015_specific_upgrade": "Démarrage de la mise à jour des paquets du système qui doivent être mis à jour séparément ...",
"migration_0015_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés à la suite de la mise à niveau : {manually_modified_files}", "migration_0015_modified_files": "Veuillez noter que les fichiers suivants ont été modifiés manuellement et pourraient être écrasés à la suite de la mise à niveau : {manually_modified_files}",
"migration_0015_problematic_apps_warning": "Veuillez noter que des applications qui peuvent poser problèmes ont été détectées. Il semble qu'elles n'aient pas été installées à partir du catalogue d'applications YunoHost, ou bien qu'elles ne soient pas signalées comme \"fonctionnelles\". Par conséquent, il n'est pas possible de garantir que les applications suivantes fonctionneront encore après la mise à niveau : {problematic_apps}", "migration_0015_problematic_apps_warning": "Veuillez noter que des applications qui peuvent poser problèmes ont été détectées. Il semble qu'elles n'aient pas été installées à partir du catalogue d'applications YunoHost, ou bien qu'elles ne soient pas signalées comme \"fonctionnelles\". Par conséquent, il n'est pas possible de garantir que les applications suivantes fonctionneront encore après la mise à niveau : {problematic_apps}",
"migration_0015_general_warning": "Veuillez noter que cette migration est une opération délicate. L'équipe YunoHost a fait de son mieux pour la revérifier et la tester, mais la migration pourrait quand même casser des éléments du système ou de ses applications.\n\nIl est donc recommandé :\n…- de faire une sauvegarde de toute donnée ou application critique. Plus d'informations ici https://yunohost.org/backup ;\n…- d'être patient après le lancement de la migration. Selon votre connexion internet et votre matériel, la mise à niveau peut prendre jusqu'à quelques heures.", "migration_0015_general_warning": "Veuillez noter que cette migration est une opération délicate. L'équipe YunoHost a fait de son mieux pour la revérifier et la tester, mais la migration pourrait quand même casser des éléments du système ou de ses applications.\n\nIl est donc recommandé :\n…- de faire une sauvegarde de toute donnée ou application critique. Plus d'informations ici https://yunohost.org/backup ;\n…- d'être patient après le lancement de la migration. Selon votre connexion internet et votre matériel, la mise à niveau peut prendre jusqu'à quelques heures.",
"migration_0015_system_not_fully_up_to_date": "Votre système n'est pas entièrement à jour. Veuillez effectuer une mise à jour normale avant de lancer la migration vers Buster.", "migration_0015_system_not_fully_up_to_date": "Votre système n'est pas entièrement à jour. Veuillez effectuer une mise à jour normale avant de lancer la migration vers Buster.",
"migration_0015_not_enough_free_space": "L'espace libre est très faible dans /var/ ! Vous devriez avoir au moins 1 Go de libre pour effectuer cette migration.", "migration_0015_not_enough_free_space": "L'espace libre est très faible dans /var/ ! Vous devriez avoir au moins 1 Go de libre pour effectuer cette migration.",
"migration_0015_not_stretch": "La distribution Debian actuelle n'est pas Stretch !", "migration_0015_not_stretch": "La distribution Debian actuelle n'est pas Stretch !",
"migration_0015_yunohost_upgrade": "Démarrage de la mise à jour de YunoHost...", "migration_0015_yunohost_upgrade": "Démarrage de la mise à jour de YunoHost ...",
"migration_0015_still_on_stretch_after_main_upgrade": "Quelque chose s'est mal passé lors de la mise à niveau, le système semble toujours être sous Debian Stretch", "migration_0015_still_on_stretch_after_main_upgrade": "Quelque chose s'est mal passé lors de la mise à niveau, le système semble toujours être sous Debian Stretch",
"migration_0015_main_upgrade": "Démarrage de la mise à niveau générale...", "migration_0015_main_upgrade": "Démarrage de la mise à niveau générale ...",
"migration_0015_patching_sources_list": "Mise à jour du fichier sources.lists...", "migration_0015_patching_sources_list": "Mise à jour du fichier sources.lists ...",
"migration_0015_start": "Démarrage de la migration vers Buster", "migration_0015_start": "Démarrage de la migration vers Buster",
"migration_description_0015_migrate_to_buster": "Mise à niveau du système vers Debian Buster et YunoHost 4.x", "migration_description_0015_migrate_to_buster": "Mise à niveau du système vers Debian Buster et YunoHost 4.x",
"diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant <cmd>yunohost dyndns update --force</cmd>.", "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par YunoHost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant <cmd>yunohost dyndns update --force</cmd>.",
"app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.", "app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.",
"migration_0015_weak_certs": "Il a été constaté que les certificats suivants utilisent encore des algorithmes de signature peu robustes et doivent être mis à jour pour être compatibles avec la prochaine version de NGINX : {certs}", "migration_0015_weak_certs": "Il a été constaté que les certificats suivants utilisent encore des algorithmes de signature peu robustes et doivent être mis à jour pour être compatibles avec la prochaine version de NGINX : {certs}",
"global_settings_setting_backup_compress_tar_archives": "Compresser les archives (.tar.gz) au lieu de créer des archives non-compressées lors de la création des backups. N.B. Activer cette option permet de créer des archives plus légères, mais aussi d'avoir des procédures de backup significativement plus longues et plus gourmandes en CPU.", "global_settings_setting_backup_compress_tar_archives": "Compresser les archives (.tar.gz) au lieu de créer des archives non-compressées lors de la création des sauvegardes. N.B. Activer cette option permet de créer des archives plus légères, mais aussi d'avoir des procédures de sauvegarde significativement plus longues et plus gourmandes 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", "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", "service_description_php7.3-fpm": "Exécute les applications écrites en PHP avec NGINX",
"migration_0018_failed_to_reset_legacy_rules": "La réinitialisation des règles iptable par défaut a échoué : {error}", "migration_0018_failed_to_reset_legacy_rules": "La réinitialisation des règles iptable par défaut a échoué : {error}",
@ -612,7 +612,7 @@
"additional_urls_already_removed": "URL supplémentaire '{url}' déjà supprimées pour la permission '{permission}'", "additional_urls_already_removed": "URL supplémentaire '{url}' déjà supprimées pour la permission '{permission}'",
"invalid_number": "Doit être un nombre", "invalid_number": "Doit être un nombre",
"migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives", "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_basesystem_hardware_model": "Le modèle/architecture du serveur est {model}",
"diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système.", "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système.",
"postinstall_low_rootfsspace": "Le système de fichiers a une taille totale inférieure à 10 Go, ce qui est préoccupant et devrait attirer votre attention ! Vous allez certainement arriver à court d'espace disque (très) rapidement ! Il est recommandé d'avoir au moins 16 Go à la racine pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez la post-installation avec --force-diskspace", "postinstall_low_rootfsspace": "Le système de fichiers a une taille totale inférieure à 10 Go, ce qui est préoccupant et devrait attirer votre attention ! Vous allez certainement arriver à court d'espace disque (très) rapidement ! Il est recommandé d'avoir au moins 16 Go à la racine pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez la post-installation 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}]", "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}]",
@ -633,5 +633,9 @@
"diagnosis_sshd_config_inconsistent_details": "Veuillez exécuter <cmd>yunohost settings set security.ssh.port -v VOTRE_PORT_SSH</cmd> pour définir le port SSH, et vérifiez <cmd>yunohost tools regen-conf ssh --dry-run --with-diff</cmd> et <cmd>yunohost tools regen-conf ssh --force</cmd> pour réinitialiser votre configuration aux recommandations YunoHost.", "diagnosis_sshd_config_inconsistent_details": "Veuillez exécuter <cmd>yunohost settings set security.ssh.port -v VOTRE_PORT_SSH</cmd> pour définir le port SSH, et vérifiez <cmd>yunohost tools regen-conf ssh --dry-run --with-diff</cmd> et <cmd>yunohost tools regen-conf ssh --force</cmd> pour réinitialiser votre configuration aux recommandations YunoHost.",
"diagnosis_sshd_config_inconsistent": "Il semble que le port SSH a été modifié manuellement dans /etc/ssh/sshd_config. Depuis YunoHost 4.2, un nouveau paramètre global 'security.ssh.port' est disponible pour éviter de modifier manuellement la configuration.", "diagnosis_sshd_config_inconsistent": "Il semble que le port SSH a été modifié manuellement dans /etc/ssh/sshd_config. Depuis YunoHost 4.2, un nouveau paramètre global 'security.ssh.port' est disponible pour éviter de modifier manuellement la configuration.",
"diagnosis_sshd_config_insecure": "La configuration SSH semble avoir été modifiée manuellement et n'est pas sécurisée car elle ne contient aucune directive 'AllowGroups' ou 'AllowUsers' pour limiter l'accès aux utilisateurs autorisés.", "diagnosis_sshd_config_insecure": "La configuration SSH semble avoir été modifiée manuellement et n'est pas sécurisée car elle ne contient aucune directive 'AllowGroups' ou 'AllowUsers' pour limiter l'accès aux utilisateurs autorisés.",
"backup_create_size_estimation": "L'archive contiendra environ {size} de données." "backup_create_size_estimation": "L'archive contiendra environ {size} de données.",
"global_settings_setting_security_webadmin_allowlist": "Adresses IP autorisées à accéder à la page web du portail d'administration (webadmin). Elles doivent être séparées par une virgule.",
"global_settings_setting_security_webadmin_allowlist_enabled": "Autorisez seulement certaines IP à accéder à la page web du portail d'administration (webadmin).",
"diagnosis_http_localdomain": "Le domaine {domain}, avec un TLD .local, ne devrait pas être atteint depuis l'extérieur du réseau local.",
"diagnosis_dns_specialusedomain": "Le domaine {domain} est basé sur un domaine de premier niveau (TLD) à usage spécial et ne devrait donc pas avoir d'enregistrements DNS réels."
} }

View file

@ -67,7 +67,7 @@
"app_remove_after_failed_install": "Eliminando a app debido ao fallo na instalación...", "app_remove_after_failed_install": "Eliminando a app debido ao fallo na instalación...",
"app_requirements_unmeet": "Non se cumpren os requerimentos de {app}, o paquete {pkgname} ({version}) debe ser {spec}", "app_requirements_unmeet": "Non se cumpren os requerimentos de {app}, o paquete {pkgname} ({version}) debe ser {spec}",
"app_requirements_checking": "Comprobando os paquetes requeridos por {app}...", "app_requirements_checking": "Comprobando os paquetes requeridos por {app}...",
"app_removed": "{app} eliminada", "app_removed": "{app} desinstalada",
"app_not_properly_removed": "{app} non se eliminou de xeito correcto", "app_not_properly_removed": "{app} non se eliminou de xeito correcto",
"app_not_installed": "Non se puido atopar {app} na lista de apps instaladas: {all_apps}", "app_not_installed": "Non se puido atopar {app} na lista de apps instaladas: {all_apps}",
"app_not_correctly_installed": "{app} semella que non está instalada correctamente", "app_not_correctly_installed": "{app} semella que non está instalada correctamente",
@ -345,5 +345,182 @@
"global_settings_setting_smtp_relay_password": "Contrasinal no repetidor SMTP", "global_settings_setting_smtp_relay_password": "Contrasinal no repetidor SMTP",
"global_settings_setting_smtp_relay_user": "Conta de usuaria no repetidor SMTP", "global_settings_setting_smtp_relay_user": "Conta de usuaria no repetidor SMTP",
"global_settings_setting_smtp_relay_port": "Porto do repetidor SMTP", "global_settings_setting_smtp_relay_port": "Porto do repetidor SMTP",
"global_settings_setting_smtp_relay_host": "Servidor repetidor SMTP para enviar emails no lugar da túa instancia yunohost. É útil se estás nunha destas situacións: o teu porto 25 está bloqueado polo teu provedor ISP u VPN, se tes unha IP residencial nunha lista DUHL, se non podes configurar DNS inversa ou se este servidor non ten conexión directa a internet e queres utilizar outro para enviar os emails." "global_settings_setting_smtp_relay_host": "Servidor repetidor SMTP para enviar emails no lugar da túa instancia yunohost. É útil se estás nunha destas situacións: o teu porto 25 está bloqueado polo teu provedor ISP u VPN, se tes unha IP residencial nunha lista DUHL, se non podes configurar DNS inversa ou se este servidor non ten conexión directa a internet e queres utilizar outro para enviar os emails.",
} "group_updated": "Grupo '{group}' actualizado",
"group_unknown": "Grupo descoñecido '{group}'",
"group_deletion_failed": "Non se eliminou o grupo '{group}': {error}",
"group_deleted": "Grupo '{group}' eliminado",
"group_cannot_be_deleted": "O grupo {group} non se pode eliminar manualmente.",
"group_cannot_edit_primary_group": "O grupo '{group}' non se pode editar manualmente. É o grupo primario que contén só a unha usuaria concreta.",
"group_cannot_edit_visitors": "O grupo 'visitors' non se pode editar manualmente. É un grupo especial que representa a tódas visitantes anónimas",
"group_cannot_edit_all_users": "O grupo 'all_users' non se pode editar manualmente. É un grupo especial que contén tódalas usuarias rexistradas en YunoHost",
"global_settings_setting_security_webadmin_allowlist": "Enderezos IP con permiso para acceder á webadmin. Separados por vírgulas.",
"global_settings_setting_security_webadmin_allowlist_enabled": "Permitir que só algúns IPs accedan á webadmin.",
"disk_space_not_sufficient_update": "Non hai espazo suficiente no disco para actualizar esta aplicación",
"disk_space_not_sufficient_install": "Non queda espazo suficiente no disco para instalar esta aplicación",
"log_help_to_get_log": "Para ver o rexistro completo da operación '{desc}', usa o comando 'yunohost log show {name}{name}'",
"log_link_to_log": "Rexistro completo desta operación: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'",
"log_corrupted_md_file": "O ficheiro YAML con metadatos asociado aos rexistros está danado: '{md_file}\nErro: {error}'",
"iptables_unavailable": "Non podes andar remexendo en iptables aquí. Ou ben estás nun contedor ou o teu kernel non ten soporte para isto",
"ip6tables_unavailable": "Non podes remexer en ip6tables aquí. Ou ben estás nun contedor ou o teu kernel non ten soporte para isto",
"invalid_regex": "Regex non válido: '{regex}'",
"installation_complete": "Instalación completa",
"hook_name_unknown": "Nome descoñecido do gancho '{name}'",
"hook_list_by_invalid": "Esta propiedade non se pode usar para enumerar os ganchos",
"hook_json_return_error": "Non se puido ler a info de retorno do gancho {path}. Erro: {msg}. Contido en bruto: {raw_content}",
"hook_exec_not_terminated": "O script non rematou correctamente: {path}",
"hook_exec_failed": "Non se executou o script: {path}",
"group_user_not_in_group": "A usuaria {user} non está no grupo {group}",
"group_user_already_in_group": "A usuaria {user} xa está no grupo {group}",
"group_update_failed": "Non se actualizou o grupo '{group}': {error}",
"log_permission_delete": "Eliminar permiso '{}'",
"log_permission_create": "Crear permiso '{}'",
"log_letsencrypt_cert_install": "Instalar un certificado Let's Encrypt para o dominio '{}'",
"log_dyndns_update": "Actualizar o IP asociado ao teu subdominio YunoHost '{}'",
"log_dyndns_subscribe": "Subscribirse a un subdominio YunoHost '{}'",
"log_domain_remove": "Eliminar o dominio '{}' da configuración do sistema",
"log_domain_add": "Engadir dominio '{}' á configuración do sistema",
"log_remove_on_failed_install": "Eliminar '{}' tras unha instalación fallida",
"log_remove_on_failed_restore": "Eliminar '{}' tras un intento fallido de restablecemento desde copia",
"log_backup_restore_app": "Restablecer '{}' desde unha copia de apoio",
"log_backup_restore_system": "Restablecer o sistema desde unha copia de apoio",
"log_backup_create": "Crear copia de apoio",
"log_available_on_yunopaste": "Este rexistro está dispoñible en {url}",
"log_app_config_apply": "Aplicar a configuración da app '{}'",
"log_app_config_show_panel": "Mostrar o panel de configuración da app '{}'",
"log_app_action_run": "Executar acción da app '{}'",
"log_app_makedefault": "Converter '{}' na app por defecto",
"log_app_upgrade": "Actualizar a app '{}'",
"log_app_remove": "Eliminar a app '{}'",
"log_app_install": "Instalar a app '{}'",
"log_app_change_url": "Cambiar o URL da app '{}'",
"log_operation_unit_unclosed_properly": "Non se pechou correctamente a unidade da operación",
"log_does_exists": "Non hai rexistro de operación co nome '{log}', usa 'yunohost log list' para ver tódolos rexistros de operacións dispoñibles",
"log_help_to_get_failed_log": "A operación '{desc}' non se completou. Comparte o rexistro completo da operación utilizando o comando 'yunohost log share {name}' para obter axuda",
"log_link_to_failed_log": "Non se completou a operación '{desc}'. Por favor envía o rexistro completo desta operación <a href=\"#/tools/logs/{name}\">premendo aquí</a> para obter axuda",
"migration_0015_start": "Comezando a migración a Buster",
"migration_update_LDAP_schema": "Actualizando esquema LDAP...",
"migration_ldap_rollback_success": "Sistema restablecido.",
"migration_ldap_migration_failed_trying_to_rollback": "Non se puido migrar... intentando volver á versión anterior do sistema.",
"migration_ldap_can_not_backup_before_migration": "O sistema de copia de apoio do sistema non se completou antes de que fallase a migración. Erro: {error}",
"migration_ldap_backup_before_migration": "Crear copia de apoio da base de datos LDAP e axustes de apps antes de realizar a migración.",
"migration_description_0020_ssh_sftp_permissions": "Engadir soporte para permisos SSH e SFTP",
"migration_description_0019_extend_permissions_features": "Extender/recrear o sistema de xestión de permisos de apps",
"migration_description_0018_xtable_to_nftable": "Migrar as regras de tráfico de rede antigas ao novo sistema nftable",
"migration_description_0017_postgresql_9p6_to_11": "Migrar bases de datos desde PostgreSQL 9.6 a 11",
"migration_description_0016_php70_to_php73_pools": "Migrar o ficheiros de configuración 'pool' de php7.0-fpm a php7.3",
"migration_description_0015_migrate_to_buster": "Actualizar o sistema a Debian Buster e YunoHost 4.x",
"migrating_legacy_permission_settings": "Migrando os axustes dos permisos anteriores...",
"main_domain_changed": "Foi cambiado o dominio principal",
"main_domain_change_failed": "Non se pode cambiar o dominio principal",
"mail_unavailable": "Este enderezo de email está reservado e debería adxudicarse automáticamente á primeira usuaria",
"mailbox_used_space_dovecot_down": "O servizo de caixa de correo Dovecot ten que estar activo se queres obter o espazo utilizado polo correo",
"mailbox_disabled": "Desactivado email para usuaria {user}",
"mail_forward_remove_failed": "Non se eliminou o reenvío de email '{mail}'",
"mail_domain_unknown": "Enderezo de email non válido para o dominio '{domain}'. Usa un dominio administrado por este servidor.",
"mail_alias_remove_failed": "Non se puido eliminar o alias de email '{mail}'",
"log_tools_reboot": "Reiniciar o servidor",
"log_tools_shutdown": "Apagar o servidor",
"log_tools_upgrade": "Actualizar paquetes do sistema",
"log_tools_postinstall": "Postinstalación do servidor YunoHost",
"log_tools_migrations_migrate_forward": "Executar migracións",
"log_domain_main_domain": "Facer que '{}' sexa o dominio principal",
"log_user_permission_reset": "Restablecer permiso '{}'",
"log_user_permission_update": "Actualizar accesos para permiso '{}'",
"log_user_update": "Actualizar info da usuaria '{}'",
"log_user_group_update": "Actualizar grupo '{}'",
"log_user_group_delete": "Eliminar grupo '{}'",
"log_user_group_create": "Crear grupo '{}'",
"log_user_delete": "Eliminar usuaria '{}'",
"log_user_create": "Engadir usuaria '{}'",
"log_regen_conf": "Rexerar configuración do sistema '{}'",
"log_letsencrypt_cert_renew": "Anovar certificado Let's Encrypt para '{}'",
"log_selfsigned_cert_install": "Instalar certificado auto-asinado para o dominio '{}'",
"log_permission_url": "Actualizar url relativo ao permiso '{}'",
"migration_0015_general_warning": "Ten en conta que a migración é unha operación delicada. O equipo YunoHost esforzouse revisando e comprobandoa, aínda así algo podería fallar en partes do teu sistema ou as súas apps.\n\nPor tanto, é recomendable:\n- realiza unha copia de apoio de tódolos datos ou apps importantes. Máis info en https://yunohost.org/backup;\n - ten paciencia tras iniciar o proceso: dependendo da túa conexión de internet e hardware podería demorar varias horas a actualización de tódolos compoñentes.",
"migration_0015_system_not_fully_up_to_date": "O teu sistema non está ao día. Realiza unha actualización común antes de realizar a migración a Buster.",
"migration_0015_not_enough_free_space": "Queda moi pouco espazo en /var/! Deberías ter polo menos 1GB libre para realizar a migración.",
"migration_0015_not_stretch": "A distribución Debian actual non é Stretch!",
"migration_0015_yunohost_upgrade": "Iniciando a actualización do núcleo YunoHost...",
"migration_0015_still_on_stretch_after_main_upgrade": "Algo foi mal durante a actualiza ión principal, o sistema semella que aínda está en Debian Stretch",
"migration_0015_main_upgrade": "Iniciando a actualización principal...",
"migration_0015_patching_sources_list": "Correxindo os sources.lists...",
"migrations_already_ran": "Xa se realizaron estas migracións: {ids}",
"migration_0019_slapd_config_will_be_overwritten": "Semella que editaches manualmente a configuración slapd. Para esta migración crítica YunoHost precisa forzar a actualización da configuración slapd. Os ficheiros orixinais van ser copiados en {conf_backup_folder}.",
"migration_0019_add_new_attributes_in_ldap": "Engadir novos atributos para os permisos na base de datos LDAP",
"migration_0018_failed_to_reset_legacy_rules": "Fallou o restablecemento das regras antigas de iptables: {error}",
"migration_0018_failed_to_migrate_iptables_rules": "Fallou a migración das regras antigas de iptables a nftables: {error}",
"migration_0017_not_enough_space": "Crea espazo suficiente en {path} para executar a migración.",
"migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 está instado, pero non postgresql 11? Algo raro debeu acontecer no teu sistema :(...",
"migration_0017_postgresql_96_not_installed": "PostgreSQL non está instalado no teu sistema. Nada que facer.",
"migration_0015_weak_certs": "Os seguintes certificados están a utilizar algoritmos de sinatura débiles e teñen que ser actualizados para ser compatibles coa seguinte versión de nginx: {certs}",
"migration_0015_cleaning_up": "Limpando a caché e paquetes que xa non son útiles...",
"migration_0015_specific_upgrade": "Iniciando a actualización dos paquetes do sistema que precisan ser actualizados de xeito independente...",
"migration_0015_modified_files": "Ten en conta que os seguintes ficheiros semella que foron modificados manualmente e poderían ser sobrescritos na actualización: {manually_modified_files}",
"migration_0015_problematic_apps_warning": "Ten en conta que se detectaron as seguintes apps que poderían ser problemáticas. Semella que non foron instaladas usando o catálogo de YunoHost, ou non están marcadas como 'funcionais'. En consecuencia, non se pode garantir que seguirán funcionando após a actualización: {problematic_apps}",
"diagnosis_http_localdomain": "O dominio {domain}, cun TLD .local, non é de agardar que sexa accesible desde o exterior da rede local.",
"diagnosis_dns_specialusedomain": "O dominio {domain} baséase un dominio de nivel alto e uso especial (TLD) polo que non é de agardar que realmente teña rexistros DNS.",
"upnp_enabled": "UPnP activado",
"upnp_disabled": "UPnP desactivado",
"permission_creation_failed": "Non se creou o permiso '{permission}': {error}",
"permission_created": "Creado o permiso '{permission}'",
"permission_cannot_remove_main": "Non está permitido eliminar un permiso principal",
"permission_already_up_to_date": "Non se actualizou o permiso porque as solicitudes de adición/retirada xa coinciden co estado actual.",
"permission_already_exist": "Xa existe o permiso '{permission}'",
"permission_already_disallowed": "O grupo '{group}' xa ten o permiso '{permission}' desactivado",
"permission_already_allowed": "O grupo '{group}' xa ten o permiso '{permission}' activado",
"pattern_password_app": "Lamentámolo, os contrasinais non poden conter os seguintes caracteres: {forbidden_chars}",
"pattern_username": "Só admite caracteres alfanuméricos en minúscula e trazo baixo",
"pattern_positive_number": "Ten que ser un número positivo",
"pattern_port_or_range": "Debe ser un número válido de porto (entre 0-65535) ou rango de portos (ex. 100:200)",
"pattern_password": "Ten que ter polo menos 3 caracteres",
"pattern_mailbox_quota": "Ten que ser un tamaño co sufixo b/k/M/G/T ou 0 para non ter unha cota",
"pattern_lastname": "Ten que ser un apelido válido",
"pattern_firstname": "Ten que ser un nome válido",
"pattern_email": "Ten que ser un enderezo de email válido, sen o símbolo '+' (ex. persoa@exemplo.com)",
"pattern_email_forward": "Ten que ser un enderezo de email válido, está aceptado o símbolo '+' (ex. persoa+etiqueta@exemplo.com)",
"pattern_domain": "Ten que ser un nome de dominio válido (ex. dominiopropio.org)",
"pattern_backup_archive_name": "Ten que ser un nome de ficheiro válido con 30 caracteres como máximo, alfanuméricos ou só caracteres -_.",
"password_too_simple_4": "O contrasinal debe ter 12 caracteres como mínimo e conter un díxito, maiúsculas, minúsculas e caracteres especiais",
"password_too_simple_3": "O contrasinal debe ter 8 caracteres como mínimo e conter un díxito, maiúsculas, minúsculas e caracteres especiais",
"password_too_simple_2": "O contrasinal debe ter 8 caracteres como mínimo e conter un díxito, maiúsculas e minúsculas",
"password_listed": "Este contrasinal está entre os máis utilizados no mundo. Por favor elixe outro que sexa máis orixinal.",
"packages_upgrade_failed": "Non se puideron actualizar tódolos paquetes",
"operation_interrupted": "Foi interrumpida manualmente a operación?",
"invalid_number": "Ten que ser un número",
"not_enough_disk_space": "Non hai espazo libre abondo en '{path}'",
"migrations_to_be_ran_manually": "A migración {id} ten que ser executada manualmente. Vaite a Ferramentas → Migracións na páxina webadmin, ou executa `yunohost tools migrations run`.",
"migrations_success_forward": "Migración {id} completada",
"migrations_skip_migration": "Omitindo migración {id}...",
"migrations_running_forward": "Realizando migración {id}...",
"migrations_pending_cant_rerun": "Esas migracións están pendentes, polo que non ser executadas outra vez: {ids}",
"migrations_not_pending_cant_skip": "Esas migracións non están pendentes, polo que non poden ser omitidas: {ids}",
"migrations_no_such_migration": "Non hai migración co nome '{id}'",
"migrations_no_migrations_to_run": "Sen migracións a executar",
"migrations_need_to_accept_disclaimer": "Para executar a migración {id}, tes que aceptar o seguinte aviso:\n---\n{disclaimer}\n---\nSe aceptas executar a migración, por favor volve a executar o comando coa opción '--accept-disclaimer'.",
"migrations_must_provide_explicit_targets": "Debes proporcionar obxectivos explícitos ao utilizar '--skip' ou '--force-rerun'",
"migrations_migration_has_failed": "A migración {id} non se completou, abortando. Erro: {exception}",
"migrations_loading_migration": "Cargando a migración {id}...",
"migrations_list_conflict_pending_done": "Non podes usar ao mesmo tempo '--previous' e '--done'.",
"migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' son opcións que se exclúen unhas a outras.",
"migrations_failed_to_load_migration": "Non se cargou a migración {id}: {error}",
"migrations_dependencies_not_satisfied": "Executar estas migracións: '{dependencies_id}', antes da migración {id}.",
"migrations_cant_reach_migration_file": "Non se pode acceder aos ficheiros de migración na ruta '%s'",
"regenconf_file_manually_removed": "O ficheiro de configuración '{conf}' foi eliminado manualmente e non será creado",
"regenconf_file_manually_modified": "O ficheiro de configuración '{conf}' foi modificado manualmente e non vai ser actualizado",
"regenconf_file_kept_back": "Era de agardar que o ficheiro de configuración '{conf}' fose eliminado por regen-conf (categoría {category}) mais foi mantido.",
"regenconf_file_copy_failed": "Non se puido copiar o novo ficheiro de configuración '{new}' a '{conf}'",
"regenconf_file_backed_up": "Ficheiro de configuración '{conf}' copiado a '{backup}'",
"postinstall_low_rootfsspace": "O sistema de ficheiros raiz ten un espazo total menor de 10GB, que é pouco! Probablemente vas quedar sen espazo moi pronto! É recomendable ter polo menos 16GB para o sistema raíz. Se queres instalar YunoHost obviando este aviso, volve a executar a postinstalación con --force-diskspace",
"port_already_opened": "O porto {port:d} xa está aberto para conexións {ip_version}",
"port_already_closed": "O porto {port:d} xa está pechado para conexións {ip_version}",
"permission_require_account": "O permiso {permission} só ten sentido para usuarias cunha conta, e por tanto non pode concederse a visitantes.",
"permission_protected": "O permiso {permission} está protexido. Non podes engadir ou eliminar o grupo visitantes a/de este permiso.",
"permission_updated": "Permiso '{permission}' actualizado",
"permission_update_failed": "Non se actualizou o permiso '{permission}': {error}",
"permission_not_found": "Non se atopa o permiso '{permission}'",
"permission_deletion_failed": "Non se puido eliminar o permiso '{permission}': {error}",
"permission_deleted": "O permiso '{permission}' foi eliminado",
"permission_cant_add_to_all_users": "O permiso {permission} non pode ser concecido a tódalas usuarias.",
"permission_currently_allowed_for_all_users": "Este permiso está concedido actualmente a tódalas usuarias ademáis de a outros grupos. Probablemente queiras ben eliminar o permiso 'all_users' ou ben eliminar os outros grupos que teñen permiso."
}

View file

@ -30,7 +30,7 @@
"app_not_correctly_installed": "{app} sembra di non essere installata correttamente", "app_not_correctly_installed": "{app} sembra di non essere installata correttamente",
"app_not_properly_removed": "{app} non è stata correttamente rimossa", "app_not_properly_removed": "{app} non è stata correttamente rimossa",
"action_invalid": "L'azione '{action}' non è valida", "action_invalid": "L'azione '{action}' non è valida",
"app_removed": "{app} rimossa", "app_removed": "{app} disinstallata",
"app_sources_fetch_failed": "Impossibile riportare i file sorgenti, l'URL è corretto?", "app_sources_fetch_failed": "Impossibile riportare i file sorgenti, l'URL è corretto?",
"app_upgrade_failed": "Impossibile aggiornare {app}: {error}", "app_upgrade_failed": "Impossibile aggiornare {app}: {error}",
"app_upgraded": "{app} aggiornata", "app_upgraded": "{app} aggiornata",
@ -172,7 +172,7 @@
"backup_applying_method_custom": "Chiamando il metodo di backup personalizzato '{method}'...", "backup_applying_method_custom": "Chiamando il metodo di backup personalizzato '{method}'...",
"backup_applying_method_tar": "Creando l'archivio TAR del backup...", "backup_applying_method_tar": "Creando l'archivio TAR del backup...",
"backup_archive_system_part_not_available": "La parte di sistema '{part}' non è disponibile in questo backup", "backup_archive_system_part_not_available": "La parte di sistema '{part}' non è disponibile in questo backup",
"backup_archive_writing_error": "Impossibile aggiungere i file '{source}' (indicati nell'archivio '{dest}') al backup nell'archivio compresso '{archive}'", "backup_archive_writing_error": "Impossibile aggiungere i file '{source}' (indicati nell'archivio '{dest}') al backup nell'archivio compresso '{archive}'",
"backup_ask_for_copying_if_needed": "Vuoi effettuare il backup usando {size}MB temporaneamente? (È necessario usare questo sistema poiché alcuni file non possono essere preparati in un modo più efficiente)", "backup_ask_for_copying_if_needed": "Vuoi effettuare il backup usando {size}MB temporaneamente? (È necessario usare questo sistema poiché alcuni file non possono essere preparati in un modo più efficiente)",
"backup_cant_mount_uncompress_archive": "Impossibile montare in modalità sola lettura la cartella di archivio non compressa", "backup_cant_mount_uncompress_archive": "Impossibile montare in modalità sola lettura la cartella di archivio non compressa",
"backup_copying_to_organize_the_archive": "Copiando {size}MB per organizzare l'archivio", "backup_copying_to_organize_the_archive": "Copiando {size}MB per organizzare l'archivio",
@ -428,7 +428,6 @@
"service_description_fail2ban": "Ti protegge dal brute-force e altri tipi di attacchi da Internet", "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_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_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}]", "server_reboot_confirm": "Il server si riavvierà immediatamente, sei sicuro? [{answers}]",
"server_reboot": "Il server si riavvierà", "server_reboot": "Il server si riavvierà",
"server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers}]", "server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers}]",
@ -631,5 +630,9 @@
"diagnosis_sshd_config_inconsistent": "Sembra che la porta SSH sia stata modificata manualmente in /etc/ssh/sshd_config: A partire da YunoHost 4.2, una nuova configurazione globale 'security.ssh.port' è disponibile per evitare di modificare manualmente la configurazione.", "diagnosis_sshd_config_inconsistent": "Sembra che la porta SSH sia stata modificata manualmente in /etc/ssh/sshd_config: A partire da YunoHost 4.2, una nuova configurazione globale 'security.ssh.port' è disponibile per evitare di modificare manualmente la configurazione.",
"diagnosis_sshd_config_insecure": "Sembra che la configurazione SSH sia stata modificata manualmente, ed non è sicuro dato che non contiene le direttive 'AllowGroups' o 'Allowusers' che limitano l'accesso agli utenti autorizzati.", "diagnosis_sshd_config_insecure": "Sembra che la configurazione SSH sia stata modificata manualmente, ed non è sicuro dato che non contiene le direttive 'AllowGroups' o 'Allowusers' che limitano l'accesso agli utenti autorizzati.",
"backup_create_size_estimation": "L'archivio conterrà circa {size} di dati.", "backup_create_size_estimation": "L'archivio conterrà circa {size} di dati.",
"app_restore_script_failed": "C'è stato un errore all'interno dello script di recupero" "app_restore_script_failed": "C'è stato un errore all'interno dello script di recupero",
"global_settings_setting_security_webadmin_allowlist": "Indirizzi IP con il permesso di accedere al webadmin, separati da virgola.",
"global_settings_setting_security_webadmin_allowlist_enabled": "Permetti solo ad alcuni IP di accedere al webadmin.",
"disk_space_not_sufficient_update": "Non c'è abbastanza spazio libero per aggiornare questa applicazione",
"disk_space_not_sufficient_install": "Non c'è abbastanza spazio libero per installare questa applicazione"
} }

View file

@ -193,7 +193,6 @@
"user_unknown": "Utilizaire « {user} »desconegut", "user_unknown": "Utilizaire « {user} »desconegut",
"user_update_failed": "Modificacion impossibla de lutilizaire", "user_update_failed": "Modificacion impossibla de lutilizaire",
"user_updated": "Lutilizaire es estat modificat", "user_updated": "Lutilizaire es estat modificat",
"service_description_avahi-daemon": "permet daténher vòstre servidor via yunohost.local sus vòstre ret local",
"service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)", "service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)",
"updating_apt_cache": "Actualizacion de la lista dels paquets disponibles…", "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles…",
"server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament? {answers}", "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament? {answers}",

View file

@ -1,12 +1,12 @@
{ {
"action_invalid": "Acção Inválida '{action}'", "action_invalid": "Acção Inválida '{action}'",
"admin_password": "Senha de administração", "admin_password": "Senha de administração",
"admin_password_change_failed": "Não é possível alterar a senha", "admin_password_change_failed": "Não foi possível alterar a senha",
"admin_password_changed": "A senha da administração foi alterada", "admin_password_changed": "A senha da administração foi alterada",
"app_already_installed": "{app} já está instalada", "app_already_installed": "{app} já está instalada",
"app_extraction_failed": "Não foi possível extrair os ficheiros para instalação", "app_extraction_failed": "Não foi possível extrair os arquivos para instalação",
"app_id_invalid": "A ID da aplicação é inválida", "app_id_invalid": "App ID invaĺido",
"app_install_files_invalid": "Ficheiros para instalação corrompidos", "app_install_files_invalid": "Esses arquivos não podem ser instalados",
"app_manifest_invalid": "Manifesto da aplicação inválido: {error}", "app_manifest_invalid": "Manifesto da aplicação inválido: {error}",
"app_not_installed": "{app} não está instalada", "app_not_installed": "{app} não está instalada",
"app_removed": "{app} removida com êxito", "app_removed": "{app} removida com êxito",
@ -108,12 +108,12 @@
"backup_hook_unknown": "Gancho de backup '{hook}' desconhecido", "backup_hook_unknown": "Gancho de backup '{hook}' desconhecido",
"backup_nothings_done": "Não há nada para guardar", "backup_nothings_done": "Não há nada para guardar",
"backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas", "backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas",
"app_already_installed_cant_change_url": "Este aplicativo já está instalado. A URL não pode ser alterada apenas por esta função. Olhe para o `app changeurl` se estiver disponível.", "app_already_installed_cant_change_url": "Este aplicativo já está instalado. A URL não pode ser alterada apenas por esta função. Confira em `app changeurl` se está disponível.",
"app_already_up_to_date": "{app} já está atualizado", "app_already_up_to_date": "{app} já está atualizado",
"app_argument_choice_invalid": "Escolha inválida para o argumento '{name}', deve ser um dos {choices}", "app_argument_choice_invalid": "Use uma das opções '{choices}' para o argumento '{name}'",
"app_argument_invalid": "Valor inválido de argumento '{name}': {error}", "app_argument_invalid": "Escolha um valor válido para o argumento '{name}': {error}",
"app_argument_required": "O argumento '{name}' é obrigatório", "app_argument_required": "O argumento '{name}' é obrigatório",
"app_change_url_failed_nginx_reload": "Falha ao reiniciar o nginx. Aqui está o retorno de 'nginx -t':\n{nginx_errors}", "app_change_url_failed_nginx_reload": "Não foi possível reiniciar o nginx. Aqui está o retorno de 'nginx -t':\n{nginx_errors}",
"app_location_unavailable": "Esta url não está disponível ou está em conflito com outra aplicação já instalada", "app_location_unavailable": "Esta url não está disponível ou está em conflito com outra aplicação já instalada",
"app_upgrade_app_name": "Atualizando aplicação {app}…", "app_upgrade_app_name": "Atualizando aplicação {app}…",
"app_upgrade_some_app_failed": "Não foi possível atualizar algumas aplicações", "app_upgrade_some_app_failed": "Não foi possível atualizar algumas aplicações",
@ -129,5 +129,16 @@
"app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain}{path}'), nada para fazer.", "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain}{path}'), nada para fazer.",
"password_too_simple_1": "A senha precisa ter pelo menos 8 caracteres", "password_too_simple_1": "A senha precisa ter pelo menos 8 caracteres",
"admin_password_too_long": "Escolha uma senha que contenha menos de 127 caracteres", "admin_password_too_long": "Escolha uma senha que contenha menos de 127 caracteres",
"aborting": "Abortando." "aborting": "Abortando.",
"app_change_url_no_script": "A aplicação '{app_name}' ainda não permite modificar a URL. Talvez devesse atualizá-la.",
"app_argument_password_no_default": "Erro ao interpretar argumento da senha '{name}': O argumento da senha não pode ter um valor padrão por segurança",
"app_action_cannot_be_ran_because_required_services_down": "Estes serviços devem estar funcionado para executar esta ação: {services}. Tente reiniciá-los para continuar (e possivelmente investigar o porquê de não estarem funcionado).",
"app_action_broke_system": "Esta ação parece ter quebrado estes serviços importantes: {services}",
"already_up_to_date": "Nada a ser feito. Tudo já está atualizado.",
"additional_urls_already_removed": "A URL adicional '{url}'já está removida para a permissão '{permission}'",
"additional_urls_already_added": "A URL adicional '{url}' já está adicionada para a permissão '{permission}'",
"app_install_script_failed": "Ocorreu um erro dentro do script de instalação do aplicativo",
"app_install_failed": "Não foi possível instalar {app}: {error}",
"app_full_domain_unavailable": "Desculpe, esse app deve ser instalado num domínio próprio mas já há outros apps instalados no domínio '{domain}'. Você pode usar um subdomínio dedicado a esse aplicativo.",
"app_change_url_success": "A URL agora é {domain}{path}"
} }

View file

@ -1 +1,36 @@
{} {
"app_manifest_install_ask_domain": "Оберіть домен, в якому треба встановити цей застосунок",
"app_manifest_invalid": "Щось не так з маніфестом застосунку: {error}",
"app_location_unavailable": "Ця URL-адреса або недоступна, або конфліктує з уже встановленим застосунком (застосунками):\n{apps}",
"app_label_deprecated": "Ця команда застаріла! Будь ласка, використовуйте нову команду 'yunohost user permission update' для управління заголовком застосунку.",
"app_make_default_location_already_used": "Неможливо зробити '{app}' типовим застосунком на домені, '{domain}' вже використовується '{other_app}'",
"app_install_script_failed": "Сталася помилка в скрипті встановлення застосунку",
"app_install_failed": "Неможливо встановити {app}: {error}",
"app_install_files_invalid": "Ці файли не можуть бути встановлені",
"app_id_invalid": "Неприпустимий ID застосунку",
"app_full_domain_unavailable": "Вибачте, цей застосунок повинен бути встановлений на власному домені, але інші застосунки вже встановлені на домені '{domain}'. Замість цього ви можете використовувати піддомен, призначений для цього застосунку.",
"app_extraction_failed": "Не вдалося витягти файли встановлення",
"app_change_url_success": "URL-адреса {app} тепер {domain}{path}",
"app_change_url_no_script": "Застосунок '{app_name}' поки не підтримує зміну URL-адрес. Можливо, вам слід оновити його.",
"app_change_url_identical_domains": "Старий і новий domain/url_path збігаються ('{domain}{path}'), нічого робити не треба.",
"app_change_url_failed_nginx_reload": "Не вдалося перезавантажити NGINX. Ось результат 'nginx -t':\n{nginx_errors}",
"app_argument_required": "Аргумент '{name}' необхідний",
"app_argument_password_no_default": "Помилка під час розбору аргументу пароля '{name}': аргумент пароля не може мати типове значення з причин безпеки",
"app_argument_invalid": "Виберіть правильне значення для аргументу '{name}': {error}",
"app_argument_choice_invalid": "Використовуйте один з цих варіантів '{choices}' для аргументу '{name}'",
"app_already_up_to_date": "{app} має найостаннішу версію",
"app_already_installed_cant_change_url": "Цей застосунок уже встановлено. URL-адреса не може бути змінена тільки цією функцією. Перевірте в `app changeurl`, якщо вона доступна.",
"app_already_installed": "{app} уже встановлено",
"app_action_broke_system": "Ця дія, схоже, порушила роботу наступних важливих служб: {services}",
"app_action_cannot_be_ran_because_required_services_down": "Для виконання цієї дії повинні бути запущені наступні необхідні служби: {services}. Спробуйте перезапустити їх, щоб продовжити (і, можливо, з'ясувати, чому вони не працюють).",
"already_up_to_date": "Нічого не потрібно робити. Все вже актуально.",
"admin_password_too_long": "Будь ласка, виберіть пароль коротше 127 символів",
"admin_password_changed": "Пароль адміністратора було змінено",
"admin_password_change_failed": "Неможливо змінити пароль",
"admin_password": "Пароль адміністратора",
"additional_urls_already_removed": "Додаткова URL-адреса '{url}' вже видалена в додатковій URL-адресі для дозволу '{permission}'",
"additional_urls_already_added": "Додаткова URL-адреса '{url}' вже додана в додаткову URL-адресу для дозволу '{permission}'",
"action_invalid": "Неприпустима дія '{action}'",
"aborting": "Переривання.",
"diagnosis_description_web": "Мережа"
}

View file

@ -226,7 +226,6 @@
"service_description_fail2ban": "防止来自互联网的暴力攻击和其他类型的攻击", "service_description_fail2ban": "防止来自互联网的暴力攻击和其他类型的攻击",
"service_description_dovecot": "允许电子邮件客户端访问/获取电子邮件通过IMAP和POP3", "service_description_dovecot": "允许电子邮件客户端访问/获取电子邮件通过IMAP和POP3",
"service_description_dnsmasq": "处理域名解析DNS", "service_description_dnsmasq": "处理域名解析DNS",
"service_description_avahi-daemon": "允许您使用本地网络中的“ yunohost.local”访问服务器",
"service_started": "服务 '{service}' 已启动", "service_started": "服务 '{service}' 已启动",
"service_start_failed": "无法启动服务 '{service}'\n\n最近的服务日志:{logs}", "service_start_failed": "无法启动服务 '{service}'\n\n最近的服务日志:{logs}",
"service_reloaded_or_restarted": "服务'{service}'已重新加载或重新启动", "service_reloaded_or_restarted": "服务'{service}'已重新加载或重新启动",

View file

@ -38,8 +38,8 @@ import tempfile
import readline import readline
from collections import OrderedDict from collections import OrderedDict
from moulinette import msignals, m18n, msettings
from moulinette.interfaces.cli import colorize from moulinette.interfaces.cli import colorize
from moulinette import Moulinette, m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.network import download_json from moulinette.utils.network import download_json
@ -195,7 +195,8 @@ def app_info(app, full=False):
_assert_is_installed(app) _assert_is_installed(app)
local_manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app)) setting_path = os.path.join(APPS_SETTING_PATH, app)
local_manifest = _get_manifest_of_app(setting_path)
permissions = user_permission_list(full=True, absolute_urls=True, apps=[app])[ permissions = user_permission_list(full=True, absolute_urls=True, apps=[app])[
"permissions" "permissions"
] ]
@ -214,6 +215,7 @@ def app_info(app, full=False):
if not full: if not full:
return ret return ret
ret["setting_path"] = setting_path
ret["manifest"] = local_manifest ret["manifest"] = local_manifest
ret["manifest"]["arguments"] = _set_default_ask_questions( ret["manifest"]["arguments"] = _set_default_ask_questions(
ret["manifest"].get("arguments", {}) ret["manifest"].get("arguments", {})
@ -224,11 +226,11 @@ def app_info(app, full=False):
ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {}) ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {})
ret["upgradable"] = _app_upgradable(ret) ret["upgradable"] = _app_upgradable(ret)
ret["supports_change_url"] = os.path.exists( ret["supports_change_url"] = os.path.exists(
os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url") os.path.join(setting_path, "scripts", "change_url")
) )
ret["supports_backup_restore"] = os.path.exists( ret["supports_backup_restore"] = os.path.exists(
os.path.join(APPS_SETTING_PATH, app, "scripts", "backup") os.path.join(setting_path, "scripts", "backup")
) and os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "restore")) ) and os.path.exists(os.path.join(setting_path, "scripts", "restore"))
ret["supports_multi_instance"] = is_true( ret["supports_multi_instance"] = is_true(
local_manifest.get("multi_instance", False) local_manifest.get("multi_instance", False)
) )
@ -503,7 +505,7 @@ def app_change_url(operation_logger, app, domain, path):
hook_callback("post_app_change_url", env=env_dict) hook_callback("post_app_change_url", env=env_dict)
def app_upgrade(app=[], url=None, file=None, force=False): def app_upgrade(app=[], url=None, file=None, force=False, no_safety_backup=False):
""" """
Upgrade app Upgrade app
@ -511,6 +513,7 @@ def app_upgrade(app=[], url=None, file=None, force=False):
file -- Folder or tarball for upgrade file -- Folder or tarball for upgrade
app -- App(s) to upgrade (default all) app -- App(s) to upgrade (default all)
url -- Git url to fetch for upgrade url -- Git url to fetch for upgrade
no_safety_backup -- Disable the safety backup during upgrade
""" """
from packaging import version from packaging import version
@ -617,6 +620,7 @@ def app_upgrade(app=[], url=None, file=None, force=False):
env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type
env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version) env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version)
env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version) env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version)
env_dict["NO_BACKUP_UPGRADE"] = "1" if no_safety_backup else "0"
# We'll check that the app didn't brutally edit some system configuration # We'll check that the app didn't brutally edit some system configuration
manually_modified_files_before_install = manually_modified_files() manually_modified_files_before_install = manually_modified_files()
@ -646,7 +650,7 @@ def app_upgrade(app=[], url=None, file=None, force=False):
m18n.n("app_upgrade_failed", app=app_instance_name, error=error) m18n.n("app_upgrade_failed", app=app_instance_name, error=error)
) )
failure_message_with_debug_instructions = operation_logger.error(error) failure_message_with_debug_instructions = operation_logger.error(error)
if msettings.get("interface") != "api": if Moulinette.interface.type != "api":
dump_app_log_extract_for_debugging(operation_logger) dump_app_log_extract_for_debugging(operation_logger)
# Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
@ -823,11 +827,11 @@ def app_install(
def confirm_install(confirm): def confirm_install(confirm):
# Ignore if there's nothing for confirm (good quality app), if --force is used # Ignore if there's nothing for confirm (good quality app), if --force is used
# or if request on the API (confirm already implemented on the API side) # or if request on the API (confirm already implemented on the API side)
if confirm is None or force or msettings.get("interface") == "api": if confirm is None or force or Moulinette.interface.type == "api":
return return
if confirm in ["danger", "thirdparty"]: if confirm in ["danger", "thirdparty"]:
answer = msignals.prompt( answer = Moulinette.prompt(
m18n.n("confirm_app_install_" + confirm, answers="Yes, I understand"), m18n.n("confirm_app_install_" + confirm, answers="Yes, I understand"),
color="red", color="red",
) )
@ -835,7 +839,7 @@ def app_install(
raise YunohostError("aborting") raise YunohostError("aborting")
else: else:
answer = msignals.prompt( answer = Moulinette.prompt(
m18n.n("confirm_app_install_" + confirm, answers="Y/N"), color="yellow" m18n.n("confirm_app_install_" + confirm, answers="Y/N"), color="yellow"
) )
if answer.upper() != "Y": if answer.upper() != "Y":
@ -883,7 +887,7 @@ def app_install(
raise YunohostValidationError("disk_space_not_sufficient_install") raise YunohostValidationError("disk_space_not_sufficient_install")
# Check ID # Check ID
if "id" not in manifest or "__" in manifest["id"]: if "id" not in manifest or "__" in manifest["id"] or "." in manifest["id"]:
raise YunohostValidationError("app_id_invalid") raise YunohostValidationError("app_id_invalid")
app_id = manifest["id"] app_id = manifest["id"]
@ -1010,7 +1014,7 @@ def app_install(
error = m18n.n("app_install_script_failed") error = m18n.n("app_install_script_failed")
logger.error(m18n.n("app_install_failed", app=app_id, error=error)) logger.error(m18n.n("app_install_failed", app=app_id, error=error))
failure_message_with_debug_instructions = operation_logger.error(error) failure_message_with_debug_instructions = operation_logger.error(error)
if msettings.get("interface") != "api": if Moulinette.interface.type != "api":
dump_app_log_extract_for_debugging(operation_logger) dump_app_log_extract_for_debugging(operation_logger)
# Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
@ -1184,12 +1188,13 @@ def dump_app_log_extract_for_debugging(operation_logger):
@is_unit_operation() @is_unit_operation()
def app_remove(operation_logger, app): def app_remove(operation_logger, app, purge=False):
""" """
Remove app Remove app
Keyword argument: Keyword arguments:
app -- App(s) to delete app -- App(s) to delete
purge -- Remove with all app data
""" """
from yunohost.hook import hook_exec, hook_remove, hook_callback from yunohost.hook import hook_exec, hook_remove, hook_callback
@ -1227,6 +1232,7 @@ def app_remove(operation_logger, app):
env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NAME"] = app
env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb)
env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?")
env_dict["YNH_APP_PURGE"] = str(purge)
operation_logger.extra.update({"env": env_dict}) operation_logger.extra.update({"env": env_dict})
operation_logger.flush() operation_logger.flush()
@ -1515,7 +1521,7 @@ def app_setting(app, key, value=None, delete=False):
# SET # SET
else: else:
if key in ["redirected_urls", "redirected_regex"]: if key in ["redirected_urls", "redirected_regex"]:
value = yaml.load(value) value = yaml.safe_load(value)
app_settings[key] = value app_settings[key] = value
_set_app_settings(app, app_settings) _set_app_settings(app, app_settings)
@ -1861,13 +1867,13 @@ def app_config_set(operation_logger, app, key=None, value=None, args=None):
logger.debug("Asking unanswered question and prevalidating...") logger.debug("Asking unanswered question and prevalidating...")
args_dict = {} args_dict = {}
for panel in config_panel.get("panel", []): for panel in config_panel.get("panel", []):
if msettings.get('interface') == 'cli' and len(filter_key.split('.')) < 3: if Moulinette.get('interface') == 'cli' and len(filter_key.split('.')) < 3:
msignals.display(colorize("\n" + "=" * 40, 'purple')) Moulinette.display(colorize("\n" + "=" * 40, 'purple'))
msignals.display(colorize(f">>>> {panel['name']}", 'purple')) Moulinette.display(colorize(f">>>> {panel['name']}", 'purple'))
msignals.display(colorize("=" * 40, 'purple')) Moulinette.display(colorize("=" * 40, 'purple'))
for section in panel.get("sections", []): for section in panel.get("sections", []):
if msettings.get('interface') == 'cli' and len(filter_key.split('.')) < 3: if Moulinette.get('interface') == 'cli' and len(filter_key.split('.')) < 3:
msignals.display(colorize(f"\n# {section['name']}", 'purple')) Moulinette.display(colorize(f"\n# {section['name']}", 'purple'))
# Check and ask unanswered questions # Check and ask unanswered questions
args_dict.update(_parse_args_in_yunohost_format( args_dict.update(_parse_args_in_yunohost_format(
@ -2284,7 +2290,7 @@ def _get_app_settings(app_id):
) )
try: try:
with open(os.path.join(APPS_SETTING_PATH, app_id, "settings.yml")) as f: with open(os.path.join(APPS_SETTING_PATH, app_id, "settings.yml")) as f:
settings = yaml.load(f) settings = yaml.safe_load(f)
# If label contains unicode char, this may later trigger issues when building strings... # If label contains unicode char, this may later trigger issues when building strings...
# FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think... # FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think...
settings = {k: v for k, v in settings.items()} settings = {k: v for k, v in settings.items()}
@ -2851,12 +2857,12 @@ class YunoHostArgumentFormatParser(object):
while True: while True:
# Display question if no value filled or if it's a readonly message # Display question if no value filled or if it's a readonly message
if msettings.get('interface') == 'cli': if Moulinette.get('interface') == 'cli':
text_for_user_input_in_cli = self._format_text_for_user_input_in_cli( text_for_user_input_in_cli = self._format_text_for_user_input_in_cli(
question question
) )
if getattr(self, "readonly", False): if getattr(self, "readonly", False):
msignals.display(text_for_user_input_in_cli) Moulinette.display(text_for_user_input_in_cli)
elif question.value is None: elif question.value is None:
prefill = None prefill = None
@ -2866,7 +2872,7 @@ class YunoHostArgumentFormatParser(object):
prefill = question.default prefill = question.default
readline.set_startup_hook(lambda: readline.insert_text(prefill)) readline.set_startup_hook(lambda: readline.insert_text(prefill))
try: try:
question.value = msignals.prompt( question.value = Moulinette.prompt(
message=text_for_user_input_in_cli, message=text_for_user_input_in_cli,
is_password=self.hide_user_input_in_prompt, is_password=self.hide_user_input_in_prompt,
confirm=self.hide_user_input_in_prompt confirm=self.hide_user_input_in_prompt
@ -2887,9 +2893,9 @@ class YunoHostArgumentFormatParser(object):
try: try:
self._prevalidate(question) self._prevalidate(question)
except YunohostValidationError as e: except YunohostValidationError as e:
if msettings.get('interface') == 'api': if Moulinette.get('interface') == 'api':
raise raise
msignals.display(str(e), 'error') Moulinette.display(str(e), 'error')
question.value = None question.value = None
continue continue
break break
@ -3173,7 +3179,7 @@ class FileArgumentParser(YunoHostArgumentFormatParser):
@classmethod @classmethod
def clean_upload_dirs(cls): def clean_upload_dirs(cls):
# Delete files uploaded from API # Delete files uploaded from API
if msettings.get('interface') == 'api': if Moulinette.get('interface') == 'api':
for upload_dir in cls.upload_dirs: for upload_dir in cls.upload_dirs:
if os.path.exists(upload_dir): if os.path.exists(upload_dir):
shutil.rmtree(upload_dir) shutil.rmtree(upload_dir)
@ -3186,7 +3192,7 @@ class FileArgumentParser(YunoHostArgumentFormatParser):
question_parsed.accept = question.get('accept').replace(' ', '').split(',') question_parsed.accept = question.get('accept').replace(' ', '').split(',')
else: else:
question_parsed.accept = [] question_parsed.accept = []
if msettings.get('interface') == 'api': if Moulinette.get('interface') == 'api':
if user_answers.get(question_parsed.name): if user_answers.get(question_parsed.name):
question_parsed.value = { question_parsed.value = {
'content': question_parsed.value, 'content': question_parsed.value,
@ -3217,7 +3223,7 @@ class FileArgumentParser(YunoHostArgumentFormatParser):
if not question.value: if not question.value:
return question.value return question.value
if msettings.get('interface') == 'api': if Moulinette.get('interface') == 'api':
upload_dir = tempfile.mkdtemp(prefix='tmp_configpanel_') upload_dir = tempfile.mkdtemp(prefix='tmp_configpanel_')
FileArgumentParser.upload_dirs += [upload_dir] FileArgumentParser.upload_dirs += [upload_dir]

View file

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
import os
import logging
import ldap
import ldap.sasl
import time
from moulinette import m18n
from moulinette.authentication import BaseAuthenticator
from yunohost.utils.error import YunohostError
logger = logging.getLogger("yunohost.authenticators.ldap_admin")
class Authenticator(BaseAuthenticator):
name = "ldap_admin"
def __init__(self, *args, **kwargs):
self.uri = "ldap://localhost:389"
self.basedn = "dc=yunohost,dc=org"
self.admindn = "cn=admin,dc=yunohost,dc=org"
def _authenticate_credentials(self, credentials=None):
# TODO : change authentication format
# to support another dn to support multi-admins
def _reconnect():
con = ldap.ldapobject.ReconnectLDAPObject(
self.uri, retry_max=10, retry_delay=0.5
)
con.simple_bind_s(self.admindn, credentials)
return con
try:
con = _reconnect()
except ldap.INVALID_CREDENTIALS:
raise YunohostError("invalid_password")
except ldap.SERVER_DOWN:
# ldap is down, attempt to restart it before really failing
logger.warning(m18n.n("ldap_server_is_down_restart_it"))
os.system("systemctl restart slapd")
time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted
try:
con = _reconnect()
except ldap.SERVER_DOWN:
raise YunohostError("ldap_server_down")
# Check that we are indeed logged in with the expected identity
try:
# whoami_s return dn:..., then delete these 3 characters
who = con.whoami_s()[3:]
except Exception as e:
logger.warning("Error during ldap authentication process: %s", e)
raise
else:
if who != self.admindn:
raise YunohostError(
f"Not logged with the appropriate identity ? Found {who}, expected {self.admindn} !?",
raw_msg=True,
)
finally:
# Free the connection, we don't really need it to keep it open as the point is only to check authentication...
if con:
con.unbind_s()

View file

@ -38,7 +38,7 @@ from collections import OrderedDict
from functools import reduce from functools import reduce
from packaging import version from packaging import version
from moulinette import msignals, m18n, msettings from moulinette import Moulinette, m18n
from moulinette.utils import filesystem from moulinette.utils import filesystem
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, mkdir, write_to_yaml, read_yaml from moulinette.utils.filesystem import read_file, mkdir, write_to_yaml, read_yaml
@ -1509,7 +1509,7 @@ class RestoreManager:
m18n.n("app_restore_failed", app=app_instance_name, error=error) m18n.n("app_restore_failed", app=app_instance_name, error=error)
) )
failure_message_with_debug_instructions = operation_logger.error(error) failure_message_with_debug_instructions = operation_logger.error(error)
if msettings.get("interface") != "api": if Moulinette.interface.type != "api":
dump_app_log_extract_for_debugging(operation_logger) dump_app_log_extract_for_debugging(operation_logger)
# Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
@ -1840,7 +1840,7 @@ class BackupMethod(object):
# Ask confirmation for copying # Ask confirmation for copying
if size > MB_ALLOWED_TO_ORGANIZE: if size > MB_ALLOWED_TO_ORGANIZE:
try: try:
i = msignals.prompt( i = Moulinette.prompt(
m18n.n( m18n.n(
"backup_ask_for_copying_if_needed", "backup_ask_for_copying_if_needed",
answers="y/N", answers="y/N",
@ -2344,7 +2344,7 @@ def backup_restore(name, system=[], apps=[], force=False):
if not force: if not force:
try: try:
# Ask confirmation for restoring # Ask confirmation for restoring
i = msignals.prompt( i = Moulinette.prompt(
m18n.n("restore_confirm_yunohost_installed", answers="y/N") m18n.n("restore_confirm_yunohost_installed", answers="y/N")
) )
except NotImplemented: except NotImplemented:
@ -2418,7 +2418,7 @@ def backup_list(with_info=False, human_readable=False):
def backup_download(name): def backup_download(name):
if msettings.get("interface") != "api": if Moulinette.interface.type != "api":
logger.error( logger.error(
"This option is only meant for the API/webadmin and doesn't make sense for the command line." "This option is only meant for the API/webadmin and doesn't make sense for the command line."
) )

View file

@ -28,7 +28,7 @@ import re
import os import os
import time import time
from moulinette import m18n, msettings from moulinette import m18n, Moulinette
from moulinette.utils import log from moulinette.utils import log
from moulinette.utils.filesystem import ( from moulinette.utils.filesystem import (
read_json, read_json,
@ -138,7 +138,7 @@ def diagnosis_show(
url = yunopaste(content) url = yunopaste(content)
logger.info(m18n.n("log_available_on_yunopaste", url=url)) logger.info(m18n.n("log_available_on_yunopaste", url=url))
if msettings.get("interface") == "api": if Moulinette.interface.type == "api":
return {"url": url} return {"url": url}
else: else:
return return
@ -219,7 +219,7 @@ def diagnosis_run(
if email: if email:
_email_diagnosis_issues() _email_diagnosis_issues()
if issues and msettings.get("interface") == "cli": if issues and Moulinette.interface.type == "cli":
logger.warning(m18n.n("diagnosis_display_tip")) logger.warning(m18n.n("diagnosis_display_tip"))
@ -595,7 +595,7 @@ class Diagnoser:
info[1].update(meta_data) info[1].update(meta_data)
s = m18n.n(info[0], **(info[1])) s = m18n.n(info[0], **(info[1]))
# In cli, we remove the html tags # In cli, we remove the html tags
if msettings.get("interface") != "api" or force_remove_html_tags: if Moulinette.interface.type != "api" or force_remove_html_tags:
s = s.replace("<cmd>", "'").replace("</cmd>", "'") s = s.replace("<cmd>", "'").replace("</cmd>", "'")
s = html_tags.sub("", s.replace("<br>", "\n")) s = html_tags.sub("", s.replace("<br>", "\n"))
else: else:

View file

@ -26,7 +26,7 @@
import os import os
import re import re
from moulinette import m18n, msettings, msignals from moulinette import m18n, Moulinette
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
@ -166,7 +166,9 @@ def domain_add(operation_logger, domain, dyndns=False):
# because it's one of the major service, but in the long term we # because it's one of the major service, but in the long term we
# should identify the root of this bug... # should identify the root of this bug...
_force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain]) _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", "mdns"]
)
app_ssowatconf() app_ssowatconf()
except Exception as e: except Exception as e:
@ -239,8 +241,8 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
if apps_on_that_domain: if apps_on_that_domain:
if remove_apps: if remove_apps:
if msettings.get("interface") == "cli" and not force: if Moulinette.interface.type == "cli" and not force:
answer = msignals.prompt( answer = Moulinette.prompt(
m18n.n( m18n.n(
"domain_remove_confirm_apps_removal", "domain_remove_confirm_apps_removal",
apps="\n".join([x[1] for x in apps_on_that_domain]), apps="\n".join([x[1] for x in apps_on_that_domain]),
@ -293,7 +295,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False):
"/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True "/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", "rspamd", "mdns"])
app_ssowatconf() app_ssowatconf()
hook_callback("post_domain_remove", args=[domain]) hook_callback("post_domain_remove", args=[domain])
@ -346,7 +348,7 @@ def domain_dns_conf(domain, ttl=None):
for record in record_list: for record in record_list:
result += "\n{name} {ttl} IN {type} {value}".format(**record) result += "\n{name} {ttl} IN {type} {value}".format(**record)
if msettings.get("interface") == "cli": if Moulinette.interface.type == "cli":
logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation")) logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation"))
return result return result

View file

@ -179,7 +179,7 @@ def firewall_list(raw=False, by_ip_version=False, list_forwarded=False):
""" """
with open(FIREWALL_FILE) as f: with open(FIREWALL_FILE) as f:
firewall = yaml.load(f) firewall = yaml.safe_load(f)
if raw: if raw:
return firewall return firewall

View file

@ -31,7 +31,7 @@ import mimetypes
from glob import iglob from glob import iglob
from importlib import import_module from importlib import import_module
from moulinette import m18n, msettings from moulinette import m18n, Moulinette
from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils import log from moulinette.utils import log
from moulinette.utils.filesystem import read_yaml from moulinette.utils.filesystem import read_yaml
@ -416,7 +416,7 @@ def _hook_exec_bash(path, args, chdir, env, user, return_format, loggers):
env = {} env = {}
env["YNH_CWD"] = chdir env["YNH_CWD"] = chdir
env["YNH_INTERFACE"] = msettings.get("interface") env["YNH_INTERFACE"] = Moulinette.interface.type
stdreturn = os.path.join(tempfile.mkdtemp(), "stdreturn") stdreturn = os.path.join(tempfile.mkdtemp(), "stdreturn")
with open(stdreturn, "w") as f: with open(stdreturn, "w") as f:

View file

@ -33,7 +33,7 @@ import psutil
from datetime import datetime, timedelta from datetime import datetime, timedelta
from logging import FileHandler, getLogger, Formatter from logging import FileHandler, getLogger, Formatter
from moulinette import m18n, msettings from moulinette import m18n, Moulinette
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.packages import get_ynh_package_version from yunohost.utils.packages import get_ynh_package_version
@ -44,7 +44,6 @@ CATEGORIES_PATH = "/var/log/yunohost/categories/"
OPERATIONS_PATH = "/var/log/yunohost/categories/operation/" OPERATIONS_PATH = "/var/log/yunohost/categories/operation/"
METADATA_FILE_EXT = ".yml" METADATA_FILE_EXT = ".yml"
LOG_FILE_EXT = ".log" LOG_FILE_EXT = ".log"
RELATED_CATEGORIES = ["app", "domain", "group", "service", "user"]
logger = getActionLogger("yunohost.log") logger = getActionLogger("yunohost.log")
@ -125,7 +124,7 @@ def log_list(limit=None, with_details=False, with_suboperations=False):
operations = list(reversed(sorted(operations, key=lambda o: o["name"]))) operations = list(reversed(sorted(operations, key=lambda o: o["name"])))
# Reverse the order of log when in cli, more comfortable to read (avoid # Reverse the order of log when in cli, more comfortable to read (avoid
# unecessary scrolling) # unecessary scrolling)
is_api = msettings.get("interface") == "api" is_api = Moulinette.interface.type == "api"
if not is_api: if not is_api:
operations = list(reversed(operations)) operations = list(reversed(operations))
@ -214,7 +213,7 @@ def log_show(
url = yunopaste(content) url = yunopaste(content)
logger.info(m18n.n("log_available_on_yunopaste", url=url)) logger.info(m18n.n("log_available_on_yunopaste", url=url))
if msettings.get("interface") == "api": if Moulinette.interface.type == "api":
return {"url": url} return {"url": url}
else: else:
return return
@ -609,7 +608,7 @@ class OperationLogger(object):
"operation": self.operation, "operation": self.operation,
"parent": self.parent, "parent": self.parent,
"yunohost_version": get_ynh_package_version("yunohost")["version"], "yunohost_version": get_ynh_package_version("yunohost")["version"],
"interface": msettings.get("interface"), "interface": Moulinette.interface.type,
} }
if self.related_to is not None: if self.related_to is not None:
data["related_to"] = self.related_to data["related_to"] = self.related_to
@ -663,7 +662,7 @@ class OperationLogger(object):
self.logger.removeHandler(self.file_handler) self.logger.removeHandler(self.file_handler)
self.file_handler.close() self.file_handler.close()
is_api = msettings.get("interface") == "api" is_api = Moulinette.interface.type == "api"
desc = _get_description_from_name(self.name) desc = _get_description_from_name(self.name)
if error is None: if error is None:
if is_api: if is_api:

View file

@ -444,7 +444,7 @@ def _get_regenconf_infos():
""" """
try: try:
with open(REGEN_CONF_FILE, "r") as f: with open(REGEN_CONF_FILE, "r") as f:
return yaml.load(f) return yaml.safe_load(f)
except Exception: except Exception:
return {} return {}

View file

@ -37,10 +37,19 @@ from moulinette import m18n
from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.error import YunohostError, YunohostValidationError
from moulinette.utils.process import check_output from moulinette.utils.process import check_output
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, append_to_file, write_to_file from moulinette.utils.filesystem import (
read_file,
append_to_file,
write_to_file,
read_yaml,
write_to_yaml,
)
MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock"
SERVICES_CONF = "/etc/yunohost/services.yml"
SERVICES_CONF_BASE = "/usr/share/yunohost/templates/yunohost/services.yml"
logger = getActionLogger("yunohost.service") logger = getActionLogger("yunohost.service")
@ -127,7 +136,8 @@ def service_add(
try: try:
_save_services(services) _save_services(services)
except Exception: except Exception as e:
logger.warning(e)
# we'll get a logger.warning with more details in _save_services # we'll get a logger.warning with more details in _save_services
raise YunohostError("service_add_failed", service=name) raise YunohostError("service_add_failed", service=name)
@ -669,17 +679,21 @@ def _get_services():
""" """
try: try:
with open("/etc/yunohost/services.yml", "r") as f: services = read_yaml(SERVICES_CONF_BASE) or {}
services = yaml.load(f) or {}
# These are keys flagged 'null' in the base conf
legacy_keys_to_delete = [k for k, v in services.items() if v is None]
services.update(read_yaml(SERVICES_CONF) or {})
services = {
name: infos
for name, infos in services.items()
if name not in legacy_keys_to_delete
}
except Exception: except Exception:
return {} return {}
# some services are marked as None to remove them from YunoHost
# filter this
for key, value in list(services.items()):
if value is None:
del services[key]
# Dirty hack to automatically find custom SSH port ... # Dirty hack to automatically find custom SSH port ...
ssh_port_line = re.findall( ssh_port_line = re.findall(
r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config") r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config")
@ -703,6 +717,13 @@ def _get_services():
del services["postgresql"]["description"] del services["postgresql"]["description"]
services["postgresql"]["actual_systemd_service"] = "postgresql@11-main" services["postgresql"]["actual_systemd_service"] = "postgresql@11-main"
# Remove legacy /var/log/daemon.log and /var/log/syslog from log entries
# because they are too general. Instead, now the journalctl log is
# returned by default which is more relevant.
for infos in services.values():
if infos.get("log") in ["/var/log/syslog", "/var/log/daemon.log"]:
del infos["log"]
return services return services
@ -714,12 +735,26 @@ def _save_services(services):
services -- A dict of managed services with their parameters services -- A dict of managed services with their parameters
""" """
try:
with open("/etc/yunohost/services.yml", "w") as f: # Compute the diff with the base file
yaml.safe_dump(services, f, default_flow_style=False) # such that /etc/yunohost/services.yml contains the minimal
except Exception as e: # changes with respect to the base conf
logger.warning("Error while saving services, exception: %s", e, exc_info=1)
raise conf_base = yaml.safe_load(open(SERVICES_CONF_BASE)) or {}
diff = {}
for service_name, service_infos in services.items():
service_conf_base = conf_base.get(service_name, {})
diff[service_name] = {}
for key, value in service_infos.items():
if service_conf_base.get(key) != value:
diff[service_name][key] = value
diff = {name: infos for name, infos in diff.items() if infos}
write_to_yaml(SERVICES_CONF, diff)
def _tail(file, n): def _tail(file, n):

View file

@ -102,6 +102,7 @@ DEFAULTS = OrderedDict(
("ssowat.panel_overlay.enabled", {"type": "bool", "default": True}), ("ssowat.panel_overlay.enabled", {"type": "bool", "default": True}),
("security.webadmin.allowlist.enabled", {"type": "bool", "default": False}), ("security.webadmin.allowlist.enabled", {"type": "bool", "default": False}),
("security.webadmin.allowlist", {"type": "string", "default": ""}), ("security.webadmin.allowlist", {"type": "string", "default": ""}),
("security.experimental.enabled", {"type": "bool", "default": False}),
] ]
) )
@ -399,6 +400,12 @@ def reconfigure_nginx(setting_name, old_value, new_value):
regen_conf(names=["nginx"]) regen_conf(names=["nginx"])
@post_change_hook("security.experimental.enabled")
def reconfigure_nginx_and_yunohost(setting_name, old_value, new_value):
if old_value != new_value:
regen_conf(names=["nginx", "yunohost"])
@post_change_hook("security.ssh.compatibility") @post_change_hook("security.ssh.compatibility")
def reconfigure_ssh(setting_name, old_value, new_value): def reconfigure_ssh(setting_name, old_value, new_value):
if old_value != new_value: if old_value != new_value:

View file

@ -3,7 +3,7 @@ import pytest
import sys import sys
import moulinette import moulinette
from moulinette import m18n, msettings from moulinette import m18n, Moulinette
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
from contextlib import contextmanager from contextlib import contextmanager
@ -81,4 +81,12 @@ def pytest_cmdline_main(config):
import yunohost import yunohost
yunohost.init(debug=config.option.yunodebug) yunohost.init(debug=config.option.yunodebug)
msettings["interface"] = "test"
class DummyInterface:
type = "test"
def prompt(*args, **kwargs):
raise NotImplementedError
Moulinette._interface = DummyInterface()

View file

@ -5,7 +5,7 @@ from mock import patch
from io import StringIO from io import StringIO
from collections import OrderedDict from collections import OrderedDict
from moulinette import msignals from moulinette import Moulinette
from yunohost import domain, user from yunohost import domain, user
from yunohost.app import _parse_args_in_yunohost_format, PasswordArgumentParser from yunohost.app import _parse_args_in_yunohost_format, PasswordArgumentParser
@ -84,7 +84,7 @@ def test_parse_args_in_yunohost_format_string_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -97,7 +97,7 @@ def test_parse_args_in_yunohost_format_string_input_no_ask():
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -124,7 +124,7 @@ def test_parse_args_in_yunohost_format_string_optional_with_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -139,7 +139,7 @@ def test_parse_args_in_yunohost_format_string_optional_with_empty_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("", "string")}) expected_result = OrderedDict({"some_string": ("", "string")})
with patch.object(msignals, "prompt", return_value=""): with patch.object(Moulinette.interface, "prompt", return_value=""):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -153,7 +153,7 @@ def test_parse_args_in_yunohost_format_string_optional_with_input_without_ask():
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -180,7 +180,9 @@ def test_parse_args_in_yunohost_format_string_input_test_ask():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
prompt.assert_called_with(ask_text, False) prompt.assert_called_with(ask_text, False)
@ -197,7 +199,9 @@ def test_parse_args_in_yunohost_format_string_input_test_ask_with_default():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
prompt.assert_called_with("%s (default: %s)" % (ask_text, default_text), False) prompt.assert_called_with("%s (default: %s)" % (ask_text, default_text), False)
@ -215,7 +219,9 @@ def test_parse_args_in_yunohost_format_string_input_test_ask_with_example():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
assert ask_text in prompt.call_args[0][0] assert ask_text in prompt.call_args[0][0]
assert example_text in prompt.call_args[0][0] assert example_text in prompt.call_args[0][0]
@ -234,7 +240,9 @@ def test_parse_args_in_yunohost_format_string_input_test_ask_with_help():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
assert ask_text in prompt.call_args[0][0] assert ask_text in prompt.call_args[0][0]
assert help_text in prompt.call_args[0][0] assert help_text in prompt.call_args[0][0]
@ -251,7 +259,7 @@ def test_parse_args_in_yunohost_format_string_with_choice_prompt():
questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}] questions = [{"name": "some_string", "type": "string", "choices": ["fr", "en"]}]
answers = {"some_string": "fr"} answers = {"some_string": "fr"}
expected_result = OrderedDict({"some_string": ("fr", "string")}) expected_result = OrderedDict({"some_string": ("fr", "string")})
with patch.object(msignals, "prompt", return_value="fr"): with patch.object(Moulinette.interface, "prompt", return_value="fr"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -275,7 +283,7 @@ def test_parse_args_in_yunohost_format_string_with_choice_ask():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="ru") as prompt: with patch.object(Moulinette.interface, "prompt", return_value="ru") as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
assert ask_text in prompt.call_args[0][0] assert ask_text in prompt.call_args[0][0]
@ -333,7 +341,7 @@ def test_parse_args_in_yunohost_format_password_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_password": ("some_value", "password")}) expected_result = OrderedDict({"some_password": ("some_value", "password")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -347,7 +355,7 @@ def test_parse_args_in_yunohost_format_password_input_no_ask():
answers = {} answers = {}
expected_result = OrderedDict({"some_password": ("some_value", "password")}) expected_result = OrderedDict({"some_password": ("some_value", "password")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -383,7 +391,7 @@ def test_parse_args_in_yunohost_format_password_optional_with_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_password": ("some_value", "password")}) expected_result = OrderedDict({"some_password": ("some_value", "password")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -399,7 +407,7 @@ def test_parse_args_in_yunohost_format_password_optional_with_empty_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_password": ("", "password")}) expected_result = OrderedDict({"some_password": ("", "password")})
with patch.object(msignals, "prompt", return_value=""): with patch.object(Moulinette.interface, "prompt", return_value=""):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -414,7 +422,7 @@ def test_parse_args_in_yunohost_format_password_optional_with_input_without_ask(
answers = {} answers = {}
expected_result = OrderedDict({"some_password": ("some_value", "password")}) expected_result = OrderedDict({"some_password": ("some_value", "password")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -462,7 +470,9 @@ def test_parse_args_in_yunohost_format_password_input_test_ask():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
prompt.assert_called_with(ask_text, True) prompt.assert_called_with(ask_text, True)
@ -481,7 +491,9 @@ def test_parse_args_in_yunohost_format_password_input_test_ask_with_example():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
assert ask_text in prompt.call_args[0][0] assert ask_text in prompt.call_args[0][0]
assert example_text in prompt.call_args[0][0] assert example_text in prompt.call_args[0][0]
@ -501,7 +513,9 @@ def test_parse_args_in_yunohost_format_password_input_test_ask_with_help():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
assert ask_text in prompt.call_args[0][0] assert ask_text in prompt.call_args[0][0]
assert help_text in prompt.call_args[0][0] assert help_text in prompt.call_args[0][0]
@ -594,7 +608,7 @@ def test_parse_args_in_yunohost_format_path_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("some_value", "path")}) expected_result = OrderedDict({"some_path": ("some_value", "path")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -608,7 +622,7 @@ def test_parse_args_in_yunohost_format_path_input_no_ask():
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("some_value", "path")}) expected_result = OrderedDict({"some_path": ("some_value", "path")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -637,7 +651,7 @@ def test_parse_args_in_yunohost_format_path_optional_with_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("some_value", "path")}) expected_result = OrderedDict({"some_path": ("some_value", "path")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -653,7 +667,7 @@ def test_parse_args_in_yunohost_format_path_optional_with_empty_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("", "path")}) expected_result = OrderedDict({"some_path": ("", "path")})
with patch.object(msignals, "prompt", return_value=""): with patch.object(Moulinette.interface, "prompt", return_value=""):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -668,7 +682,7 @@ def test_parse_args_in_yunohost_format_path_optional_with_input_without_ask():
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("some_value", "path")}) expected_result = OrderedDict({"some_path": ("some_value", "path")})
with patch.object(msignals, "prompt", return_value="some_value"): with patch.object(Moulinette.interface, "prompt", return_value="some_value"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -697,7 +711,9 @@ def test_parse_args_in_yunohost_format_path_input_test_ask():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
prompt.assert_called_with(ask_text, False) prompt.assert_called_with(ask_text, False)
@ -715,7 +731,9 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_default():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
prompt.assert_called_with("%s (default: %s)" % (ask_text, default_text), False) prompt.assert_called_with("%s (default: %s)" % (ask_text, default_text), False)
@ -734,7 +752,9 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_example():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
assert ask_text in prompt.call_args[0][0] assert ask_text in prompt.call_args[0][0]
assert example_text in prompt.call_args[0][0] assert example_text in prompt.call_args[0][0]
@ -754,7 +774,9 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_help():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(
Moulinette.interface, "prompt", return_value="some_value"
) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
assert ask_text in prompt.call_args[0][0] assert ask_text in prompt.call_args[0][0]
assert help_text in prompt.call_args[0][0] assert help_text in prompt.call_args[0][0]
@ -918,11 +940,11 @@ def test_parse_args_in_yunohost_format_boolean_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": (1, "boolean")}) expected_result = OrderedDict({"some_boolean": (1, "boolean")})
with patch.object(msignals, "prompt", return_value="y"): with patch.object(Moulinette.interface, "prompt", return_value="y"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
expected_result = OrderedDict({"some_boolean": (0, "boolean")}) expected_result = OrderedDict({"some_boolean": (0, "boolean")})
with patch.object(msignals, "prompt", return_value="n"): with patch.object(Moulinette.interface, "prompt", return_value="n"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -936,7 +958,7 @@ def test_parse_args_in_yunohost_format_boolean_input_no_ask():
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": (1, "boolean")}) expected_result = OrderedDict({"some_boolean": (1, "boolean")})
with patch.object(msignals, "prompt", return_value="y"): with patch.object(Moulinette.interface, "prompt", return_value="y"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -965,7 +987,7 @@ def test_parse_args_in_yunohost_format_boolean_optional_with_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": (1, "boolean")}) expected_result = OrderedDict({"some_boolean": (1, "boolean")})
with patch.object(msignals, "prompt", return_value="y"): with patch.object(Moulinette.interface, "prompt", return_value="y"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -981,7 +1003,7 @@ def test_parse_args_in_yunohost_format_boolean_optional_with_empty_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false
with patch.object(msignals, "prompt", return_value=""): with patch.object(Moulinette.interface, "prompt", return_value=""):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -996,7 +1018,7 @@ def test_parse_args_in_yunohost_format_boolean_optional_with_input_without_ask()
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": (0, "boolean")}) expected_result = OrderedDict({"some_boolean": (0, "boolean")})
with patch.object(msignals, "prompt", return_value="n"): with patch.object(Moulinette.interface, "prompt", return_value="n"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -1039,7 +1061,7 @@ def test_parse_args_in_yunohost_format_boolean_input_test_ask():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value=0) as prompt: with patch.object(Moulinette.interface, "prompt", return_value=0) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
prompt.assert_called_with(ask_text + " [yes | no] (default: no)", False) prompt.assert_called_with(ask_text + " [yes | no] (default: no)", False)
@ -1057,7 +1079,7 @@ def test_parse_args_in_yunohost_format_boolean_input_test_ask_with_default():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value=1) as prompt: with patch.object(Moulinette.interface, "prompt", return_value=1) as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
prompt.assert_called_with("%s [yes | no] (default: yes)" % ask_text, False) prompt.assert_called_with("%s [yes | no] (default: yes)" % ask_text, False)
@ -1193,11 +1215,11 @@ def test_parse_args_in_yunohost_format_domain_two_domains_default_input():
domain, "_get_maindomain", return_value=main_domain domain, "_get_maindomain", return_value=main_domain
), patch.object(domain, "domain_list", return_value={"domains": domains}): ), patch.object(domain, "domain_list", return_value={"domains": domains}):
expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
with patch.object(msignals, "prompt", return_value=main_domain): with patch.object(Moulinette.interface, "prompt", return_value=main_domain):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
expected_result = OrderedDict({"some_domain": (other_domain, "domain")}) expected_result = OrderedDict({"some_domain": (other_domain, "domain")})
with patch.object(msignals, "prompt", return_value=other_domain): with patch.object(Moulinette.interface, "prompt", return_value=other_domain):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -1380,14 +1402,14 @@ def test_parse_args_in_yunohost_format_user_two_users_default_input():
with patch.object(user, "user_list", return_value={"users": users}): with patch.object(user, "user_list", return_value={"users": users}):
with patch.object(user, "user_info", return_value={}): with patch.object(user, "user_info", return_value={}):
expected_result = OrderedDict({"some_user": (username, "user")}) expected_result = OrderedDict({"some_user": (username, "user")})
with patch.object(msignals, "prompt", return_value=username): with patch.object(Moulinette.interface, "prompt", return_value=username):
assert ( assert (
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
== expected_result == expected_result
) )
expected_result = OrderedDict({"some_user": (other_user, "user")}) expected_result = OrderedDict({"some_user": (other_user, "user")})
with patch.object(msignals, "prompt", return_value=other_user): with patch.object(Moulinette.interface, "prompt", return_value=other_user):
assert ( assert (
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
== expected_result == expected_result
@ -1447,14 +1469,14 @@ def test_parse_args_in_yunohost_format_number_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_number": (1337, "number")}) expected_result = OrderedDict({"some_number": (1337, "number")})
with patch.object(msignals, "prompt", return_value="1337"): with patch.object(Moulinette.interface, "prompt", return_value="1337"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
with patch.object(msignals, "prompt", return_value=1337): with patch.object(Moulinette.interface, "prompt", return_value=1337):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
expected_result = OrderedDict({"some_number": (0, "number")}) expected_result = OrderedDict({"some_number": (0, "number")})
with patch.object(msignals, "prompt", return_value=""): with patch.object(Moulinette.interface, "prompt", return_value=""):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -1468,7 +1490,7 @@ def test_parse_args_in_yunohost_format_number_input_no_ask():
answers = {} answers = {}
expected_result = OrderedDict({"some_number": (1337, "number")}) expected_result = OrderedDict({"some_number": (1337, "number")})
with patch.object(msignals, "prompt", return_value="1337"): with patch.object(Moulinette.interface, "prompt", return_value="1337"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -1497,7 +1519,7 @@ def test_parse_args_in_yunohost_format_number_optional_with_input():
answers = {} answers = {}
expected_result = OrderedDict({"some_number": (1337, "number")}) expected_result = OrderedDict({"some_number": (1337, "number")})
with patch.object(msignals, "prompt", return_value="1337"): with patch.object(Moulinette.interface, "prompt", return_value="1337"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -1512,7 +1534,7 @@ def test_parse_args_in_yunohost_format_number_optional_with_input_without_ask():
answers = {} answers = {}
expected_result = OrderedDict({"some_number": (0, "number")}) expected_result = OrderedDict({"some_number": (0, "number")})
with patch.object(msignals, "prompt", return_value="0"): with patch.object(Moulinette.interface, "prompt", return_value="0"):
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -1555,7 +1577,7 @@ def test_parse_args_in_yunohost_format_number_input_test_ask():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="1111") as prompt: with patch.object(Moulinette.interface, "prompt", return_value="1111") as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
prompt.assert_called_with("%s (default: 0)" % (ask_text), False) prompt.assert_called_with("%s (default: 0)" % (ask_text), False)
@ -1573,7 +1595,7 @@ def test_parse_args_in_yunohost_format_number_input_test_ask_with_default():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="1111") as prompt: with patch.object(Moulinette.interface, "prompt", return_value="1111") as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
prompt.assert_called_with("%s (default: %s)" % (ask_text, default_value), False) prompt.assert_called_with("%s (default: %s)" % (ask_text, default_value), False)
@ -1592,7 +1614,7 @@ def test_parse_args_in_yunohost_format_number_input_test_ask_with_example():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="1111") as prompt: with patch.object(Moulinette.interface, "prompt", return_value="1111") as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
assert ask_text in prompt.call_args[0][0] assert ask_text in prompt.call_args[0][0]
assert example_value in prompt.call_args[0][0] assert example_value in prompt.call_args[0][0]
@ -1612,7 +1634,7 @@ def test_parse_args_in_yunohost_format_number_input_test_ask_with_help():
] ]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="1111") as prompt: with patch.object(Moulinette.interface, "prompt", return_value="1111") as prompt:
_parse_args_in_yunohost_format(answers, questions) _parse_args_in_yunohost_format(answers, questions)
assert ask_text in prompt.call_args[0][0] assert ask_text in prompt.call_args[0][0]
assert help_value in prompt.call_args[0][0] assert help_value in prompt.call_args[0][0]

View file

@ -0,0 +1,59 @@
import pytest
import os
from yunohost.authenticators.ldap_admin import Authenticator as LDAPAuth
from yunohost.tools import tools_adminpw
from moulinette import m18n
from moulinette.core import MoulinetteError
def setup_function(function):
if os.system("systemctl is-active slapd") != 0:
os.system("systemctl start slapd && sleep 3")
tools_adminpw("yunohost", check_strength=False)
def test_authenticate():
LDAPAuth().authenticate_credentials(credentials="yunohost")
def test_authenticate_with_wrong_password():
with pytest.raises(MoulinetteError) as exception:
LDAPAuth().authenticate_credentials(credentials="bad_password_lul")
translation = m18n.n("invalid_password")
expected_msg = translation.format()
assert expected_msg in str(exception)
def test_authenticate_server_down(mocker):
os.system("systemctl stop slapd && sleep 3")
# Now if slapd is down, moulinette tries to restart it
mocker.patch("os.system")
mocker.patch("time.sleep")
with pytest.raises(MoulinetteError) as exception:
LDAPAuth().authenticate_credentials(credentials="yunohost")
translation = m18n.n("ldap_server_down")
expected_msg = translation.format()
assert expected_msg in str(exception)
def test_authenticate_change_password():
LDAPAuth().authenticate_credentials(credentials="yunohost")
tools_adminpw("plopette", check_strength=False)
with pytest.raises(MoulinetteError) as exception:
LDAPAuth().authenticate_credentials(credentials="yunohost")
translation = m18n.n("invalid_password")
expected_msg = translation.format()
assert expected_msg in str(exception)
LDAPAuth().authenticate_credentials(credentials="plopette")

View file

@ -30,7 +30,7 @@ import time
from importlib import import_module from importlib import import_module
from packaging import version from packaging import version
from moulinette import msignals, m18n from moulinette import Moulinette, m18n
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output, call_async_output from moulinette.utils.process import check_output, call_async_output
from moulinette.utils.filesystem import read_yaml, write_to_yaml from moulinette.utils.filesystem import read_yaml, write_to_yaml
@ -692,7 +692,7 @@ def tools_shutdown(operation_logger, force=False):
if not shutdown: if not shutdown:
try: try:
# Ask confirmation for server shutdown # Ask confirmation for server shutdown
i = msignals.prompt(m18n.n("server_shutdown_confirm", answers="y/N")) i = Moulinette.prompt(m18n.n("server_shutdown_confirm", answers="y/N"))
except NotImplemented: except NotImplemented:
pass pass
else: else:
@ -711,7 +711,7 @@ def tools_reboot(operation_logger, force=False):
if not reboot: if not reboot:
try: try:
# Ask confirmation for restoring # Ask confirmation for restoring
i = msignals.prompt(m18n.n("server_reboot_confirm", answers="y/N")) i = Moulinette.prompt(m18n.n("server_reboot_confirm", answers="y/N"))
except NotImplemented: except NotImplemented:
pass pass
else: else:

View file

@ -33,7 +33,7 @@ import string
import subprocess import subprocess
import copy import copy
from moulinette import msignals, msettings, m18n from moulinette import Moulinette, m18n
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output from moulinette.utils.process import check_output
@ -117,18 +117,18 @@ def user_create(
# Validate domain used for email address/xmpp account # Validate domain used for email address/xmpp account
if domain is None: if domain is None:
if msettings.get("interface") == "api": if Moulinette.interface.type == "api":
raise YunohostValidationError( raise YunohostValidationError(
"Invalid usage, you should specify a domain argument" "Invalid usage, you should specify a domain argument"
) )
else: else:
# On affiche les differents domaines possibles # On affiche les differents domaines possibles
msignals.display(m18n.n("domains_available")) Moulinette.display(m18n.n("domains_available"))
for domain in domain_list()["domains"]: for domain in domain_list()["domains"]:
msignals.display("- {}".format(domain)) Moulinette.display("- {}".format(domain))
maindomain = _get_maindomain() maindomain = _get_maindomain()
domain = msignals.prompt( domain = Moulinette.prompt(
m18n.n("ask_user_domain") + " (default: %s)" % maindomain m18n.n("ask_user_domain") + " (default: %s)" % maindomain
) )
if not domain: if not domain:
@ -379,8 +379,8 @@ def user_update(
# when in the cli interface if the option to change the password is called # when in the cli interface if the option to change the password is called
# without a specified value, change_password will be set to the const 0. # without a specified value, change_password will be set to the const 0.
# In this case we prompt for the new password. # In this case we prompt for the new password.
if msettings.get("interface") == "cli" and not change_password: if Moulinette.interface.type == "cli" and not change_password:
change_password = msignals.prompt(m18n.n("ask_password"), True, True) change_password = Moulinette.prompt(m18n.n("ask_password"), True, True)
# Ensure sufficiently complex password # Ensure sufficiently complex password
assert_password_is_strong_enough("user", change_password) assert_password_is_strong_enough("user", change_password)

View file

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" License """ License
Copyright (C) 2019 YunoHost Copyright (C) 2019 YunoHost
@ -21,10 +20,18 @@
import os import os
import atexit import atexit
from moulinette.core import MoulinetteLdapIsDownError import logging
from moulinette.authenticators import ldap import ldap
import ldap.sasl
import time
import ldap.modlist as modlist
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
logger = logging.getLogger("yunohost.utils.ldap")
# We use a global variable to do some caching # We use a global variable to do some caching
# to avoid re-authenticating in case we call _get_ldap_authenticator multiple times # to avoid re-authenticating in case we call _get_ldap_authenticator multiple times
_ldap_interface = None _ldap_interface = None
@ -35,47 +42,17 @@ def _get_ldap_interface():
global _ldap_interface global _ldap_interface
if _ldap_interface is None: if _ldap_interface is None:
_ldap_interface = LDAPInterface()
conf = {
"vendor": "ldap",
"name": "as-root",
"parameters": {
"uri": "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi",
"base_dn": "dc=yunohost,dc=org",
"user_rdn": "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth",
},
"extra": {},
}
try:
_ldap_interface = ldap.Authenticator(**conf)
except MoulinetteLdapIsDownError:
raise YunohostError(
"Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'"
)
assert_slapd_is_running()
return _ldap_interface return _ldap_interface
def assert_slapd_is_running():
# Assert slapd is running...
if not os.system("pgrep slapd >/dev/null") == 0:
raise YunohostError(
"Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'"
)
# We regularly want to extract stuff like 'bar' in ldap path like # We regularly want to extract stuff like 'bar' in ldap path like
# foo=bar,dn=users.example.org,ou=example.org,dc=org so this small helper allow # foo=bar,dn=users.example.org,ou=example.org,dc=org so this small helper allow
# to do this without relying of dozens of mysterious string.split()[0] # to do this without relying of dozens of mysterious string.split()[0]
# #
# e.g. using _ldap_path_extract(path, "foo") on the previous example will # e.g. using _ldap_path_extract(path, "foo") on the previous example will
# return bar # return bar
def _ldap_path_extract(path, info): def _ldap_path_extract(path, info):
for element in path.split(","): for element in path.split(","):
if element.startswith(info + "="): if element.startswith(info + "="):
@ -93,3 +70,246 @@ def _destroy_ldap_interface():
atexit.register(_destroy_ldap_interface) atexit.register(_destroy_ldap_interface)
class LDAPInterface:
def __init__(self):
logger.debug("initializing ldap interface")
self.uri = "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi"
self.basedn = "dc=yunohost,dc=org"
self.rootdn = "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth"
self.connect()
def connect(self):
def _reconnect():
con = ldap.ldapobject.ReconnectLDAPObject(
self.uri, retry_max=10, retry_delay=0.5
)
con.sasl_non_interactive_bind_s("EXTERNAL")
return con
try:
con = _reconnect()
except ldap.SERVER_DOWN:
# ldap is down, attempt to restart it before really failing
logger.warning(m18n.n("ldap_server_is_down_restart_it"))
os.system("systemctl restart slapd")
time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted
try:
con = _reconnect()
except ldap.SERVER_DOWN:
raise YunohostError(
"Service slapd is not running but is required to perform this action ... "
"You can try to investigate what's happening with 'systemctl status slapd'"
)
# Check that we are indeed logged in with the right identity
try:
# whoami_s return dn:..., then delete these 3 characters
who = con.whoami_s()[3:]
except Exception as e:
logger.warning("Error during ldap authentication process: %s", e)
raise
else:
if who != self.rootdn:
raise MoulinetteError("Not logged in with the expected userdn ?!")
else:
self.con = con
def __del__(self):
"""Disconnect and free ressources"""
if hasattr(self, "con") and self.con:
self.con.unbind_s()
def search(self, base=None, filter="(objectClass=*)", attrs=["dn"]):
"""Search in LDAP base
Perform an LDAP search operation with given arguments and return
results as a list.
Keyword arguments:
- base -- The dn to search into
- filter -- A string representation of the filter to apply
- attrs -- A list of attributes to fetch
Returns:
A list of all results
"""
if not base:
base = self.basedn
try:
result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
except Exception as e:
raise MoulinetteError(
"error during LDAP search operation with: base='%s', "
"filter='%s', attrs=%s and exception %s" % (base, filter, attrs, e),
raw_msg=True,
)
result_list = []
if not attrs or "dn" not in attrs:
result_list = [entry for dn, entry in result]
else:
for dn, entry in result:
entry["dn"] = [dn]
result_list.append(entry)
def decode(value):
if isinstance(value, bytes):
value = value.decode("utf-8")
return value
# result_list is for example :
# [{'virtualdomain': [b'test.com']}, {'virtualdomain': [b'yolo.test']},
for stuff in result_list:
if isinstance(stuff, dict):
for key, values in stuff.items():
stuff[key] = [decode(v) for v in values]
return result_list
def add(self, rdn, attr_dict):
"""
Add LDAP entry
Keyword arguments:
rdn -- DN without domain
attr_dict -- Dictionnary of attributes/values to add
Returns:
Boolean | MoulinetteError
"""
dn = rdn + "," + self.basedn
ldif = modlist.addModlist(attr_dict)
for i, (k, v) in enumerate(ldif):
if isinstance(v, list):
v = [a.encode("utf-8") for a in v]
elif isinstance(v, str):
v = [v.encode("utf-8")]
ldif[i] = (k, v)
try:
self.con.add_s(dn, ldif)
except Exception as e:
raise MoulinetteError(
"error during LDAP add operation with: rdn='%s', "
"attr_dict=%s and exception %s" % (rdn, attr_dict, e),
raw_msg=True,
)
else:
return True
def remove(self, rdn):
"""
Remove LDAP entry
Keyword arguments:
rdn -- DN without domain
Returns:
Boolean | MoulinetteError
"""
dn = rdn + "," + self.basedn
try:
self.con.delete_s(dn)
except Exception as e:
raise MoulinetteError(
"error during LDAP delete operation with: rdn='%s' and exception %s"
% (rdn, e),
raw_msg=True,
)
else:
return True
def update(self, rdn, attr_dict, new_rdn=False):
"""
Modify LDAP entry
Keyword arguments:
rdn -- DN without domain
attr_dict -- Dictionnary of attributes/values to add
new_rdn -- New RDN for modification
Returns:
Boolean | MoulinetteError
"""
dn = rdn + "," + self.basedn
actual_entry = self.search(base=dn, attrs=None)
ldif = modlist.modifyModlist(actual_entry[0], attr_dict, ignore_oldexistent=1)
if ldif == []:
logger.debug("Nothing to update in LDAP")
return True
try:
if new_rdn:
self.con.rename_s(dn, new_rdn)
new_base = dn.split(",", 1)[1]
dn = new_rdn + "," + new_base
for i, (a, k, vs) in enumerate(ldif):
if isinstance(vs, list):
vs = [v.encode("utf-8") for v in vs]
elif isinstance(vs, str):
vs = [vs.encode("utf-8")]
ldif[i] = (a, k, vs)
self.con.modify_ext_s(dn, ldif)
except Exception as e:
raise MoulinetteError(
"error during LDAP update operation with: rdn='%s', "
"attr_dict=%s, new_rdn=%s and exception: %s"
% (rdn, attr_dict, new_rdn, e),
raw_msg=True,
)
else:
return True
def validate_uniqueness(self, value_dict):
"""
Check uniqueness of values
Keyword arguments:
value_dict -- Dictionnary of attributes/values to check
Returns:
Boolean | MoulinetteError
"""
attr_found = self.get_conflict(value_dict)
if attr_found:
logger.info(
"attribute '%s' with value '%s' is not unique",
attr_found[0],
attr_found[1],
)
raise MoulinetteError(
"ldap_attribute_already_exists",
attribute=attr_found[0],
value=attr_found[1],
)
return True
def get_conflict(self, value_dict, base_dn=None):
"""
Check uniqueness of values
Keyword arguments:
value_dict -- Dictionnary of attributes/values to check
Returns:
None | tuple with Fist conflict attribute name and value
"""
for attr, value in value_dict.items():
if not self.search(base=base_dn, filter=attr + "=" + value):
continue
else:
return (attr, value)
return None

View file

@ -2,4 +2,4 @@ import yaml
def test_yaml_syntax(): def test_yaml_syntax():
yaml.load(open("data/actionsmap/yunohost.yml")) yaml.safe_load(open("data/actionsmap/yunohost.yml"))

View file

@ -35,6 +35,7 @@ def find_expected_string_keys():
python_files = glob.glob("src/yunohost/*.py") python_files = glob.glob("src/yunohost/*.py")
python_files.extend(glob.glob("src/yunohost/utils/*.py")) python_files.extend(glob.glob("src/yunohost/utils/*.py"))
python_files.extend(glob.glob("src/yunohost/data_migrations/*.py")) python_files.extend(glob.glob("src/yunohost/data_migrations/*.py"))
python_files.extend(glob.glob("src/yunohost/authenticators/*.py"))
python_files.extend(glob.glob("data/hooks/diagnosis/*.py")) python_files.extend(glob.glob("data/hooks/diagnosis/*.py"))
python_files.append("bin/yunohost") python_files.append("bin/yunohost")
@ -108,7 +109,7 @@ def find_expected_string_keys():
yield m yield m
# Keys for the actionmap ... # Keys for the actionmap ...
for category in yaml.load(open("data/actionsmap/yunohost.yml")).values(): for category in yaml.safe_load(open("data/actionsmap/yunohost.yml")).values():
if "actions" not in category.keys(): if "actions" not in category.keys():
continue continue
for action in category["actions"].values(): for action in category["actions"].values():