Merge branch 'unstable' into tsig-sha256

This commit is contained in:
Laurent Peuch 2018-01-03 19:48:38 +01:00
commit 34433f07a4
17 changed files with 416 additions and 171 deletions

View file

@ -6,7 +6,7 @@
#
# If no argument provided, a standard directory will be use. /var/log/${app}
# You can provide a path with the directory only or with the logfile.
# /parentdir/logdir/
# /parentdir/logdir
# /parentdir/logdir/logfile.log
#
# It's possible to use this helper several times, each config will be added to the same logrotate config file.
@ -24,7 +24,7 @@ ynh_use_logrotate () {
if [ "$(echo ${1##*.})" == "log" ]; then # Keep only the extension to check if it's a logfile
logfile=$1 # In this case, focus logrotate on the logfile
else
logfile=$1/.log # Else, uses the directory and all logfile into it.
logfile=$1/*.log # Else, uses the directory and all logfile into it.
fi
else
logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log
@ -123,6 +123,9 @@ ynh_add_nginx_config () {
# To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable.
# Substitute in a nginx config file only if the variable is not empty
if test -n "${path_url:-}"; then
# path_url_slash_less is path_url, or a blank value if path_url is only '/'
path_url_slash_less=${path_url%/}
ynh_replace_string "__PATH__/" "$path_url_slash_less/" "$finalnginxconf"
ynh_replace_string "__PATH__" "$path_url" "$finalnginxconf"
fi
if test -n "${domain:-}"; then

View file

@ -73,7 +73,7 @@ ynh_mysql_drop_db() {
# | arg: db - the database name to dump
# | ret: the mysqldump output
ynh_mysql_dump_db() {
mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" "$1"
mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" --single-transaction --skip-dump-date "$1"
}
# Create a user

View file

@ -126,9 +126,6 @@ ynh_install_app_dependencies () {
version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file.
dep_app=${app//_/-} # Replace all '_' by '-'
if ynh_package_is_installed "${dep_app}-ynh-deps"; then
echo "A package named ${dep_app}-ynh-deps is already installed" >&2
else
cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build
Section: misc
Priority: optional
@ -143,7 +140,6 @@ EOF
|| ynh_die "Unable to install dependencies" # Install the fake package and its dependencies
rm /tmp/${dep_app}-ynh-deps.control
ynh_app_setting_set $app apt_dependencies $dependencies
fi
}
# Remove fake package and its dependencies

View file

@ -10,17 +10,50 @@ ynh_string_random() {
| sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p'
}
# Substitute/replace a string by another in a file
# Substitute/replace a string (or expression) by another in a file
#
# usage: ynh_replace_string match_string replace_string target_file
# | arg: match_string - String to be searched and replaced in the file
# | arg: replace_string - String that will replace matches
# | arg: target_file - File in which the string will be replaced.
#
# As this helper is based on sed command, regular expressions and
# references to sub-expressions can be used
# (see sed manual page for more information)
ynh_replace_string () {
delimit=@
match_string=${1//${delimit}/"\\${delimit}"} # Escape the delimiter if it's in the string.
replace_string=${2//${delimit}/"\\${delimit}"}
workfile=$3
local delimit=@
local match_string=$1
local replace_string=$2
local workfile=$3
# Escape the delimiter if it's in the string.
match_string=${match_string//${delimit}/"\\${delimit}"}
replace_string=${replace_string//${delimit}/"\\${delimit}"}
sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$workfile"
}
# Substitute/replace a special string by another in a file
#
# usage: ynh_replace_special_string match_string replace_string target_file
# | arg: match_string - String to be searched and replaced in the file
# | arg: replace_string - String that will replace matches
# | arg: target_file - File in which the string will be replaced.
#
# This helper will use ynh_replace_string, but as you can use special
# characters, you can't use some regular expressions and sub-expressions.
ynh_replace_special_string () {
local match_string=$1
local replace_string=$2
local workfile=$3
# Escape any backslash to preserve them as simple backslash.
match_string=${match_string//\\/"\\\\"}
replace_string=${replace_string//\\/"\\\\"}
# Escape the & character, who has a special function in sed.
match_string=${match_string//&/"\&"}
replace_string=${replace_string//&/"\&"}
ynh_replace_string "$match_string" "$replace_string" "$workfile"
}

View file

@ -59,6 +59,11 @@ ynh_restore_upgradebackup () {
# ynh_abort_if_errors
#
ynh_backup_before_upgrade () {
if [ ! -e "/etc/yunohost/apps/$app/scripts/backup" ]
then
echo "This app doesn't have any backup script." >&2
return
fi
backup_number=1
old_backup_number=2
app_bck=${app//_/-} # Replace all '_' by '-'

View file

@ -1,7 +1,7 @@
uPnP:
enabled: false
TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269]
UDP: [53]
TCP: [22, 25, 80, 443, 465, 587, 993, 5222, 5269]
UDP: []
ipv4:
TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269]
UDP: [53, 5353]

View file

@ -47,6 +47,7 @@ yunohost-api:
log: /var/log/yunohost/yunohost-api.log
yunohost-firewall:
status: service
need_lock: true
nslcd:
status: service
log: /var/log/syslog

45
debian/changelog vendored
View file

@ -1,3 +1,48 @@
yunohost (2.7.5) stable; urgency=low
(Bumping version number for stable release)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 02 Dec 2017 12:38:00 -0500
yunohost (2.7.4) testing; urgency=low
* [fix] Update acme-tiny as LE updated its ToS (#386)
* [fix] Fix helper for old apps without backup script (#388)
* [mod] Remove port 53 from UPnP (but keep it open on local network) (#362)
* [i18n] Improve French translation
Thanks to all contributors <3 ! (jibec, Moul, Maniack, Aleks)
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 28 Nov 2017 19:01:41 -0500
yunohost (2.7.3) testing; urgency=low
Major changes :
* [fix] Refactor/clean madness related to DynDNS (#353)
* [i18n] Improve french translation (#355)
* [fix] Use cryptorandom to generate password (#358)
* [enh] Support for single app upgrade from the webadmin (#359)
* [enh] Be able to give lock to son processes detached by systemctl (#367)
* [enh] Make MySQL dumps with a single transaction to ensure backup consistency (#370)
Misc fixes/improvements :
* [enh] Escape some special character in ynh_replace_string (#354)
* [fix] Allow dash at the beginning of app settings value (#357)
* [enh] Handle root path in nginx conf (#361)
* [enh] Add debugging in ldap init (#365)
* [fix] Fix app_upgrade_string with missing key
* [fix] Fix for change_url path normalizing with root url (#368)
* [fix] Missing 'ask_path' string (#369)
* [enh] Remove date from sql dump (#371)
* [fix] Fix unicode error in backup/restore (#375)
* [fix] Fix an error in ynh_replace_string (#379)
Thanks to all contributors <3 ! (Bram, Maniack C, ljf, JimboJoe, ariasuni, Jibec, Aleks)
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 12 Oct 2017 16:18:51 -0400
yunohost (2.7.2) stable; urgency=low
* [mod] pep8

View file

@ -55,6 +55,7 @@
"ask_main_domain": "Main domain",
"ask_new_admin_password": "New administration password",
"ask_password": "Password",
"ask_path": "Path",
"backup_abstract_method": "This backup method hasn't yet been implemented",
"backup_action_required": "You must specify something to save",
"backup_app_failed": "Unable to back up the app '{app:s}'",
@ -157,6 +158,7 @@
"domains_available": "Available domains:",
"done": "Done",
"downloading": "Downloading...",
"dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.",
"dyndns_cron_installed": "The DynDNS cron job has been installed",
"dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job",
"dyndns_cron_removed": "The DynDNS cron job has been removed",
@ -167,7 +169,8 @@
"dyndns_no_domain_registered": "No domain has been registered with DynDNS",
"dyndns_registered": "The DynDNS domain has been registered",
"dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}",
"dyndns_unavailable": "Unavailable DynDNS subdomain",
"dyndns_domain_not_provided": "Dyndns provider {provider:s} cannot provide domain {domain:s}.",
"dyndns_unavailable": "Domain {domain:s} is not available.",
"executing_command": "Executing command '{command:s}'...",
"executing_script": "Executing script '{script:s}'...",
"extracting": "Extracting...",

View file

@ -1,7 +1,7 @@
{
"action_invalid": "Action « {action:s} » incorrecte",
"admin_password": "Mot de passe d'administration",
"admin_password_change_failed": "Impossible de modifier le mot de passe d'administration",
"admin_password_change_failed": "Impossible de changer le mot de passe",
"admin_password_changed": "Le mot de passe d'administration a été modifié",
"app_already_installed": "{app:s} est déjà installé",
"app_argument_choice_invalid": "Choix invalide pour le paramètre « {name:s} », il doit être l'un de {choices:s}",
@ -14,7 +14,7 @@
"app_install_files_invalid": "Fichiers d'installation incorrects",
"app_location_already_used": "Une application est déjà installée à cet emplacement",
"app_location_install_failed": "Impossible d'installer l'application à cet emplacement",
"app_manifest_invalid": "Manifeste d'application incorrect",
"app_manifest_invalid": "Manifeste d'application incorrect : {error}",
"app_no_upgrade": "Aucune application à mettre à jour",
"app_not_correctly_installed": "{app:s} semble être mal installé",
"app_not_installed": "{app:s} n'est pas installé",
@ -98,7 +98,7 @@
"dyndns_no_domain_registered": "Aucun domaine n'a été enregistré avec DynDNS",
"dyndns_registered": "Le domaine DynDNS a été enregistré",
"dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {error:s}",
"dyndns_unavailable": "Sous-domaine DynDNS indisponible",
"dyndns_unavailable": "Le domaine {domain:s} est indisponible.",
"executing_command": "Exécution de la commande « {command:s} »...",
"executing_script": "Exécution du script « {script:s} »...",
"extracting": "Extraction...",
@ -320,7 +320,7 @@
"backup_archive_system_part_not_available": "La partie « {part:s} » du système nest pas disponible dans cette sauvegarde",
"backup_archive_mount_failed": "Le montage de larchive de sauvegarde a échoué",
"backup_archive_writing_error": "Impossible dajouter les fichiers à la sauvegarde dans larchive compressée",
"backup_ask_for_copying_if_needed": "Votre système ne prend pas complètement en charge la méthode rapide dorganisation des fichiers dans larchive, voulez-vous les organiser en copiant {size:s} Mio ?",
"backup_ask_for_copying_if_needed": "Certains fichiers nont pas pu être préparés pour être sauvegardée en utilisant la méthode qui évite de temporairement gaspiller de lespace sur le système. Pour mener la sauvegarde, {size:s} Mio doivent être temporairement utilisés. Acceptez-vous ?",
"backup_borg_not_implemented": "La méthode de sauvegarde Bord nest pas encore implémentée",
"backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de larchive décompressée",
"backup_copying_to_organize_the_archive": "Copie de {size:s} Mio pour organiser larchive",
@ -344,5 +344,28 @@
"restore_mounting_archive": "Montage de larchive dans « {path:s} »",
"restore_may_be_not_enough_disk_space": "Votre système semble ne pas avoir suffisamment despace disponible (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)",
"restore_not_enough_disk_space": "Espace disponible insuffisant (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)",
"restore_system_part_failed": "Impossible de restaurer la partie « {part:s} » du système"
"restore_system_part_failed": "Impossible de restaurer la partie « {part:s} » du système",
"backup_couldnt_bind": "Impossible de lier {src:s} avec {dest:s}.",
"domain_dns_conf_is_just_a_recommendation": "Cette page montre la configuration *recommandée*. Elle ne configure *pas* le DNS pour vous. Il est de votre responsabilité que de configurer votre zone DNS chez votre registrar DNS avec cette recommandation.",
"domain_dyndns_dynette_is_unreachable": "Impossible de contacter la dynette YunoHost, soit YunoHost nest pas correctement connecté à internet ou alors le serveur de dynette est arrêté. Erreur : {error}",
"migrations_backward": "Migration en arrière.",
"migrations_bad_value_for_target": "Nombre invalide pour le paramètre « target », les numéros de migration sont ou {}",
"migrations_cant_reach_migration_file": "Impossible daccéder aux fichiers de migrations avec le chemin %s",
"migrations_current_target": "La cible de migration est {}",
"migrations_error_failed_to_load_migration": "ERREUR : échec du chargement de migration {number} {name}",
"migrations_forward": "Migration en avant",
"migrations_loading_migration": "Chargement de la migration {number} {name}…",
"migrations_migration_has_failed": "La migration {number} {name} a échoué avec lexception {exception}, annulation",
"migrations_no_migrations_to_run": "Aucune migration à lancer",
"migrations_show_currently_running_migration": "Application de la migration {number} {name}…",
"migrations_show_last_migration": "La dernière migration appliquée est {}",
"migrations_skip_migration": "Omission de la migration {number} {name}…",
"server_shutdown": "Le serveur sera éteint",
"server_shutdown_confirm": "Le serveur immédiatement être éteint, le voulez-vous vraiment ? [{answers:s}]",
"server_reboot": "Le serveur va redémarrer",
"server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers:s}]",
"app_upgrade_some_app_failed": "Impossible de mettre à jour certaines applications",
"ask_path": "Chemin",
"dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.",
"dyndns_domain_not_provided": "Le fournisseur Dyndns {provider:s} ne peut pas fournir le domaine {domain:s}."
}

View file

@ -38,7 +38,7 @@ import pwd
import grp
from collections import OrderedDict
from moulinette import msignals, m18n
from moulinette import msignals, m18n, msettings
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
@ -445,8 +445,9 @@ def app_change_url(auth, app, domain, path):
# Normalize path and domain format
domain = domain.strip().lower()
old_path = '/' + old_path.strip("/").strip() + '/'
path = '/' + path.strip("/").strip() + '/'
old_path = normalize_url_path(old_path)
path = normalize_url_path(path)
if (domain, path) == (old_domain, old_path):
raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_identical_domains", domain=domain, path=path))
@ -531,6 +532,9 @@ def app_upgrade(auth, app=[], url=None, file=None):
"""
from yunohost.hook import hook_add, hook_remove, hook_exec
# Retrieve interface
is_api = msettings.get('interface') == 'api'
try:
app_list()
except MoulinetteError:
@ -632,6 +636,10 @@ def app_upgrade(auth, app=[], url=None, file=None):
logger.success(m18n.n('upgrade_complete'))
# Return API logs if it is an API call
if is_api:
return {"log": service_log('yunohost-api', number="100").values()[0]}
def app_install(auth, app, label=None, args=None, no_remove_on_failure=False):
"""
@ -2105,3 +2113,10 @@ def random_password(length=8):
char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase
return ''.join([random.SystemRandom().choice(char_set) for x in range(length)])
def normalize_url_path(url_path):
if url_path.strip("/").strip():
return '/' + url_path.strip("/").strip() + '/'
return "/"

View file

@ -166,11 +166,11 @@ class BackupRestoreTargetsManager(object):
or (exclude and isinstance(exclude, list) and not include)
if include:
return [target for target in self.targets[category]
return [target.encode("Utf-8") for target in self.targets[category]
if self.results[category][target] in include]
if exclude:
return [target for target in self.targets[category]
return [target.encode("Utf-8") for target in self.targets[category]
if self.results[category][target] not in exclude]

View file

@ -82,28 +82,23 @@ def domain_add(auth, domain, dyndns=False):
# DynDNS domain
if dyndns:
if len(domain.split('.')) < 3:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_invalid'))
# Do not allow to subscribe to multiple dyndns domains...
if os.path.exists('/etc/cron.d/yunohost-dyndns'):
raise MoulinetteError(errno.EPERM,
m18n.n('domain_dyndns_already_subscribed'))
try:
r = requests.get('https://dyndns.yunohost.org/domains', timeout=30)
except requests.ConnectionError as e:
raise MoulinetteError(errno.EHOSTUNREACH,
m18n.n('domain_dyndns_dynette_is_unreachable', error=str(e)))
from yunohost.dyndns import dyndns_subscribe
from yunohost.dyndns import dyndns_subscribe, _dyndns_provides
dyndomains = json.loads(r.text)
dyndomain = '.'.join(domain.split('.')[1:])
if dyndomain in dyndomains:
dyndns_subscribe(domain=domain)
else:
# Check that this domain can effectively be provided by
# dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st)
if not _dyndns_provides("dyndns.yunohost.org", domain):
raise MoulinetteError(errno.EINVAL,
m18n.n('domain_dyndns_root_unknown'))
# Actually subscribe
dyndns_subscribe(domain=domain)
try:
yunohost.certificate._certificate_install_selfsigned([domain], False)
@ -281,6 +276,25 @@ def get_public_ip(protocol=4):
raise MoulinetteError(errno.ENETUNREACH,
m18n.n('no_internet_connection'))
def get_public_ips():
"""
Retrieve the public IPv4 and v6 from ip. and ip6.yunohost.org
Returns a 2-tuple (ipv4, ipv6). ipv4 or ipv6 can be None if they were not
found.
"""
try:
ipv4 = get_public_ip()
except:
ipv4 = None
try:
ipv6 = get_public_ip(6)
except:
ipv6 = None
return (ipv4, ipv6)
def _get_maindomain():
with open('/etc/yunohost/current_host', 'r') as f:

View file

@ -36,45 +36,82 @@ import subprocess
from moulinette import m18n
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, write_to_file, rm
from moulinette.utils.network import download_json
from yunohost.domain import get_public_ip, _get_maindomain, _build_dns_conf
from yunohost.domain import get_public_ips, _get_maindomain, _build_dns_conf
logger = getActionLogger('yunohost.dyndns')
class IPRouteLine(object):
""" Utility class to parse an ip route output line
The output of ip ro is variable and hard to parse completly, it would
require a real parser, not just a regexp, so do minimal parsing here...
>>> a = IPRouteLine('2001:: from :: via fe80::c23f:fe:1e:cafe dev eth0 src 2000:de:beef:ca:0:fe:1e:cafe metric 0')
>>> a.src_addr
"2000:de:beef:ca:0:fe:1e:cafe"
"""
regexp = re.compile(
r'(?P<unreachable>unreachable)?.*src\s+(?P<src_addr>[0-9a-f:]+).*')
def __init__(self, line):
self.m = self.regexp.match(line)
if not self.m:
raise ValueError("Not a valid ip route get line")
# make regexp group available as object attributes
for k, v in self.m.groupdict().items():
setattr(self, k, v)
OLD_IPV4_FILE = '/etc/yunohost/dyndns/old_ip'
OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6'
DYNDNS_ZONE = '/etc/yunohost/dyndns/zone'
RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile(
r'.*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$'
)
RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile(
r'.*/K(?P<domain>[^\s\+]+)\.\+165.+\.private$'
)
def _dyndns_provides(provider, domain):
"""
Checks if a provider provide/manage a given domain.
Keyword arguments:
provider -- The url of the provider, e.g. "dyndns.yunohost.org"
domain -- The full domain that you'd like.. e.g. "foo.nohost.me"
Returns:
True if the provider provide/manages the domain. False otherwise.
"""
logger.debug("Checking if %s is managed by %s ..." % (domain, provider))
try:
# Dyndomains will be a list of domains supported by the provider
# e.g. [ "nohost.me", "noho.st" ]
dyndomains = download_json('https://%s/domains' % provider, timeout=30)
except MoulinetteError as e:
logger.error(str(e))
raise MoulinetteError(errno.EIO,
m18n.n('dyndns_could_not_check_provide',
domain=domain, provider=provider))
# Extract 'dyndomain' from 'domain', e.g. 'nohost.me' from 'foo.nohost.me'
dyndomain = '.'.join(domain.split('.')[1:])
return dyndomain in dyndomains
def _dyndns_available(provider, domain):
"""
Checks if a domain is available from a given provider.
Keyword arguments:
provider -- The url of the provider, e.g. "dyndns.yunohost.org"
domain -- The full domain that you'd like.. e.g. "foo.nohost.me"
Returns:
True if the domain is avaible, False otherwise.
"""
logger.debug("Checking if domain %s is available on %s ..."
% (domain, provider))
try:
r = download_json('https://%s/test/%s' % (provider, domain),
expected_status_code=None)
except MoulinetteError as e:
logger.error(str(e))
raise MoulinetteError(errno.EIO,
m18n.n('dyndns_could_not_check_available',
domain=domain, provider=provider))
return r == u"Domain %s is available" % domain
def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None):
"""
Subscribe to a DynDNS service
@ -88,12 +125,16 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None
if domain is None:
domain = _get_maindomain()
# Verify if domain is provided by subscribe_host
if not _dyndns_provides(subscribe_host, domain):
raise MoulinetteError(errno.ENOENT,
m18n.n('dyndns_domain_not_provided',
domain=domain, provider=subscribe_host))
# Verify if domain is available
try:
if requests.get('https://%s/test/%s' % (subscribe_host, domain), timeout=30).status_code != 200:
raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable'))
except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
if not _dyndns_available(subscribe_host, domain):
raise MoulinetteError(errno.ENOENT,
m18n.n('dyndns_unavailable', domain=domain))
if key is None:
if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0:
@ -141,75 +182,40 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
ipv6 -- IPv6 address to send
"""
# IPv4
# Get old ipv4/v6
old_ipv4, old_ipv6 = (None, None) # (default values)
if os.path.isfile(OLD_IPV4_FILE):
old_ipv4 = read_file(OLD_IPV4_FILE).rstrip()
if os.path.isfile(OLD_IPV6_FILE):
old_ipv6 = read_file(OLD_IPV6_FILE).rstrip()
# Get current IPv4 and IPv6
(ipv4_, ipv6_) = get_public_ips()
if ipv4 is None:
ipv4 = get_public_ip()
try:
with open('/etc/yunohost/dyndns/old_ip', 'r') as f:
old_ip = f.readline().rstrip()
except IOError:
old_ip = '0.0.0.0'
# IPv6
if ipv6 is None:
try:
ip_route_out = subprocess.check_output(
['ip', 'route', 'get', '2000::']).split('\n')
if len(ip_route_out) > 0:
route = IPRouteLine(ip_route_out[0])
if not route.unreachable:
ipv6 = route.src_addr
except (OSError, ValueError) as e:
# Unlikely case "ip route" does not return status 0
# or produces unexpected output
raise MoulinetteError(errno.EBADMSG,
"ip route cmd error : {}".format(e))
ipv4 = ipv4_
if ipv6 is None:
logger.info(m18n.n('no_ipv6_connectivity'))
ipv6 = ipv6_
try:
with open('/etc/yunohost/dyndns/old_ipv6', 'r') as f:
old_ipv6 = f.readline().rstrip()
except IOError:
old_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000'
logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6))
logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6))
# no need to update
if old_ip == ipv4 and old_ipv6 == ipv6:
if old_ipv4 == ipv4 and old_ipv6 == ipv6:
logger.info("No updated needed.")
return
else:
logger.info("Updated needed, going on...")
# If domain is not given, try to guess it from keys available...
if domain is None:
# Retrieve the first registered domain
for path in glob.iglob('/etc/yunohost/dyndns/K*.private'):
match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path)
if not match:
match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path)
if not match:
continue
_domain = match.group('domain')
try:
# Check if domain is registered
request_url = 'https://{0}/test/{1}'.format(dyn_host, _domain)
if requests.get(request_url, timeout=30).status_code == 200:
continue
except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH,
m18n.n('no_internet_connection'))
except requests.exceptions.Timeout:
logger.warning("Correction timed out on {}, skip it".format(
request_url))
domain = _domain
key = path
break
if not domain:
raise MoulinetteError(errno.EINVAL,
m18n.n('dyndns_no_domain_registered'))
if key is None:
(domain, key) = _guess_current_dyndns_domain(dyn_host)
# If key is not given, pick the first file we find with the domain given
elif key is None:
keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain))
if not keys:
@ -221,9 +227,12 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
if "+157" in key:
key = _migrate_from_md5_tsig_to_sha512_tsig(key, domain, dyn_host)
# Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me'
host = domain.split('.')[1:]
host = '.'.join(host)
logger.debug("Building zone update file ...")
lines = [
'server %s' % dyn_host,
'zone %s' % host,
@ -260,21 +269,27 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
'send'
]
with open('/etc/yunohost/dyndns/zone', 'w') as zone:
zone.write('\n'.join(lines))
# Write the actions to do to update to a file, to be able to pass it
# to nsupdate as argument
write_to_file(DYNDNS_ZONE, '\n'.join(lines))
if os.system('/usr/bin/nsupdate -k %s /etc/yunohost/dyndns/zone' % key) != 0:
os.system('rm -f /etc/yunohost/dyndns/old_ip')
os.system('rm -f /etc/yunohost/dyndns/old_ipv6')
logger.info("Now pushing new conf to DynDNS host...")
try:
command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE]
subprocess.check_call(command)
except subprocess.CalledProcessError:
rm(OLD_IPV4_FILE, force=True) # Remove file (ignore if non-existent)
rm(OLD_IPV6_FILE, force=True) # Remove file (ignore if non-existent)
raise MoulinetteError(errno.EPERM,
m18n.n('dyndns_ip_update_failed'))
logger.success(m18n.n('dyndns_ip_updated'))
with open('/etc/yunohost/dyndns/old_ip', 'w') as f:
f.write(ipv4)
if ipv4 is not None:
write_to_file(OLD_IPV4_FILE, ipv4)
if ipv6 is not None:
with open('/etc/yunohost/dyndns/old_ipv6', 'w') as f:
f.write(ipv6)
write_to_file(OLD_IPV6_FILE, ipv6)
def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host):
@ -356,3 +371,32 @@ def dyndns_removecron():
raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed'))
logger.success(m18n.n('dyndns_cron_removed'))
def _guess_current_dyndns_domain(dyn_host):
"""
This function tries to guess which domain should be updated by
"dyndns_update()" because there's not proper management of the current
dyndns domain :/ (and at the moment the code doesn't support having several
dyndns domain, which is sort of a feature so that people don't abuse the
dynette...)
"""
# Retrieve the first registered domain
for path in glob.iglob('/etc/yunohost/dyndns/K*.private'):
match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path)
if not match:
match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path)
if not match:
continue
_domain = match.group('domain')
# Verify if domain is registered (i.e., if it's available, skip
# current domain beause that's not the one we want to update..)
if _dyndns_available(dyn_host, _domain):
continue
else:
return (_domain, path)
raise MoulinetteError(errno.EINVAL,
m18n.n('dyndns_no_domain_registered'))

View file

@ -39,10 +39,10 @@ from moulinette.utils import log, filesystem
from yunohost.hook import hook_callback
BASE_CONF_PATH = '/home/yunohost.conf'
BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup')
PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, 'pending')
MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock"
logger = log.getActionLogger('yunohost.service')
@ -493,7 +493,8 @@ def _run_service_command(action, service):
service -- Service name
"""
if service not in _get_services().keys():
services = _get_services()
if service not in services.keys():
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=service))
cmd = None
@ -505,8 +506,23 @@ def _run_service_command(action, service):
else:
raise ValueError("Unknown action '%s'" % action)
need_lock = (services[service].get('need_lock') or False) \
and action in ['start', 'stop', 'restart', 'reload']
try:
ret = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
# Launch the command
logger.debug("Running '%s'" % cmd)
p = subprocess.Popen(cmd.split(), stderr=subprocess.STDOUT)
# If this command needs a lock (because the service uses yunohost
# commands inside), find the PID and add a lock for it
if need_lock:
PID = _give_lock(action, service, p)
# Wait for the command to complete
p.communicate()
# Remove the lock if one was given
if need_lock and PID != 0:
_remove_lock(PID)
except subprocess.CalledProcessError as e:
# TODO: Log output?
logger.warning(m18n.n('service_cmd_exec_failed', command=' '.join(e.cmd)))
@ -514,6 +530,41 @@ def _run_service_command(action, service):
return True
def _give_lock(action, service, p):
# Depending of the action, systemctl calls the PID differently :/
if action == "start" or action == "restart":
systemctl_PID_name = "MainPID"
else:
systemctl_PID_name = "ControlPID"
cmd_get_son_PID ="systemctl show %s -p %s" % (service, systemctl_PID_name)
son_PID = 0
# As long as we did not found the PID and that the command is still running
while son_PID == 0 and p.poll() == None:
# Call systemctl to get the PID
# Output of the command is e.g. ControlPID=1234
son_PID = subprocess.check_output(cmd_get_son_PID.split()) \
.strip().split("=")[1]
son_PID = int(son_PID)
time.sleep(1)
# If we found a PID
if son_PID != 0:
# Append the PID to the lock file
logger.debug("Giving a lock to PID %s for service %s !"
% (str(son_PID), service))
filesystem.append_to_file(MOULINETTE_LOCK, "\n%s" % str(son_PID))
return son_PID
def _remove_lock(PID_to_remove):
PIDs = filesystem.read_file(MOULINETTE_LOCK).split("\n")
PIDs_to_keep = [ PID for PID in PIDs if int(PID) != PID_to_remove ]
filesystem.write_to_file(MOULINETTE_LOCK, '\n'.join(PIDs_to_keep))
def _get_services():
"""
Get a dict of managed services with their parameters

View file

@ -45,7 +45,7 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_json, write_to_json
from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron
from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain
from yunohost.dyndns import dyndns_subscribe
from yunohost.dyndns import _dyndns_available, _dyndns_provides
from yunohost.firewall import firewall_upnp
from yunohost.service import service_status, service_regen_conf, service_log
from yunohost.monitor import monitor_disk, monitor_system
@ -253,27 +253,39 @@ def tools_postinstall(domain, password, ignore_dyndns=False):
password -- YunoHost admin password
"""
dyndns = not ignore_dyndns
dyndns_provider = "dyndns.yunohost.org"
# Do some checks at first
if os.path.isfile('/etc/yunohost/installed'):
raise MoulinetteError(errno.EPERM,
m18n.n('yunohost_already_installed'))
if len(domain.split('.')) >= 3 and not ignore_dyndns:
try:
r = requests.get('https://dyndns.yunohost.org/domains')
except requests.ConnectionError:
pass
else:
dyndomains = json.loads(r.text)
dyndomain = '.'.join(domain.split('.')[1:])
if dyndomain in dyndomains:
if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200:
if not ignore_dyndns:
# Check if yunohost dyndns can handle the given domain
# (i.e. is it a .nohost.me ? a .noho.st ?)
try:
is_nohostme_or_nohost = _dyndns_provides(dyndns_provider, domain)
# If an exception is thrown, most likely we don't have internet
# connectivity or something. Assume that this domain isn't manageable
# and inform the user that we could not contact the dyndns host server.
except:
logger.warning(m18n.n('dyndns_provider_unreachable',
provider=dyndns_provider))
is_nohostme_or_nohost = False
# If this is a nohost.me/noho.st, actually check for availability
if is_nohostme_or_nohost:
# (Except if the user explicitly said he/she doesn't care about dyndns)
if ignore_dyndns:
dyndns = False
# Check if the domain is available...
elif _dyndns_available(dyndns_provider, domain):
dyndns = True
# If not, abort the postinstall
else:
raise MoulinetteError(errno.EEXIST,
m18n.n('dyndns_unavailable'))
m18n.n('dyndns_unavailable',
domain=domain))
else:
dyndns = False
else:

View file

@ -82,7 +82,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA):
if proc.returncode != 0:
raise IOError("Error loading {0}: {1}".format(csr, err))
domains = set([])
common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8'))
common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8'))
if common_name is not None:
domains.add(common_name.group(1))
subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL)
@ -95,7 +95,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA):
log.info("Registering account...")
code, result = _send_signed_request(CA + "/acme/new-reg", {
"resource": "new-reg",
"agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf",
"agreement": json.loads(urlopen(CA + "/directory").read().decode('utf8'))['meta']['terms-of-service'],
})
if code == 201:
log.info("Registered!")