mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Merge pull request #653 from YunoHost/decouple-regenconf-from-services
[enh] Decouple the regen-conf mechanism from services
This commit is contained in:
commit
d36c09120f
14 changed files with 690 additions and 522 deletions
|
@ -1623,6 +1623,32 @@ tools:
|
||||||
full: --force
|
full: --force
|
||||||
action: store_true
|
action: store_true
|
||||||
|
|
||||||
|
### tools_regen_conf()
|
||||||
|
regen-conf:
|
||||||
|
action_help: Regenerate the configuration file(s)
|
||||||
|
api: PUT /tools/regenconf
|
||||||
|
arguments:
|
||||||
|
names:
|
||||||
|
help: Categories to regenerate configuration of (all by default)
|
||||||
|
nargs: "*"
|
||||||
|
metavar: NAME
|
||||||
|
-d:
|
||||||
|
full: --with-diff
|
||||||
|
help: Show differences in case of configuration changes
|
||||||
|
action: store_true
|
||||||
|
-f:
|
||||||
|
full: --force
|
||||||
|
help: Override all manual modifications in configuration files
|
||||||
|
action: store_true
|
||||||
|
-n:
|
||||||
|
full: --dry-run
|
||||||
|
help: Show what would have been regenerated
|
||||||
|
action: store_true
|
||||||
|
-p:
|
||||||
|
full: --list-pending
|
||||||
|
help: List pending configuration files and exit
|
||||||
|
action: store_true
|
||||||
|
|
||||||
subcategories:
|
subcategories:
|
||||||
|
|
||||||
migrations:
|
migrations:
|
||||||
|
|
|
@ -20,8 +20,6 @@ mysql:
|
||||||
glances: {}
|
glances: {}
|
||||||
ssh:
|
ssh:
|
||||||
log: /var/log/auth.log
|
log: /var/log/auth.log
|
||||||
ssl:
|
|
||||||
status: null
|
|
||||||
metronome:
|
metronome:
|
||||||
log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err]
|
log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err]
|
||||||
slapd:
|
slapd:
|
||||||
|
@ -34,10 +32,9 @@ yunohost-firewall:
|
||||||
need_lock: true
|
need_lock: true
|
||||||
nslcd:
|
nslcd:
|
||||||
log: /var/log/syslog
|
log: /var/log/syslog
|
||||||
nsswitch:
|
nsswitch: null
|
||||||
status: null
|
ssl: null
|
||||||
yunohost:
|
yunohost: null
|
||||||
status: null
|
|
||||||
bind9: null
|
bind9: null
|
||||||
tahoe-lafs: null
|
tahoe-lafs: null
|
||||||
memcached: null
|
memcached: null
|
||||||
|
|
2
debian/postinst
vendored
2
debian/postinst
vendored
|
@ -12,7 +12,7 @@ do_configure() {
|
||||||
bash /usr/share/yunohost/hooks/conf_regen/15-nginx init
|
bash /usr/share/yunohost/hooks/conf_regen/15-nginx init
|
||||||
else
|
else
|
||||||
echo "Regenerating configuration, this might take a while..."
|
echo "Regenerating configuration, this might take a while..."
|
||||||
yunohost service regen-conf --output-as none
|
yunohost tools regen-conf --output-as none
|
||||||
|
|
||||||
echo "Launching migrations.."
|
echo "Launching migrations.."
|
||||||
yunohost tools migrations migrate --auto
|
yunohost tools migrations migrate --auto
|
||||||
|
|
|
@ -262,7 +262,7 @@
|
||||||
"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_service_enable": "Enable '{}' service",
|
"log_service_enable": "Enable '{}' service",
|
||||||
"log_service_regen_conf": "Regenerate system configurations '{}'",
|
"log_regen_conf": "Regenerate system configurations '{}'",
|
||||||
"log_user_create": "Add '{}' user",
|
"log_user_create": "Add '{}' user",
|
||||||
"log_user_delete": "Delete '{}' user",
|
"log_user_delete": "Delete '{}' user",
|
||||||
"log_user_update": "Update information of '{}' user",
|
"log_user_update": "Update information of '{}' user",
|
||||||
|
@ -299,6 +299,7 @@
|
||||||
"migration_description_0006_sync_admin_and_root_passwords": "Synchronize admin and root passwords",
|
"migration_description_0006_sync_admin_and_root_passwords": "Synchronize admin and root passwords",
|
||||||
"migration_description_0007_ssh_conf_managed_by_yunohost_step1": "Let the SSH configuration be managed by YunoHost (step 1, automatic)",
|
"migration_description_0007_ssh_conf_managed_by_yunohost_step1": "Let the SSH configuration be managed by YunoHost (step 1, automatic)",
|
||||||
"migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Let the SSH configuration be managed by YunoHost (step 2, manual)",
|
"migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Let the SSH configuration be managed by YunoHost (step 2, manual)",
|
||||||
|
"migration_description_0009_decouple_regenconf_from_services": "Decouple the regen-conf mechanism from services",
|
||||||
"migration_0003_backward_impossible": "The stretch migration cannot be reverted.",
|
"migration_0003_backward_impossible": "The stretch migration cannot be reverted.",
|
||||||
"migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.",
|
"migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.",
|
||||||
"migration_0003_patching_sources_list": "Patching the sources.lists…",
|
"migration_0003_patching_sources_list": "Patching the sources.lists…",
|
||||||
|
@ -324,6 +325,7 @@
|
||||||
"migration_0008_dsa": " - the DSA key will be disabled. Hence, you might need to invalidate a spooky warning from your SSH client, and recheck the fingerprint of your server;",
|
"migration_0008_dsa": " - the DSA key will be disabled. Hence, you might need to invalidate a spooky warning from your SSH client, and recheck the fingerprint of your server;",
|
||||||
"migration_0008_warning": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.",
|
"migration_0008_warning": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.",
|
||||||
"migration_0008_no_warning": "No major risk has been indentified about overriding your SSH configuration - but we can't be absolutely sure ;)! If you agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.",
|
"migration_0008_no_warning": "No major risk has been indentified about overriding your SSH configuration - but we can't be absolutely sure ;)! If you agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.",
|
||||||
|
"migration_0009_not_needed": "This migration already happened somehow ? Skipping.",
|
||||||
"migrations_backward": "Migrating backward.",
|
"migrations_backward": "Migrating backward.",
|
||||||
"migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}",
|
"migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}",
|
||||||
"migrations_cant_reach_migration_file": "Can't access migrations files at path %s",
|
"migrations_cant_reach_migration_file": "Can't access migrations files at path %s",
|
||||||
|
@ -391,6 +393,21 @@
|
||||||
"port_available": "Port {port:d} is available",
|
"port_available": "Port {port:d} is available",
|
||||||
"port_unavailable": "Port {port:d} is not available",
|
"port_unavailable": "Port {port:d} is not available",
|
||||||
"recommend_to_add_first_user": "The post-install is finished but YunoHost needs at least one user to work correctly, you should add one using 'yunohost user create' or the admin interface.",
|
"recommend_to_add_first_user": "The post-install is finished but YunoHost needs at least one user to work correctly, you should add one using 'yunohost user create' or the admin interface.",
|
||||||
|
"regenconf_file_backed_up": "The configuration file '{conf}' has been backed up to '{backup}'",
|
||||||
|
"regenconf_file_copy_failed": "Unable to copy the new configuration file '{new}' to '{conf}'",
|
||||||
|
"regenconf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by regen-conf (category {category}) but has been kept back.",
|
||||||
|
"regenconf_file_manually_modified": "The configuration file '{conf}' has been manually modified and will not be updated",
|
||||||
|
"regenconf_file_manually_removed": "The configuration file '{conf}' has been manually removed and will not be created",
|
||||||
|
"regenconf_file_remove_failed": "Unable to remove the configuration file '{conf}'",
|
||||||
|
"regenconf_file_removed": "The configuration file '{conf}' has been removed",
|
||||||
|
"regenconf_file_updated": "The configuration file '{conf}' has been updated",
|
||||||
|
"regenconf_now_managed_by_yunohost": "The configuration file '{conf}' is now managed by YunoHost (category {category}).",
|
||||||
|
"regenconf_up_to_date": "The configuration is already up-to-date for category '{category}'",
|
||||||
|
"regenconf_updated": "The configuration has been updated for category '{category}'",
|
||||||
|
"regenconf_would_be_updated": "The configuration would have been updated for category '{category}'",
|
||||||
|
"regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for category '{category}'…",
|
||||||
|
"regenconf_failed": "Unable to regenerate the configuration for category(s): {categories}",
|
||||||
|
"regenconf_pending_applying": "Applying pending configuration for category '{category}'…",
|
||||||
"restore_action_required": "You must specify something to restore",
|
"restore_action_required": "You must specify something to restore",
|
||||||
"restore_already_installed_app": "An app is already installed with the id '{app:s}'",
|
"restore_already_installed_app": "An app is already installed with the id '{app:s}'",
|
||||||
"restore_app_failed": "Unable to restore the app '{app:s}'",
|
"restore_app_failed": "Unable to restore the app '{app:s}'",
|
||||||
|
@ -419,18 +436,6 @@
|
||||||
"service_already_started": "Service '{service:s}' has already been started",
|
"service_already_started": "Service '{service:s}' has already been started",
|
||||||
"service_already_stopped": "Service '{service:s}' has already been stopped",
|
"service_already_stopped": "Service '{service:s}' has already been stopped",
|
||||||
"service_cmd_exec_failed": "Unable to execute command '{command:s}'",
|
"service_cmd_exec_failed": "Unable to execute command '{command:s}'",
|
||||||
"service_conf_file_backed_up": "The configuration file '{conf}' has been backed up to '{backup}'",
|
|
||||||
"service_conf_file_copy_failed": "Unable to copy the new configuration file '{new}' to '{conf}'",
|
|
||||||
"service_conf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by service {service} but has been kept back.",
|
|
||||||
"service_conf_file_manually_modified": "The configuration file '{conf}' has been manually modified and will not be updated",
|
|
||||||
"service_conf_file_manually_removed": "The configuration file '{conf}' has been manually removed and will not be created",
|
|
||||||
"service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'",
|
|
||||||
"service_conf_file_removed": "The configuration file '{conf}' has been removed",
|
|
||||||
"service_conf_file_updated": "The configuration file '{conf}' has been updated",
|
|
||||||
"service_conf_now_managed_by_yunohost": "The configuration file '{conf}' is now managed by YunoHost.",
|
|
||||||
"service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'",
|
|
||||||
"service_conf_updated": "The configuration has been updated for service '{service}'",
|
|
||||||
"service_conf_would_be_updated": "The configuration would have been updated for service '{service}'",
|
|
||||||
"service_description_avahi-daemon": "allows to reach your server using yunohost.local on your local network",
|
"service_description_avahi-daemon": "allows to reach your server using yunohost.local on 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 client to access/fetch email (via IMAP and POP3)",
|
"service_description_dovecot": "allows e-mail client to access/fetch email (via IMAP and POP3)",
|
||||||
|
@ -454,9 +459,7 @@
|
||||||
"service_enable_failed": "Unable to enable service '{service:s}'\n\nRecent service logs:{logs:s}",
|
"service_enable_failed": "Unable to enable service '{service:s}'\n\nRecent service logs:{logs:s}",
|
||||||
"service_enabled": "The service '{service:s}' has been enabled",
|
"service_enabled": "The service '{service:s}' has been enabled",
|
||||||
"service_no_log": "No log to display for service '{service:s}'",
|
"service_no_log": "No log to display for service '{service:s}'",
|
||||||
"service_regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for service '{service}'…",
|
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' is deprecated! Please use 'yunohost tools regen-conf' instead.",
|
||||||
"service_regenconf_failed": "Unable to regenerate the configuration for service(s): {services}",
|
|
||||||
"service_regenconf_pending_applying": "Applying pending configuration for service '{service}'…",
|
|
||||||
"service_remove_failed": "Unable to remove service '{service:s}'",
|
"service_remove_failed": "Unable to remove service '{service:s}'",
|
||||||
"service_removed": "The service '{service:s}' has been removed",
|
"service_removed": "The service '{service:s}' has been removed",
|
||||||
"service_reload_failed": "Unable to reload service '{service:s}'\n\nRecent service logs:{logs:s}",
|
"service_reload_failed": "Unable to reload service '{service:s}'\n\nRecent service logs:{logs:s}",
|
||||||
|
|
|
@ -50,7 +50,7 @@ from yunohost.hook import (
|
||||||
)
|
)
|
||||||
from yunohost.monitor import binary_to_human
|
from yunohost.monitor import binary_to_human
|
||||||
from yunohost.tools import tools_postinstall
|
from yunohost.tools import tools_postinstall
|
||||||
from yunohost.service import service_regen_conf
|
from yunohost.regenconf import regen_conf
|
||||||
from yunohost.log import OperationLogger
|
from yunohost.log import OperationLogger
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
@ -1212,7 +1212,7 @@ class RestoreManager():
|
||||||
else:
|
else:
|
||||||
operation_logger.success()
|
operation_logger.success()
|
||||||
|
|
||||||
service_regen_conf()
|
regen_conf()
|
||||||
|
|
||||||
def _restore_apps(self):
|
def _restore_apps(self):
|
||||||
"""Restore all apps targeted"""
|
"""Restore all apps targeted"""
|
||||||
|
|
|
@ -43,7 +43,8 @@ from yunohost.utils.network import get_public_ip
|
||||||
|
|
||||||
from moulinette import m18n
|
from moulinette import m18n
|
||||||
from yunohost.app import app_ssowatconf
|
from yunohost.app import app_ssowatconf
|
||||||
from yunohost.service import _run_service_command, service_regen_conf
|
from yunohost.service import _run_service_command
|
||||||
|
from yunohost.regenconf import regen_conf
|
||||||
from yunohost.log import OperationLogger
|
from yunohost.log import OperationLogger
|
||||||
|
|
||||||
logger = getActionLogger('yunohost.certmanager')
|
logger = getActionLogger('yunohost.certmanager')
|
||||||
|
@ -806,7 +807,7 @@ def _enable_certificate(domain, new_cert_folder):
|
||||||
if os.path.isfile('/etc/yunohost/installed'):
|
if os.path.isfile('/etc/yunohost/installed'):
|
||||||
# regen nginx conf to be sure it integrates OCSP Stapling
|
# regen nginx conf to be sure it integrates OCSP Stapling
|
||||||
# (We don't do this yet if postinstall is not finished yet)
|
# (We don't do this yet if postinstall is not finished yet)
|
||||||
service_regen_conf(names=['nginx'])
|
regen_conf(names=['nginx'])
|
||||||
|
|
||||||
_run_service_command("reload", "nginx")
|
_run_service_command("reload", "nginx")
|
||||||
|
|
||||||
|
@ -924,7 +925,7 @@ def _regen_dnsmasq_if_needed():
|
||||||
break
|
break
|
||||||
|
|
||||||
if do_regen:
|
if do_regen:
|
||||||
service_regen_conf(["dnsmasq"])
|
regen_conf(["dnsmasq"])
|
||||||
|
|
||||||
|
|
||||||
def _name_self_CA():
|
def _name_self_CA():
|
||||||
|
|
|
@ -10,9 +10,9 @@ from moulinette.utils.filesystem import read_file
|
||||||
|
|
||||||
from yunohost.tools import Migration
|
from yunohost.tools import Migration
|
||||||
from yunohost.app import unstable_apps
|
from yunohost.app import unstable_apps
|
||||||
from yunohost.service import (_run_service_command,
|
from yunohost.service import _run_service_command
|
||||||
manually_modified_files,
|
from yunohost.regenconf import (manually_modified_files,
|
||||||
manually_modified_files_compared_to_debian_default)
|
manually_modified_files_compared_to_debian_default)
|
||||||
from yunohost.utils.filesystem import free_space_in_directory
|
from yunohost.utils.filesystem import free_space_in_directory
|
||||||
from yunohost.utils.packages import get_installed_version
|
from yunohost.utils.packages import get_installed_version
|
||||||
from yunohost.utils.network import get_network_interfaces
|
from yunohost.utils.network import get_network_interfaces
|
||||||
|
|
|
@ -3,15 +3,12 @@ import re
|
||||||
|
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
from moulinette import m18n
|
|
||||||
from moulinette.utils.log import getActionLogger
|
from moulinette.utils.log import getActionLogger
|
||||||
from moulinette.utils.filesystem import mkdir, rm
|
from moulinette.utils.filesystem import mkdir, rm
|
||||||
|
|
||||||
from yunohost.tools import Migration
|
from yunohost.tools import Migration
|
||||||
from yunohost.service import service_regen_conf, \
|
from yunohost.service import _run_service_command
|
||||||
_get_conf_hashes, \
|
from yunohost.regenconf import regen_conf
|
||||||
_calculate_hash, \
|
|
||||||
_run_service_command
|
|
||||||
from yunohost.settings import settings_set
|
from yunohost.settings import settings_set
|
||||||
from yunohost.utils.error import YunohostError
|
from yunohost.utils.error import YunohostError
|
||||||
|
|
||||||
|
@ -60,7 +57,7 @@ class MyMigration(Migration):
|
||||||
if os.path.exists('/etc/yunohost/from_script'):
|
if os.path.exists('/etc/yunohost/from_script'):
|
||||||
rm('/etc/yunohost/from_script')
|
rm('/etc/yunohost/from_script')
|
||||||
copyfile(SSHD_CONF, '/etc/ssh/sshd_config.bkp')
|
copyfile(SSHD_CONF, '/etc/ssh/sshd_config.bkp')
|
||||||
service_regen_conf(names=['ssh'], force=True)
|
regen_conf(names=['ssh'], force=True)
|
||||||
copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF)
|
copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF)
|
||||||
|
|
||||||
# Restart ssh and backward if it fail
|
# Restart ssh and backward if it fail
|
||||||
|
|
|
@ -6,9 +6,8 @@ from moulinette.utils.log import getActionLogger
|
||||||
from moulinette.utils.filesystem import chown
|
from moulinette.utils.filesystem import chown
|
||||||
|
|
||||||
from yunohost.tools import Migration
|
from yunohost.tools import Migration
|
||||||
from yunohost.service import service_regen_conf, \
|
from yunohost.regenconf import _get_conf_hashes, _calculate_hash
|
||||||
_get_conf_hashes, \
|
from yunohost.regenconf import regen_conf
|
||||||
_calculate_hash
|
|
||||||
from yunohost.settings import settings_set, settings_get
|
from yunohost.settings import settings_set, settings_get
|
||||||
from yunohost.utils.error import YunohostError
|
from yunohost.utils.error import YunohostError
|
||||||
from yunohost.backup import ARCHIVES_PATH
|
from yunohost.backup import ARCHIVES_PATH
|
||||||
|
@ -36,7 +35,7 @@ class MyMigration(Migration):
|
||||||
|
|
||||||
def migrate(self):
|
def migrate(self):
|
||||||
settings_set("service.ssh.allow_deprecated_dsa_hostkey", False)
|
settings_set("service.ssh.allow_deprecated_dsa_hostkey", False)
|
||||||
service_regen_conf(names=['ssh'], force=True)
|
regen_conf(names=['ssh'], force=True)
|
||||||
|
|
||||||
# Update local archives folder permissions, so that
|
# Update local archives folder permissions, so that
|
||||||
# admin can scp archives out of the server
|
# admin can scp archives out of the server
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from moulinette import m18n
|
||||||
|
from moulinette.utils.log import getActionLogger
|
||||||
|
|
||||||
|
from moulinette.utils.filesystem import read_file
|
||||||
|
from yunohost.service import _get_services, _save_services
|
||||||
|
from yunohost.regenconf import _update_conf_hashes, REGEN_CONF_FILE
|
||||||
|
|
||||||
|
from yunohost.tools import Migration
|
||||||
|
|
||||||
|
logger = getActionLogger('yunohost.migration')
|
||||||
|
|
||||||
|
|
||||||
|
class MyMigration(Migration):
|
||||||
|
"""
|
||||||
|
Decouple the regen conf mechanism from the concept of services
|
||||||
|
"""
|
||||||
|
|
||||||
|
def migrate(self):
|
||||||
|
|
||||||
|
if "conffiles" not in read_file("/etc/yunohost/services.yml") \
|
||||||
|
or os.path.exists(REGEN_CONF_FILE):
|
||||||
|
logger.warning(m18n.n("migration_0009_not_needed"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# For all services
|
||||||
|
services = _get_services()
|
||||||
|
for service, infos in services.items():
|
||||||
|
# If there are some conffiles (file hashes)
|
||||||
|
if "conffiles" in infos.keys():
|
||||||
|
# Save them using the new regen conf thingy
|
||||||
|
_update_conf_hashes(service, infos["conffiles"])
|
||||||
|
# And delete the old conffile key from the service infos
|
||||||
|
del services[service]["conffiles"]
|
||||||
|
|
||||||
|
# (Actually save the modification of services)
|
||||||
|
_save_services(services)
|
||||||
|
|
||||||
|
def backward(self):
|
||||||
|
|
||||||
|
pass
|
|
@ -34,7 +34,7 @@ from moulinette.utils.log import getActionLogger
|
||||||
|
|
||||||
import yunohost.certificate
|
import yunohost.certificate
|
||||||
|
|
||||||
from yunohost.service import service_regen_conf
|
from yunohost.regenconf import regen_conf
|
||||||
from yunohost.utils.network import get_public_ip
|
from yunohost.utils.network import get_public_ip
|
||||||
from yunohost.log import is_unit_operation
|
from yunohost.log import is_unit_operation
|
||||||
from yunohost.hook import hook_callback
|
from yunohost.hook import hook_callback
|
||||||
|
@ -112,7 +112,7 @@ def domain_add(operation_logger, auth, domain, dyndns=False):
|
||||||
|
|
||||||
# Don't regen these conf if we're still in postinstall
|
# Don't regen these conf if we're still in postinstall
|
||||||
if os.path.exists('/etc/yunohost/installed'):
|
if os.path.exists('/etc/yunohost/installed'):
|
||||||
service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix', 'rspamd'])
|
regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix', 'rspamd'])
|
||||||
app_ssowatconf(auth)
|
app_ssowatconf(auth)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -165,7 +165,7 @@ def domain_remove(operation_logger, auth, domain, force=False):
|
||||||
else:
|
else:
|
||||||
raise YunohostError('domain_deletion_failed')
|
raise YunohostError('domain_deletion_failed')
|
||||||
|
|
||||||
service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix'])
|
regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix'])
|
||||||
app_ssowatconf(auth)
|
app_ssowatconf(auth)
|
||||||
|
|
||||||
hook_callback('post_domain_remove', args=[domain])
|
hook_callback('post_domain_remove', args=[domain])
|
||||||
|
|
555
src/yunohost/regenconf.py
Normal file
555
src/yunohost/regenconf.py
Normal file
|
@ -0,0 +1,555 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
""" License
|
||||||
|
|
||||||
|
Copyright (C) 2019 YunoHost
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program; if not, see http://www.gnu.org/licenses
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from difflib import unified_diff
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from moulinette import m18n
|
||||||
|
from moulinette.utils import log, filesystem
|
||||||
|
from moulinette.utils.filesystem import read_file
|
||||||
|
|
||||||
|
from yunohost.utils.error import YunohostError
|
||||||
|
from yunohost.log import is_unit_operation
|
||||||
|
from yunohost.hook import hook_callback, hook_list
|
||||||
|
|
||||||
|
BASE_CONF_PATH = '/home/yunohost.conf'
|
||||||
|
BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup')
|
||||||
|
PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, 'pending')
|
||||||
|
REGEN_CONF_FILE = '/etc/yunohost/regenconf.yml'
|
||||||
|
|
||||||
|
logger = log.getActionLogger('yunohost.regenconf')
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME : those ain't just services anymore ... what are we supposed to do with this ...
|
||||||
|
# FIXME : check for all reference of 'service' close to operation_logger stuff
|
||||||
|
@is_unit_operation([('names', 'configuration')])
|
||||||
|
def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False,
|
||||||
|
list_pending=False):
|
||||||
|
"""
|
||||||
|
Regenerate the configuration file(s)
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
names -- Categories to regenerate configuration of
|
||||||
|
with_diff -- Show differences in case of configuration changes
|
||||||
|
force -- Override all manual modifications in configuration files
|
||||||
|
dry_run -- Show what would have been regenerated
|
||||||
|
list_pending -- List pending configuration files and exit
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Legacy code to automatically run the migration
|
||||||
|
# This is required because regen_conf is called before the migration call
|
||||||
|
# in debian's postinst script
|
||||||
|
if os.path.exists("/etc/yunohost/installed") \
|
||||||
|
and ("conffiles" in read_file("/etc/yunohost/services.yml") \
|
||||||
|
or not os.path.exists(REGEN_CONF_FILE)):
|
||||||
|
from yunohost.tools import _get_migration_by_name
|
||||||
|
migration = _get_migration_by_name("decouple_regenconf_from_services")
|
||||||
|
migration.migrate()
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Return the list of pending conf
|
||||||
|
if list_pending:
|
||||||
|
pending_conf = _get_pending_conf(names)
|
||||||
|
|
||||||
|
if not with_diff:
|
||||||
|
return pending_conf
|
||||||
|
|
||||||
|
for category, conf_files in pending_conf.items():
|
||||||
|
for system_path, pending_path in conf_files.items():
|
||||||
|
|
||||||
|
pending_conf[category][system_path] = {
|
||||||
|
'pending_conf': pending_path,
|
||||||
|
'diff': _get_files_diff(
|
||||||
|
system_path, pending_path, True),
|
||||||
|
}
|
||||||
|
|
||||||
|
return pending_conf
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
operation_logger.related_to = [('configuration', x) for x in names]
|
||||||
|
if not names:
|
||||||
|
operation_logger.name_parameter_override = 'all'
|
||||||
|
elif len(names) != 1:
|
||||||
|
operation_logger.name_parameter_override = str(len(operation_logger.related_to)) + '_categories'
|
||||||
|
operation_logger.start()
|
||||||
|
|
||||||
|
# Clean pending conf directory
|
||||||
|
if os.path.isdir(PENDING_CONF_DIR):
|
||||||
|
if not names:
|
||||||
|
shutil.rmtree(PENDING_CONF_DIR, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
for name in names:
|
||||||
|
shutil.rmtree(os.path.join(PENDING_CONF_DIR, name),
|
||||||
|
ignore_errors=True)
|
||||||
|
else:
|
||||||
|
filesystem.mkdir(PENDING_CONF_DIR, 0o755, True)
|
||||||
|
|
||||||
|
# Format common hooks arguments
|
||||||
|
common_args = [1 if force else 0, 1 if dry_run else 0]
|
||||||
|
|
||||||
|
# Execute hooks for pre-regen
|
||||||
|
pre_args = ['pre', ] + common_args
|
||||||
|
|
||||||
|
def _pre_call(name, priority, path, args):
|
||||||
|
# create the pending conf directory for the category
|
||||||
|
category_pending_path = os.path.join(PENDING_CONF_DIR, name)
|
||||||
|
filesystem.mkdir(category_pending_path, 0o755, True, uid='root')
|
||||||
|
|
||||||
|
# return the arguments to pass to the script
|
||||||
|
return pre_args + [category_pending_path, ]
|
||||||
|
|
||||||
|
# Don't regen SSH if not specifically specified
|
||||||
|
if not names:
|
||||||
|
names = hook_list('conf_regen', list_by='name',
|
||||||
|
show_info=False)['hooks']
|
||||||
|
names.remove('ssh')
|
||||||
|
|
||||||
|
pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call)
|
||||||
|
|
||||||
|
# Keep only the hook names with at least one success
|
||||||
|
names = [hook for hook, infos in pre_result.items()
|
||||||
|
if any(result["state"] == "succeed" for result in infos.values())]
|
||||||
|
|
||||||
|
# FIXME : what do in case of partial success/failure ...
|
||||||
|
if not names:
|
||||||
|
ret_failed = [hook for hook, infos in pre_result.items()
|
||||||
|
if any(result["state"] == "failed" for result in infos.values())]
|
||||||
|
raise YunohostError('regenconf_failed',
|
||||||
|
categories=', '.join(ret_failed))
|
||||||
|
|
||||||
|
# Set the processing method
|
||||||
|
_regen = _process_regen_conf if not dry_run else lambda *a, **k: True
|
||||||
|
|
||||||
|
operation_logger.related_to = []
|
||||||
|
|
||||||
|
# Iterate over categories and process pending conf
|
||||||
|
for category, conf_files in _get_pending_conf(names).items():
|
||||||
|
if not dry_run:
|
||||||
|
operation_logger.related_to.append(('configuration', category))
|
||||||
|
|
||||||
|
logger.debug(m18n.n(
|
||||||
|
'regenconf_pending_applying' if not dry_run else
|
||||||
|
'regenconf_dry_pending_applying',
|
||||||
|
category=category))
|
||||||
|
|
||||||
|
conf_hashes = _get_conf_hashes(category)
|
||||||
|
succeed_regen = {}
|
||||||
|
failed_regen = {}
|
||||||
|
|
||||||
|
for system_path, pending_path in conf_files.items():
|
||||||
|
logger.debug("processing pending conf '%s' to system conf '%s'",
|
||||||
|
pending_path, system_path)
|
||||||
|
conf_status = None
|
||||||
|
regenerated = False
|
||||||
|
|
||||||
|
# Get the diff between files
|
||||||
|
conf_diff = _get_files_diff(
|
||||||
|
system_path, pending_path, True) if with_diff else None
|
||||||
|
|
||||||
|
# Check if the conf must be removed
|
||||||
|
to_remove = True if os.path.getsize(pending_path) == 0 else False
|
||||||
|
|
||||||
|
# Retrieve and calculate hashes
|
||||||
|
system_hash = _calculate_hash(system_path)
|
||||||
|
saved_hash = conf_hashes.get(system_path, None)
|
||||||
|
new_hash = None if to_remove else _calculate_hash(pending_path)
|
||||||
|
|
||||||
|
# -> system conf does not exists
|
||||||
|
if not system_hash:
|
||||||
|
if to_remove:
|
||||||
|
logger.debug("> system conf is already removed")
|
||||||
|
os.remove(pending_path)
|
||||||
|
continue
|
||||||
|
if not saved_hash or force:
|
||||||
|
if force:
|
||||||
|
logger.debug("> system conf has been manually removed")
|
||||||
|
conf_status = 'force-created'
|
||||||
|
else:
|
||||||
|
logger.debug("> system conf does not exist yet")
|
||||||
|
conf_status = 'created'
|
||||||
|
regenerated = _regen(
|
||||||
|
system_path, pending_path, save=False)
|
||||||
|
else:
|
||||||
|
logger.info(m18n.n(
|
||||||
|
'regenconf_file_manually_removed',
|
||||||
|
conf=system_path))
|
||||||
|
conf_status = 'removed'
|
||||||
|
|
||||||
|
# -> system conf is not managed yet
|
||||||
|
elif not saved_hash:
|
||||||
|
logger.debug("> system conf is not managed yet")
|
||||||
|
if system_hash == new_hash:
|
||||||
|
logger.debug("> no changes to system conf has been made")
|
||||||
|
conf_status = 'managed'
|
||||||
|
regenerated = True
|
||||||
|
elif not to_remove:
|
||||||
|
# If the conf exist but is not managed yet, and is not to be removed,
|
||||||
|
# we assume that it is safe to regen it, since the file is backuped
|
||||||
|
# anyway (by default in _regen), as long as we warn the user
|
||||||
|
# appropriately.
|
||||||
|
logger.info(m18n.n('regenconf_now_managed_by_yunohost',
|
||||||
|
conf=system_path, category=category))
|
||||||
|
regenerated = _regen(system_path, pending_path)
|
||||||
|
conf_status = 'new'
|
||||||
|
elif force:
|
||||||
|
regenerated = _regen(system_path)
|
||||||
|
conf_status = 'force-removed'
|
||||||
|
else:
|
||||||
|
logger.info(m18n.n('regenconf_file_kept_back',
|
||||||
|
conf=system_path, category=category))
|
||||||
|
conf_status = 'unmanaged'
|
||||||
|
|
||||||
|
# -> system conf has not been manually modified
|
||||||
|
elif system_hash == saved_hash:
|
||||||
|
if to_remove:
|
||||||
|
regenerated = _regen(system_path)
|
||||||
|
conf_status = 'removed'
|
||||||
|
elif system_hash != new_hash:
|
||||||
|
regenerated = _regen(system_path, pending_path)
|
||||||
|
conf_status = 'updated'
|
||||||
|
else:
|
||||||
|
logger.debug("> system conf is already up-to-date")
|
||||||
|
os.remove(pending_path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("> system conf has been manually modified")
|
||||||
|
if system_hash == new_hash:
|
||||||
|
logger.debug("> new conf is as current system conf")
|
||||||
|
conf_status = 'managed'
|
||||||
|
regenerated = True
|
||||||
|
elif force:
|
||||||
|
regenerated = _regen(system_path, pending_path)
|
||||||
|
conf_status = 'force-updated'
|
||||||
|
else:
|
||||||
|
logger.warning(m18n.n(
|
||||||
|
'regenconf_file_manually_modified',
|
||||||
|
conf=system_path))
|
||||||
|
conf_status = 'modified'
|
||||||
|
|
||||||
|
# Store the result
|
||||||
|
conf_result = {'status': conf_status}
|
||||||
|
if conf_diff is not None:
|
||||||
|
conf_result['diff'] = conf_diff
|
||||||
|
if regenerated:
|
||||||
|
succeed_regen[system_path] = conf_result
|
||||||
|
conf_hashes[system_path] = new_hash
|
||||||
|
if os.path.isfile(pending_path):
|
||||||
|
os.remove(pending_path)
|
||||||
|
else:
|
||||||
|
failed_regen[system_path] = conf_result
|
||||||
|
|
||||||
|
# Check for category conf changes
|
||||||
|
if not succeed_regen and not failed_regen:
|
||||||
|
logger.debug(m18n.n('regenconf_up_to_date', category=category))
|
||||||
|
continue
|
||||||
|
elif not failed_regen:
|
||||||
|
logger.success(m18n.n(
|
||||||
|
'regenconf_updated' if not dry_run else
|
||||||
|
'regenconf_would_be_updated',
|
||||||
|
category=category))
|
||||||
|
|
||||||
|
if succeed_regen and not dry_run:
|
||||||
|
_update_conf_hashes(category, conf_hashes)
|
||||||
|
|
||||||
|
# Append the category results
|
||||||
|
result[category] = {
|
||||||
|
'applied': succeed_regen,
|
||||||
|
'pending': failed_regen
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return in case of dry run
|
||||||
|
if dry_run:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Execute hooks for post-regen
|
||||||
|
post_args = ['post', ] + common_args
|
||||||
|
|
||||||
|
def _pre_call(name, priority, path, args):
|
||||||
|
# append coma-separated applied changes for the category
|
||||||
|
if name in result and result[name]['applied']:
|
||||||
|
regen_conf_files = ','.join(result[name]['applied'].keys())
|
||||||
|
else:
|
||||||
|
regen_conf_files = ''
|
||||||
|
return post_args + [regen_conf_files, ]
|
||||||
|
|
||||||
|
hook_callback('conf_regen', names, pre_callback=_pre_call)
|
||||||
|
|
||||||
|
operation_logger.success()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _get_regenconf_infos():
|
||||||
|
"""
|
||||||
|
Get a dict of regen conf informations
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(REGEN_CONF_FILE, 'r') as f:
|
||||||
|
return yaml.load(f)
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _save_regenconf_infos(infos):
|
||||||
|
"""
|
||||||
|
Save the regen conf informations
|
||||||
|
Keyword argument:
|
||||||
|
categories -- A dict containing the regenconf infos
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(REGEN_CONF_FILE, 'w') as f:
|
||||||
|
yaml.safe_dump(infos, f, default_flow_style=False)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning('Error while saving regenconf infos, exception: %s', e, exc_info=1)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True):
|
||||||
|
"""Compare two files and return the differences
|
||||||
|
|
||||||
|
Read and compare two files. The differences are returned either as a delta
|
||||||
|
in unified diff format or a formatted string if as_string is True. The
|
||||||
|
header can also be removed if skip_header is True.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if os.path.exists(orig_file):
|
||||||
|
with open(orig_file, 'r') as orig_file:
|
||||||
|
orig_file = orig_file.readlines()
|
||||||
|
else:
|
||||||
|
orig_file = []
|
||||||
|
|
||||||
|
if os.path.exists(new_file):
|
||||||
|
with open(new_file, 'r') as new_file:
|
||||||
|
new_file = new_file.readlines()
|
||||||
|
else:
|
||||||
|
new_file = []
|
||||||
|
|
||||||
|
# Compare files and format output
|
||||||
|
diff = unified_diff(orig_file, new_file)
|
||||||
|
|
||||||
|
if skip_header:
|
||||||
|
try:
|
||||||
|
next(diff)
|
||||||
|
next(diff)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if as_string:
|
||||||
|
return ''.join(diff).rstrip()
|
||||||
|
|
||||||
|
return diff
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_hash(path):
|
||||||
|
"""Calculate the MD5 hash of a file"""
|
||||||
|
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
hasher = hashlib.md5()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
hasher.update(f.read())
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
except IOError as e:
|
||||||
|
logger.warning("Error while calculating file '%s' hash: %s", path, e, exc_info=1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pending_conf(categories=[]):
|
||||||
|
"""Get pending configuration for categories
|
||||||
|
|
||||||
|
Iterate over the pending configuration directory for given categories - or
|
||||||
|
all if empty - and look for files inside. Each file is considered as a
|
||||||
|
pending configuration file and therefore must be in the same directory
|
||||||
|
tree than the system file that it replaces.
|
||||||
|
The result is returned as a dict of categories with pending configuration as
|
||||||
|
key and a dict of `system_conf_path` => `pending_conf_path` as value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if not os.path.isdir(PENDING_CONF_DIR):
|
||||||
|
return result
|
||||||
|
|
||||||
|
if not categories:
|
||||||
|
categories = os.listdir(PENDING_CONF_DIR)
|
||||||
|
|
||||||
|
for name in categories:
|
||||||
|
category_pending_path = os.path.join(PENDING_CONF_DIR, name)
|
||||||
|
|
||||||
|
if not os.path.isdir(category_pending_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
path_index = len(category_pending_path)
|
||||||
|
category_conf = {}
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(category_pending_path):
|
||||||
|
for filename in files:
|
||||||
|
pending_path = os.path.join(root, filename)
|
||||||
|
category_conf[pending_path[path_index:]] = pending_path
|
||||||
|
|
||||||
|
if category_conf:
|
||||||
|
result[name] = category_conf
|
||||||
|
else:
|
||||||
|
# remove empty directory
|
||||||
|
shutil.rmtree(category_pending_path, ignore_errors=True)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _get_conf_hashes(category):
|
||||||
|
"""Get the registered conf hashes for a category"""
|
||||||
|
|
||||||
|
categories = _get_regenconf_infos()
|
||||||
|
|
||||||
|
if category not in categories:
|
||||||
|
logger.debug("category %s is not in categories.yml yet.", category)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
elif categories[category] is None or 'conffiles' not in categories[category]:
|
||||||
|
logger.debug("No configuration files for category %s.", category)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return categories[category]['conffiles']
|
||||||
|
|
||||||
|
|
||||||
|
def _update_conf_hashes(category, hashes):
|
||||||
|
"""Update the registered conf hashes for a category"""
|
||||||
|
logger.debug("updating conf hashes for '%s' with: %s",
|
||||||
|
category, hashes)
|
||||||
|
|
||||||
|
categories = _get_regenconf_infos()
|
||||||
|
category_conf = categories.get(category, {})
|
||||||
|
|
||||||
|
# Handle the case where categories[category] is set to null in the yaml
|
||||||
|
if category_conf is None:
|
||||||
|
category_conf = {}
|
||||||
|
|
||||||
|
category_conf['conffiles'] = hashes
|
||||||
|
categories[category] = category_conf
|
||||||
|
_save_regenconf_infos(categories)
|
||||||
|
|
||||||
|
|
||||||
|
def _process_regen_conf(system_conf, new_conf=None, save=True):
|
||||||
|
"""Regenerate a given system configuration file
|
||||||
|
|
||||||
|
Replace a given system configuration file by a new one or delete it if
|
||||||
|
new_conf is None. A backup of the file - keeping its directory tree - will
|
||||||
|
be done in the backup conf directory before any operation if save is True.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if save:
|
||||||
|
backup_path = os.path.join(BACKUP_CONF_DIR, '{0}-{1}'.format(
|
||||||
|
system_conf.lstrip('/'), datetime.utcnow().strftime("%Y%m%d.%H%M%S")))
|
||||||
|
backup_dir = os.path.dirname(backup_path)
|
||||||
|
|
||||||
|
if not os.path.isdir(backup_dir):
|
||||||
|
filesystem.mkdir(backup_dir, 0o755, True)
|
||||||
|
|
||||||
|
shutil.copy2(system_conf, backup_path)
|
||||||
|
logger.debug(m18n.n('regenconf_file_backed_up',
|
||||||
|
conf=system_conf, backup=backup_path))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not new_conf:
|
||||||
|
os.remove(system_conf)
|
||||||
|
logger.debug(m18n.n('regenconf_file_removed',
|
||||||
|
conf=system_conf))
|
||||||
|
else:
|
||||||
|
system_dir = os.path.dirname(system_conf)
|
||||||
|
|
||||||
|
if not os.path.isdir(system_dir):
|
||||||
|
filesystem.mkdir(system_dir, 0o755, True)
|
||||||
|
|
||||||
|
shutil.copyfile(new_conf, system_conf)
|
||||||
|
logger.debug(m18n.n('regenconf_file_updated',
|
||||||
|
conf=system_conf))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Exception while trying to regenerate conf '%s': %s", system_conf, e, exc_info=1)
|
||||||
|
if not new_conf and os.path.exists(system_conf):
|
||||||
|
logger.warning(m18n.n('regenconf_file_remove_failed',
|
||||||
|
conf=system_conf),
|
||||||
|
exc_info=1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif new_conf:
|
||||||
|
try:
|
||||||
|
# From documentation:
|
||||||
|
# Raise an exception if an os.stat() call on either pathname fails.
|
||||||
|
# (os.stats returns a series of information from a file like type, size...)
|
||||||
|
copy_succeed = os.path.samefile(system_conf, new_conf)
|
||||||
|
except:
|
||||||
|
copy_succeed = False
|
||||||
|
finally:
|
||||||
|
if not copy_succeed:
|
||||||
|
logger.warning(m18n.n('regenconf_file_copy_failed',
|
||||||
|
conf=system_conf, new=new_conf),
|
||||||
|
exc_info=1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def manually_modified_files():
|
||||||
|
|
||||||
|
# We do this to have --quiet, i.e. don't throw a whole bunch of logs
|
||||||
|
# just to fetch this...
|
||||||
|
# Might be able to optimize this by looking at what the regen conf does
|
||||||
|
# and only do the part that checks file hashes...
|
||||||
|
cmd = "yunohost tools regen-conf --dry-run --output-as json --quiet"
|
||||||
|
j = json.loads(subprocess.check_output(cmd.split()))
|
||||||
|
|
||||||
|
# j is something like :
|
||||||
|
# {"postfix": {"applied": {}, "pending": {"/etc/postfix/main.cf": {"status": "modified"}}}
|
||||||
|
|
||||||
|
output = []
|
||||||
|
for app, actions in j.items():
|
||||||
|
for action, files in actions.items():
|
||||||
|
for filename, infos in files.items():
|
||||||
|
if infos["status"] == "modified":
|
||||||
|
output.append(filename)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def manually_modified_files_compared_to_debian_default():
|
||||||
|
|
||||||
|
# from https://serverfault.com/a/90401
|
||||||
|
r = subprocess.check_output("dpkg-query -W -f='${Conffiles}\n' '*' \
|
||||||
|
| awk 'OFS=\" \"{print $2,$1}' \
|
||||||
|
| md5sum -c 2>/dev/null \
|
||||||
|
| awk -F': ' '$2 !~ /OK/{print $1}'", shell=True)
|
||||||
|
return r.strip().split("\n")
|
|
@ -26,13 +26,9 @@
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import yaml
|
import yaml
|
||||||
import json
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from difflib import unified_diff
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from moulinette import m18n
|
from moulinette import m18n
|
||||||
|
@ -40,11 +36,7 @@ from yunohost.utils.error import YunohostError
|
||||||
from moulinette.utils import log, filesystem
|
from moulinette.utils import log, filesystem
|
||||||
|
|
||||||
from yunohost.log import is_unit_operation
|
from yunohost.log import is_unit_operation
|
||||||
from yunohost.hook import hook_callback, hook_list
|
|
||||||
|
|
||||||
BASE_CONF_PATH = '/home/yunohost.conf'
|
|
||||||
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"
|
MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock"
|
||||||
|
|
||||||
logger = log.getActionLogger('yunohost.service')
|
logger = log.getActionLogger('yunohost.service')
|
||||||
|
@ -424,253 +416,25 @@ def service_log(name, number=50):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@is_unit_operation([('names', 'service')])
|
def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False,
|
||||||
def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False,
|
|
||||||
list_pending=False):
|
list_pending=False):
|
||||||
"""
|
|
||||||
Regenerate the configuration file(s) for a service
|
|
||||||
|
|
||||||
Keyword argument:
|
services = _get_services()
|
||||||
names -- Services name to regenerate configuration of
|
|
||||||
with_diff -- Show differences in case of configuration changes
|
|
||||||
force -- Override all manual modifications in configuration files
|
|
||||||
dry_run -- Show what would have been regenerated
|
|
||||||
list_pending -- List pending configuration files and exit
|
|
||||||
|
|
||||||
"""
|
if isinstance(names, str):
|
||||||
result = {}
|
names = [names]
|
||||||
|
|
||||||
# Return the list of pending conf
|
for name in names:
|
||||||
if list_pending:
|
if name not in services.keys():
|
||||||
pending_conf = _get_pending_conf(names)
|
raise YunohostError('service_unknown', service=name)
|
||||||
|
|
||||||
if not with_diff:
|
if names is []:
|
||||||
return pending_conf
|
names = services.keys()
|
||||||
|
|
||||||
for service, conf_files in pending_conf.items():
|
logger.warning(m18n.n("service_regen_conf_is_deprecated"))
|
||||||
for system_path, pending_path in conf_files.items():
|
|
||||||
|
|
||||||
pending_conf[service][system_path] = {
|
from yunohost.regenconf import regen_conf
|
||||||
'pending_conf': pending_path,
|
return regen_conf(names, with_diff, force, dry_run, list_pending)
|
||||||
'diff': _get_files_diff(
|
|
||||||
system_path, pending_path, True),
|
|
||||||
}
|
|
||||||
|
|
||||||
return pending_conf
|
|
||||||
|
|
||||||
if not dry_run:
|
|
||||||
operation_logger.related_to = [('service', x) for x in names]
|
|
||||||
if not names:
|
|
||||||
operation_logger.name_parameter_override = 'all'
|
|
||||||
elif len(names) != 1:
|
|
||||||
operation_logger.name_parameter_override = str(len(operation_logger.related_to)) + '_services'
|
|
||||||
operation_logger.start()
|
|
||||||
|
|
||||||
# Clean pending conf directory
|
|
||||||
if os.path.isdir(PENDING_CONF_DIR):
|
|
||||||
if not names:
|
|
||||||
shutil.rmtree(PENDING_CONF_DIR, ignore_errors=True)
|
|
||||||
else:
|
|
||||||
for name in names:
|
|
||||||
shutil.rmtree(os.path.join(PENDING_CONF_DIR, name),
|
|
||||||
ignore_errors=True)
|
|
||||||
else:
|
|
||||||
filesystem.mkdir(PENDING_CONF_DIR, 0o755, True)
|
|
||||||
|
|
||||||
# Format common hooks arguments
|
|
||||||
common_args = [1 if force else 0, 1 if dry_run else 0]
|
|
||||||
|
|
||||||
# Execute hooks for pre-regen
|
|
||||||
pre_args = ['pre', ] + common_args
|
|
||||||
|
|
||||||
def _pre_call(name, priority, path, args):
|
|
||||||
# create the pending conf directory for the service
|
|
||||||
service_pending_path = os.path.join(PENDING_CONF_DIR, name)
|
|
||||||
filesystem.mkdir(service_pending_path, 0o755, True, uid='root')
|
|
||||||
|
|
||||||
# return the arguments to pass to the script
|
|
||||||
return pre_args + [service_pending_path, ]
|
|
||||||
|
|
||||||
# Don't regen SSH if not specifically specified
|
|
||||||
if not names:
|
|
||||||
names = hook_list('conf_regen', list_by='name',
|
|
||||||
show_info=False)['hooks']
|
|
||||||
names.remove('ssh')
|
|
||||||
|
|
||||||
pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call)
|
|
||||||
|
|
||||||
# Keep only the hook names with at least one success
|
|
||||||
names = [hook for hook, infos in pre_result.items()
|
|
||||||
if any(result["state"] == "succeed" for result in infos.values())]
|
|
||||||
|
|
||||||
# FIXME : what do in case of partial success/failure ...
|
|
||||||
if not names:
|
|
||||||
ret_failed = [hook for hook, infos in pre_result.items()
|
|
||||||
if any(result["state"] == "failed" for result in infos.values())]
|
|
||||||
raise YunohostError('service_regenconf_failed',
|
|
||||||
services=', '.join(ret_failed))
|
|
||||||
|
|
||||||
# Set the processing method
|
|
||||||
_regen = _process_regen_conf if not dry_run else lambda *a, **k: True
|
|
||||||
|
|
||||||
operation_logger.related_to = []
|
|
||||||
|
|
||||||
# Iterate over services and process pending conf
|
|
||||||
for service, conf_files in _get_pending_conf(names).items():
|
|
||||||
if not dry_run:
|
|
||||||
operation_logger.related_to.append(('service', service))
|
|
||||||
|
|
||||||
logger.debug(m18n.n(
|
|
||||||
'service_regenconf_pending_applying' if not dry_run else
|
|
||||||
'service_regenconf_dry_pending_applying',
|
|
||||||
service=service))
|
|
||||||
|
|
||||||
conf_hashes = _get_conf_hashes(service)
|
|
||||||
succeed_regen = {}
|
|
||||||
failed_regen = {}
|
|
||||||
|
|
||||||
for system_path, pending_path in conf_files.items():
|
|
||||||
logger.debug("processing pending conf '%s' to system conf '%s'",
|
|
||||||
pending_path, system_path)
|
|
||||||
conf_status = None
|
|
||||||
regenerated = False
|
|
||||||
|
|
||||||
# Get the diff between files
|
|
||||||
conf_diff = _get_files_diff(
|
|
||||||
system_path, pending_path, True) if with_diff else None
|
|
||||||
|
|
||||||
# Check if the conf must be removed
|
|
||||||
to_remove = True if os.path.getsize(pending_path) == 0 else False
|
|
||||||
|
|
||||||
# Retrieve and calculate hashes
|
|
||||||
system_hash = _calculate_hash(system_path)
|
|
||||||
saved_hash = conf_hashes.get(system_path, None)
|
|
||||||
new_hash = None if to_remove else _calculate_hash(pending_path)
|
|
||||||
|
|
||||||
# -> system conf does not exists
|
|
||||||
if not system_hash:
|
|
||||||
if to_remove:
|
|
||||||
logger.debug("> system conf is already removed")
|
|
||||||
os.remove(pending_path)
|
|
||||||
continue
|
|
||||||
if not saved_hash or force:
|
|
||||||
if force:
|
|
||||||
logger.debug("> system conf has been manually removed")
|
|
||||||
conf_status = 'force-created'
|
|
||||||
else:
|
|
||||||
logger.debug("> system conf does not exist yet")
|
|
||||||
conf_status = 'created'
|
|
||||||
regenerated = _regen(
|
|
||||||
system_path, pending_path, save=False)
|
|
||||||
else:
|
|
||||||
logger.info(m18n.n(
|
|
||||||
'service_conf_file_manually_removed',
|
|
||||||
conf=system_path))
|
|
||||||
conf_status = 'removed'
|
|
||||||
|
|
||||||
# -> system conf is not managed yet
|
|
||||||
elif not saved_hash:
|
|
||||||
logger.debug("> system conf is not managed yet")
|
|
||||||
if system_hash == new_hash:
|
|
||||||
logger.debug("> no changes to system conf has been made")
|
|
||||||
conf_status = 'managed'
|
|
||||||
regenerated = True
|
|
||||||
elif not to_remove:
|
|
||||||
# If the conf exist but is not managed yet, and is not to be removed,
|
|
||||||
# we assume that it is safe to regen it, since the file is backuped
|
|
||||||
# anyway (by default in _regen), as long as we warn the user
|
|
||||||
# appropriately.
|
|
||||||
logger.info(m18n.n('service_conf_now_managed_by_yunohost',
|
|
||||||
conf=system_path))
|
|
||||||
regenerated = _regen(system_path, pending_path)
|
|
||||||
conf_status = 'new'
|
|
||||||
elif force:
|
|
||||||
regenerated = _regen(system_path)
|
|
||||||
conf_status = 'force-removed'
|
|
||||||
else:
|
|
||||||
logger.info(m18n.n('service_conf_file_kept_back',
|
|
||||||
conf=system_path, service=service))
|
|
||||||
conf_status = 'unmanaged'
|
|
||||||
|
|
||||||
# -> system conf has not been manually modified
|
|
||||||
elif system_hash == saved_hash:
|
|
||||||
if to_remove:
|
|
||||||
regenerated = _regen(system_path)
|
|
||||||
conf_status = 'removed'
|
|
||||||
elif system_hash != new_hash:
|
|
||||||
regenerated = _regen(system_path, pending_path)
|
|
||||||
conf_status = 'updated'
|
|
||||||
else:
|
|
||||||
logger.debug("> system conf is already up-to-date")
|
|
||||||
os.remove(pending_path)
|
|
||||||
continue
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.debug("> system conf has been manually modified")
|
|
||||||
if system_hash == new_hash:
|
|
||||||
logger.debug("> new conf is as current system conf")
|
|
||||||
conf_status = 'managed'
|
|
||||||
regenerated = True
|
|
||||||
elif force:
|
|
||||||
regenerated = _regen(system_path, pending_path)
|
|
||||||
conf_status = 'force-updated'
|
|
||||||
else:
|
|
||||||
logger.warning(m18n.n(
|
|
||||||
'service_conf_file_manually_modified',
|
|
||||||
conf=system_path))
|
|
||||||
conf_status = 'modified'
|
|
||||||
|
|
||||||
# Store the result
|
|
||||||
conf_result = {'status': conf_status}
|
|
||||||
if conf_diff is not None:
|
|
||||||
conf_result['diff'] = conf_diff
|
|
||||||
if regenerated:
|
|
||||||
succeed_regen[system_path] = conf_result
|
|
||||||
conf_hashes[system_path] = new_hash
|
|
||||||
if os.path.isfile(pending_path):
|
|
||||||
os.remove(pending_path)
|
|
||||||
else:
|
|
||||||
failed_regen[system_path] = conf_result
|
|
||||||
|
|
||||||
# Check for service conf changes
|
|
||||||
if not succeed_regen and not failed_regen:
|
|
||||||
logger.debug(m18n.n('service_conf_up_to_date', service=service))
|
|
||||||
continue
|
|
||||||
elif not failed_regen:
|
|
||||||
logger.success(m18n.n(
|
|
||||||
'service_conf_updated' if not dry_run else
|
|
||||||
'service_conf_would_be_updated',
|
|
||||||
service=service))
|
|
||||||
|
|
||||||
if succeed_regen and not dry_run:
|
|
||||||
_update_conf_hashes(service, conf_hashes)
|
|
||||||
|
|
||||||
# Append the service results
|
|
||||||
result[service] = {
|
|
||||||
'applied': succeed_regen,
|
|
||||||
'pending': failed_regen
|
|
||||||
}
|
|
||||||
|
|
||||||
# Return in case of dry run
|
|
||||||
if dry_run:
|
|
||||||
return result
|
|
||||||
|
|
||||||
# Execute hooks for post-regen
|
|
||||||
post_args = ['post', ] + common_args
|
|
||||||
|
|
||||||
def _pre_call(name, priority, path, args):
|
|
||||||
# append coma-separated applied changes for the service
|
|
||||||
if name in result and result[name]['applied']:
|
|
||||||
regen_conf_files = ','.join(result[name]['applied'].keys())
|
|
||||||
else:
|
|
||||||
regen_conf_files = ''
|
|
||||||
return post_args + [regen_conf_files, ]
|
|
||||||
|
|
||||||
hook_callback('conf_regen', names, pre_callback=_pre_call)
|
|
||||||
|
|
||||||
operation_logger.success()
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _run_service_command(action, service):
|
def _run_service_command(action, service):
|
||||||
|
@ -870,231 +634,9 @@ def _find_previous_log_file(file):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True):
|
|
||||||
"""Compare two files and return the differences
|
|
||||||
|
|
||||||
Read and compare two files. The differences are returned either as a delta
|
|
||||||
in unified diff format or a formatted string if as_string is True. The
|
|
||||||
header can also be removed if skip_header is True.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if os.path.exists(orig_file):
|
|
||||||
with open(orig_file, 'r') as orig_file:
|
|
||||||
orig_file = orig_file.readlines()
|
|
||||||
else:
|
|
||||||
orig_file = []
|
|
||||||
|
|
||||||
if os.path.exists(new_file):
|
|
||||||
with open(new_file, 'r') as new_file:
|
|
||||||
new_file = new_file.readlines()
|
|
||||||
else:
|
|
||||||
new_file = []
|
|
||||||
|
|
||||||
# Compare files and format output
|
|
||||||
diff = unified_diff(orig_file, new_file)
|
|
||||||
|
|
||||||
if skip_header:
|
|
||||||
try:
|
|
||||||
next(diff)
|
|
||||||
next(diff)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if as_string:
|
|
||||||
return ''.join(diff).rstrip()
|
|
||||||
|
|
||||||
return diff
|
|
||||||
|
|
||||||
|
|
||||||
def _calculate_hash(path):
|
|
||||||
"""Calculate the MD5 hash of a file"""
|
|
||||||
|
|
||||||
if not os.path.exists(path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
hasher = hashlib.md5()
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(path, 'rb') as f:
|
|
||||||
hasher.update(f.read())
|
|
||||||
return hasher.hexdigest()
|
|
||||||
|
|
||||||
except IOError as e:
|
|
||||||
logger.warning("Error while calculating file '%s' hash: %s", path, e, exc_info=1)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pending_conf(services=[]):
|
|
||||||
"""Get pending configuration for service(s)
|
|
||||||
|
|
||||||
Iterate over the pending configuration directory for given service(s) - or
|
|
||||||
all if empty - and look for files inside. Each file is considered as a
|
|
||||||
pending configuration file and therefore must be in the same directory
|
|
||||||
tree than the system file that it replaces.
|
|
||||||
The result is returned as a dict of services with pending configuration as
|
|
||||||
key and a dict of `system_conf_path` => `pending_conf_path` as value.
|
|
||||||
|
|
||||||
"""
|
|
||||||
result = {}
|
|
||||||
|
|
||||||
if not os.path.isdir(PENDING_CONF_DIR):
|
|
||||||
return result
|
|
||||||
|
|
||||||
if not services:
|
|
||||||
services = os.listdir(PENDING_CONF_DIR)
|
|
||||||
|
|
||||||
for name in services:
|
|
||||||
service_pending_path = os.path.join(PENDING_CONF_DIR, name)
|
|
||||||
|
|
||||||
if not os.path.isdir(service_pending_path):
|
|
||||||
continue
|
|
||||||
|
|
||||||
path_index = len(service_pending_path)
|
|
||||||
service_conf = {}
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk(service_pending_path):
|
|
||||||
for filename in files:
|
|
||||||
pending_path = os.path.join(root, filename)
|
|
||||||
service_conf[pending_path[path_index:]] = pending_path
|
|
||||||
|
|
||||||
if service_conf:
|
|
||||||
result[name] = service_conf
|
|
||||||
else:
|
|
||||||
# remove empty directory
|
|
||||||
shutil.rmtree(service_pending_path, ignore_errors=True)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _get_conf_hashes(service):
|
|
||||||
"""Get the registered conf hashes for a service"""
|
|
||||||
|
|
||||||
services = _get_services()
|
|
||||||
|
|
||||||
if service not in services:
|
|
||||||
logger.debug("Service %s is not in services.yml yet.", service)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
elif services[service] is None or 'conffiles' not in services[service]:
|
|
||||||
logger.debug("No configuration files for service %s.", service)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
else:
|
|
||||||
return services[service]['conffiles']
|
|
||||||
|
|
||||||
|
|
||||||
def _update_conf_hashes(service, hashes):
|
|
||||||
"""Update the registered conf hashes for a service"""
|
|
||||||
logger.debug("updating conf hashes for '%s' with: %s",
|
|
||||||
service, hashes)
|
|
||||||
services = _get_services()
|
|
||||||
service_conf = services.get(service, {})
|
|
||||||
|
|
||||||
# Handle the case where services[service] is set to null in the yaml
|
|
||||||
if service_conf is None:
|
|
||||||
service_conf = {}
|
|
||||||
|
|
||||||
service_conf['conffiles'] = hashes
|
|
||||||
services[service] = service_conf
|
|
||||||
_save_services(services)
|
|
||||||
|
|
||||||
|
|
||||||
def _process_regen_conf(system_conf, new_conf=None, save=True):
|
|
||||||
"""Regenerate a given system configuration file
|
|
||||||
|
|
||||||
Replace a given system configuration file by a new one or delete it if
|
|
||||||
new_conf is None. A backup of the file - keeping its directory tree - will
|
|
||||||
be done in the backup conf directory before any operation if save is True.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if save:
|
|
||||||
backup_path = os.path.join(BACKUP_CONF_DIR, '{0}-{1}'.format(
|
|
||||||
system_conf.lstrip('/'), datetime.utcnow().strftime("%Y%m%d.%H%M%S")))
|
|
||||||
backup_dir = os.path.dirname(backup_path)
|
|
||||||
|
|
||||||
if not os.path.isdir(backup_dir):
|
|
||||||
filesystem.mkdir(backup_dir, 0o755, True)
|
|
||||||
|
|
||||||
shutil.copy2(system_conf, backup_path)
|
|
||||||
logger.debug(m18n.n('service_conf_file_backed_up',
|
|
||||||
conf=system_conf, backup=backup_path))
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not new_conf:
|
|
||||||
os.remove(system_conf)
|
|
||||||
logger.debug(m18n.n('service_conf_file_removed',
|
|
||||||
conf=system_conf))
|
|
||||||
else:
|
|
||||||
system_dir = os.path.dirname(system_conf)
|
|
||||||
|
|
||||||
if not os.path.isdir(system_dir):
|
|
||||||
filesystem.mkdir(system_dir, 0o755, True)
|
|
||||||
|
|
||||||
shutil.copyfile(new_conf, system_conf)
|
|
||||||
logger.debug(m18n.n('service_conf_file_updated',
|
|
||||||
conf=system_conf))
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("Exception while trying to regenerate conf '%s': %s", system_conf, e, exc_info=1)
|
|
||||||
if not new_conf and os.path.exists(system_conf):
|
|
||||||
logger.warning(m18n.n('service_conf_file_remove_failed',
|
|
||||||
conf=system_conf),
|
|
||||||
exc_info=1)
|
|
||||||
return False
|
|
||||||
|
|
||||||
elif new_conf:
|
|
||||||
try:
|
|
||||||
# From documentation:
|
|
||||||
# Raise an exception if an os.stat() call on either pathname fails.
|
|
||||||
# (os.stats returns a series of information from a file like type, size...)
|
|
||||||
copy_succeed = os.path.samefile(system_conf, new_conf)
|
|
||||||
except:
|
|
||||||
copy_succeed = False
|
|
||||||
finally:
|
|
||||||
if not copy_succeed:
|
|
||||||
logger.warning(m18n.n('service_conf_file_copy_failed',
|
|
||||||
conf=system_conf, new=new_conf),
|
|
||||||
exc_info=1)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def manually_modified_files():
|
|
||||||
|
|
||||||
# We do this to have --quiet, i.e. don't throw a whole bunch of logs
|
|
||||||
# just to fetch this...
|
|
||||||
# Might be able to optimize this by looking at what service_regenconf does
|
|
||||||
# and only do the part that checks file hashes...
|
|
||||||
cmd = "yunohost service regen-conf --dry-run --output-as json --quiet"
|
|
||||||
j = json.loads(subprocess.check_output(cmd.split()))
|
|
||||||
|
|
||||||
# j is something like :
|
|
||||||
# {"postfix": {"applied": {}, "pending": {"/etc/postfix/main.cf": {"status": "modified"}}}
|
|
||||||
|
|
||||||
output = []
|
|
||||||
for app, actions in j.items():
|
|
||||||
for action, files in actions.items():
|
|
||||||
for filename, infos in files.items():
|
|
||||||
if infos["status"] == "modified":
|
|
||||||
output.append(filename)
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
def _get_journalctl_logs(service, number="all"):
|
def _get_journalctl_logs(service, number="all"):
|
||||||
try:
|
try:
|
||||||
return subprocess.check_output("journalctl -xn -u {0} -n{1}".format(service, number), shell=True)
|
return subprocess.check_output("journalctl -xn -u {0} -n{1}".format(service, number), shell=True)
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
return "error while get services logs from journalctl:\n%s" % traceback.format_exc()
|
return "error while get services logs from journalctl:\n%s" % traceback.format_exc()
|
||||||
|
|
||||||
|
|
||||||
def manually_modified_files_compared_to_debian_default():
|
|
||||||
|
|
||||||
# from https://serverfault.com/a/90401
|
|
||||||
r = subprocess.check_output("dpkg-query -W -f='${Conffiles}\n' '*' \
|
|
||||||
| awk 'OFS=\" \"{print $2,$1}' \
|
|
||||||
| md5sum -c 2>/dev/null \
|
|
||||||
| awk -F': ' '$2 !~ /OK/{print $1}'", shell=True)
|
|
||||||
return r.strip().split("\n")
|
|
||||||
|
|
|
@ -48,7 +48,8 @@ from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, a
|
||||||
from yunohost.domain import domain_add, domain_list, _get_maindomain, _set_maindomain
|
from yunohost.domain import domain_add, domain_list, _get_maindomain, _set_maindomain
|
||||||
from yunohost.dyndns import _dyndns_available, _dyndns_provides
|
from yunohost.dyndns import _dyndns_available, _dyndns_provides
|
||||||
from yunohost.firewall import firewall_upnp
|
from yunohost.firewall import firewall_upnp
|
||||||
from yunohost.service import service_status, service_regen_conf, service_log, service_start, service_enable
|
from yunohost.service import service_status, service_log, service_start, service_enable
|
||||||
|
from yunohost.regenconf import regen_conf
|
||||||
from yunohost.monitor import monitor_disk, monitor_system
|
from yunohost.monitor import monitor_disk, monitor_system
|
||||||
from yunohost.utils.packages import ynh_packages_version
|
from yunohost.utils.packages import ynh_packages_version
|
||||||
from yunohost.utils.network import get_public_ip
|
from yunohost.utils.network import get_public_ip
|
||||||
|
@ -213,7 +214,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None):
|
||||||
# Regen configurations
|
# Regen configurations
|
||||||
try:
|
try:
|
||||||
with open('/etc/yunohost/installed', 'r'):
|
with open('/etc/yunohost/installed', 'r'):
|
||||||
service_regen_conf()
|
regen_conf()
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -331,7 +332,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
|
||||||
operation_logger.start()
|
operation_logger.start()
|
||||||
logger.info(m18n.n('yunohost_installing'))
|
logger.info(m18n.n('yunohost_installing'))
|
||||||
|
|
||||||
service_regen_conf(['nslcd', 'nsswitch'], force=True)
|
regen_conf(['nslcd', 'nsswitch'], force=True)
|
||||||
|
|
||||||
# Initialize LDAP for YunoHost
|
# Initialize LDAP for YunoHost
|
||||||
# TODO: Improve this part by integrate ldapinit into conf_regen hook
|
# TODO: Improve this part by integrate ldapinit into conf_regen hook
|
||||||
|
@ -382,7 +383,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
|
||||||
os.system('chmod 644 /etc/ssowat/conf.json.persistent')
|
os.system('chmod 644 /etc/ssowat/conf.json.persistent')
|
||||||
|
|
||||||
# Create SSL CA
|
# Create SSL CA
|
||||||
service_regen_conf(['ssl'], force=True)
|
regen_conf(['ssl'], force=True)
|
||||||
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
|
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
|
||||||
# (Update the serial so that it's specific to this very instance)
|
# (Update the serial so that it's specific to this very instance)
|
||||||
os.system("openssl rand -hex 19 > %s/serial" % ssl_dir)
|
os.system("openssl rand -hex 19 > %s/serial" % ssl_dir)
|
||||||
|
@ -411,7 +412,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
|
||||||
logger.success(m18n.n('yunohost_ca_creation_success'))
|
logger.success(m18n.n('yunohost_ca_creation_success'))
|
||||||
|
|
||||||
# New domain config
|
# New domain config
|
||||||
service_regen_conf(['nsswitch'], force=True)
|
regen_conf(['nsswitch'], force=True)
|
||||||
domain_add(auth, domain, dyndns)
|
domain_add(auth, domain, dyndns)
|
||||||
tools_maindomain(auth, domain)
|
tools_maindomain(auth, domain)
|
||||||
|
|
||||||
|
@ -439,7 +440,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
|
||||||
service_enable("yunohost-firewall")
|
service_enable("yunohost-firewall")
|
||||||
service_start("yunohost-firewall")
|
service_start("yunohost-firewall")
|
||||||
|
|
||||||
service_regen_conf(force=True)
|
regen_conf(force=True)
|
||||||
|
|
||||||
# Restore original ssh conf, as chosen by the
|
# Restore original ssh conf, as chosen by the
|
||||||
# admin during the initial install
|
# admin during the initial install
|
||||||
|
@ -456,13 +457,18 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
|
||||||
else:
|
else:
|
||||||
# We need to explicitly ask the regen conf to regen ssh
|
# We need to explicitly ask the regen conf to regen ssh
|
||||||
# (by default, i.e. first argument = None, it won't because it's too touchy)
|
# (by default, i.e. first argument = None, it won't because it's too touchy)
|
||||||
service_regen_conf(names=["ssh"], force=True)
|
regen_conf(names=["ssh"], force=True)
|
||||||
|
|
||||||
logger.success(m18n.n('yunohost_configured'))
|
logger.success(m18n.n('yunohost_configured'))
|
||||||
|
|
||||||
logger.warning(m18n.n('recommend_to_add_first_user'))
|
logger.warning(m18n.n('recommend_to_add_first_user'))
|
||||||
|
|
||||||
|
|
||||||
|
def tools_regen_conf(names=[], with_diff=False, force=False, dry_run=False,
|
||||||
|
list_pending=False):
|
||||||
|
return regen_conf(names, with_diff, force, dry_run, list_pending)
|
||||||
|
|
||||||
|
|
||||||
def tools_update(ignore_apps=False, ignore_packages=False):
|
def tools_update(ignore_apps=False, ignore_packages=False):
|
||||||
"""
|
"""
|
||||||
Update apps & package cache, then display changelog
|
Update apps & package cache, then display changelog
|
||||||
|
@ -758,7 +764,7 @@ def tools_diagnosis(auth, private=False):
|
||||||
# Domains
|
# Domains
|
||||||
diagnosis['private']['domains'] = domain_list(auth)['domains']
|
diagnosis['private']['domains'] = domain_list(auth)['domains']
|
||||||
|
|
||||||
diagnosis['private']['regen_conf'] = service_regen_conf(with_diff=True, dry_run=True)
|
diagnosis['private']['regen_conf'] = regen_conf(with_diff=True, dry_run=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
diagnosis['security'] = {
|
diagnosis['security'] = {
|
||||||
|
|
Loading…
Add table
Reference in a new issue