mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
531 lines
17 KiB
Python
531 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
""" License
|
|
|
|
Copyright (C) 2013 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
|
|
|
|
"""
|
|
|
|
""" yunohost_tools.py
|
|
|
|
Specific tools
|
|
"""
|
|
import os
|
|
import yaml
|
|
import requests
|
|
import json
|
|
import errno
|
|
import logging
|
|
import subprocess
|
|
import pwd
|
|
from collections import OrderedDict
|
|
|
|
import apt
|
|
import apt.progress
|
|
|
|
from moulinette.core import MoulinetteError, init_authenticator
|
|
from moulinette.utils.log import getActionLogger
|
|
from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list
|
|
from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain
|
|
from yunohost.dyndns import dyndns_subscribe
|
|
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
|
|
from yunohost.utils.packages import ynh_packages_version
|
|
|
|
apps_setting_path= '/etc/yunohost/apps/'
|
|
|
|
logger = getActionLogger('yunohost.tools')
|
|
|
|
|
|
def tools_ldapinit():
|
|
"""
|
|
YunoHost LDAP initialization
|
|
|
|
|
|
"""
|
|
|
|
# Instantiate LDAP Authenticator
|
|
auth = init_authenticator(('ldap', 'default'),
|
|
{'uri': "ldap://localhost:389",
|
|
'base_dn': "dc=yunohost,dc=org",
|
|
'user_rdn': "cn=admin" })
|
|
auth.authenticate('yunohost')
|
|
|
|
with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') as f:
|
|
ldap_map = yaml.load(f)
|
|
|
|
for rdn, attr_dict in ldap_map['parents'].items():
|
|
try:
|
|
auth.add(rdn, attr_dict)
|
|
except:
|
|
pass
|
|
|
|
for rdn, attr_dict in ldap_map['children'].items():
|
|
try:
|
|
auth.add(rdn, attr_dict)
|
|
except:
|
|
pass
|
|
|
|
admin_dict = {
|
|
'cn': 'admin',
|
|
'uid': 'admin',
|
|
'description': 'LDAP Administrator',
|
|
'gidNumber': '1007',
|
|
'uidNumber': '1007',
|
|
'homeDirectory': '/home/admin',
|
|
'loginShell': '/bin/bash',
|
|
'objectClass': ['organizationalRole', 'posixAccount', 'simpleSecurityObject'],
|
|
'userPassword': 'yunohost'
|
|
}
|
|
|
|
auth.update('cn=admin', admin_dict)
|
|
|
|
# Force nscd to refresh cache to take admin creation into account
|
|
subprocess.call(['nscd', '-i', 'passwd'])
|
|
|
|
# Check admin actually exists now
|
|
try:
|
|
pwd.getpwnam("admin")
|
|
except KeyError:
|
|
logger.error(m18n.n('ldap_init_failed_to_create_admin'))
|
|
raise MoulinetteError(errno.EINVAL, m18n.n('installation_failed'))
|
|
|
|
logger.success(m18n.n('ldap_initialized'))
|
|
return auth
|
|
|
|
def tools_adminpw(auth, new_password):
|
|
"""
|
|
Change admin password
|
|
|
|
Keyword argument:
|
|
new_password
|
|
|
|
"""
|
|
try:
|
|
auth.con.passwd_s('cn=admin,dc=yunohost,dc=org', None, new_password)
|
|
except:
|
|
logger.exception('unable to change admin password')
|
|
raise MoulinetteError(errno.EPERM,
|
|
m18n.n('admin_password_change_failed'))
|
|
else:
|
|
logger.success(m18n.n('admin_password_changed'))
|
|
|
|
|
|
def tools_maindomain(auth, new_domain=None):
|
|
"""
|
|
Main domain consultaton or change tool
|
|
|
|
Keyword argument:
|
|
new_domain -- The new domain to be set as the main domain
|
|
|
|
"""
|
|
|
|
# If no new domain specified, we return the current main domain
|
|
if not new_domain:
|
|
return { 'current_main_domain': _get_maindomain() }
|
|
|
|
# Check domain exists
|
|
if new_domain not in domain_list(auth)['domains']:
|
|
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown'))
|
|
|
|
# Apply changes to ssl certs
|
|
ssl_key = "/etc/ssl/private/yunohost_key.pem"
|
|
ssl_crt = "/etc/ssl/private/yunohost_crt.pem"
|
|
new_ssl_key = "/etc/yunohost/certs/%s/key.pem" % new_domain
|
|
new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_domain
|
|
|
|
try:
|
|
|
|
if os.path.exists(ssl_key) or os.path.lexists(ssl_key) :
|
|
os.remove(ssl_key)
|
|
if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt) :
|
|
os.remove(ssl_crt)
|
|
|
|
os.symlink(new_ssl_key, ssl_key)
|
|
os.symlink(new_ssl_crt, ssl_crt)
|
|
|
|
_set_maindomain(new_domain)
|
|
except Exception as e:
|
|
print e
|
|
raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed'))
|
|
|
|
# Regen configurations
|
|
try:
|
|
with open('/etc/yunohost/installed', 'r') as f:
|
|
service_regen_conf()
|
|
except IOError:
|
|
pass
|
|
|
|
logger.success(m18n.n('maindomain_changed'))
|
|
|
|
|
|
def tools_postinstall(domain, password, ignore_dyndns=False):
|
|
"""
|
|
YunoHost post-install
|
|
|
|
Keyword argument:
|
|
domain -- YunoHost main domain
|
|
ignore_dyndns -- Do not subscribe domain to a DynDNS service
|
|
password -- YunoHost admin password
|
|
|
|
"""
|
|
dyndns = not ignore_dyndns
|
|
|
|
# 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:
|
|
dyndns = True
|
|
else:
|
|
raise MoulinetteError(errno.EEXIST,
|
|
m18n.n('dyndns_unavailable'))
|
|
|
|
logger.info(m18n.n('yunohost_installing'))
|
|
|
|
# Initialize LDAP for YunoHost
|
|
# TODO: Improve this part by integrate ldapinit into conf_regen hook
|
|
auth = tools_ldapinit()
|
|
|
|
# Create required folders
|
|
folders_to_create = [
|
|
'/etc/yunohost/apps',
|
|
'/etc/yunohost/certs',
|
|
'/var/cache/yunohost/repo',
|
|
'/home/yunohost.backup',
|
|
'/home/yunohost.app'
|
|
]
|
|
|
|
for folder in folders_to_create:
|
|
try:
|
|
os.listdir(folder)
|
|
except OSError:
|
|
os.makedirs(folder)
|
|
|
|
# Change folders permissions
|
|
os.system('chmod 755 /home/yunohost.app')
|
|
|
|
# Set hostname to avoid amavis bug
|
|
if os.system('hostname -d') != 0:
|
|
os.system('hostname yunohost.yunohost.org')
|
|
|
|
# Add a temporary SSOwat rule to redirect SSO to admin page
|
|
try:
|
|
with open('/etc/ssowat/conf.json.persistent') as json_conf:
|
|
ssowat_conf = json.loads(str(json_conf.read()))
|
|
except ValueError as e:
|
|
raise MoulinetteError(errno.EINVAL,
|
|
m18n.n('ssowat_persistent_conf_read_error', error=e.strerror))
|
|
except IOError:
|
|
ssowat_conf = {}
|
|
|
|
if 'redirected_urls' not in ssowat_conf:
|
|
ssowat_conf['redirected_urls'] = {}
|
|
|
|
ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin'
|
|
|
|
try:
|
|
with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
|
|
json.dump(ssowat_conf, f, sort_keys=True, indent=4)
|
|
except IOError as e:
|
|
raise MoulinetteError(errno.EPERM,
|
|
m18n.n('ssowat_persistent_conf_write_error', error=e.strerror))
|
|
|
|
|
|
os.system('chmod 644 /etc/ssowat/conf.json.persistent')
|
|
|
|
# Create SSL CA
|
|
service_regen_conf(['ssl'], force=True)
|
|
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
|
|
command_list = [
|
|
'echo "01" > %s/serial' % ssl_dir,
|
|
'rm %s/index.txt' % ssl_dir,
|
|
'touch %s/index.txt' % ssl_dir,
|
|
'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir),
|
|
'sed -i "s/yunohost.org/%s/g" %s/openssl.ca.cnf ' % (domain, ssl_dir),
|
|
'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch' % (ssl_dir, ssl_dir, ssl_dir),
|
|
'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir,
|
|
'update-ca-certificates'
|
|
]
|
|
|
|
for command in command_list:
|
|
if os.system(command) != 0:
|
|
raise MoulinetteError(errno.EPERM,
|
|
m18n.n('yunohost_ca_creation_failed'))
|
|
|
|
# New domain config
|
|
domain_add(auth, domain, dyndns)
|
|
tools_maindomain(auth, domain)
|
|
|
|
# Generate SSOwat configuration file
|
|
app_ssowatconf(auth)
|
|
|
|
# Change LDAP admin password
|
|
tools_adminpw(auth, password)
|
|
|
|
# Enable UPnP silently and reload firewall
|
|
firewall_upnp('enable', no_refresh=True)
|
|
|
|
os.system('touch /etc/yunohost/installed')
|
|
|
|
# Enable and start YunoHost firewall at boot time
|
|
os.system('update-rc.d yunohost-firewall enable')
|
|
os.system('service yunohost-firewall start')
|
|
|
|
service_regen_conf(force=True)
|
|
|
|
logger.success(m18n.n('yunohost_configured'))
|
|
|
|
|
|
def tools_update(ignore_apps=False, ignore_packages=False):
|
|
"""
|
|
Update apps & package cache, then display changelog
|
|
|
|
Keyword arguments:
|
|
ignore_apps -- Ignore app list update and changelog
|
|
ignore_packages -- Ignore apt cache update and changelog
|
|
|
|
"""
|
|
packages = []
|
|
if not ignore_packages:
|
|
cache = apt.Cache()
|
|
|
|
# Update APT cache
|
|
logger.info(m18n.n('updating_apt_cache'))
|
|
if not cache.update():
|
|
raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed'))
|
|
|
|
logger.info(m18n.n('done'))
|
|
|
|
cache.open(None)
|
|
cache.upgrade(True)
|
|
|
|
# Add changelogs to the result
|
|
for pkg in cache.get_changes():
|
|
packages.append({
|
|
'name': pkg.name,
|
|
'fullname': pkg.fullname,
|
|
'changelog': pkg.get_changelog()
|
|
})
|
|
|
|
apps = []
|
|
if not ignore_apps:
|
|
try:
|
|
app_fetchlist()
|
|
except MoulinetteError:
|
|
pass
|
|
app_list = os.listdir(apps_setting_path)
|
|
if len(app_list) > 0:
|
|
for app_id in app_list:
|
|
if '__' in app_id:
|
|
original_app_id = app_id[:app_id.index('__')]
|
|
else:
|
|
original_app_id = app_id
|
|
|
|
current_app_dict = app_info(app_id, raw=True)
|
|
new_app_dict = app_info(original_app_id, raw=True)
|
|
|
|
# Custom app
|
|
if new_app_dict is None or 'lastUpdate' not in new_app_dict or 'git' not in new_app_dict:
|
|
continue
|
|
|
|
if (new_app_dict['lastUpdate'] > current_app_dict['lastUpdate']) \
|
|
or ('update_time' not in current_app_dict['settings'] \
|
|
and (new_app_dict['lastUpdate'] > current_app_dict['settings']['install_time'])) \
|
|
or ('update_time' in current_app_dict['settings'] \
|
|
and (new_app_dict['lastUpdate'] > current_app_dict['settings']['update_time'])):
|
|
apps.append({
|
|
'id': app_id,
|
|
'label': current_app_dict['settings']['label']
|
|
})
|
|
|
|
if len(apps) == 0 and len(packages) == 0:
|
|
logger.info(m18n.n('packages_no_upgrade'))
|
|
|
|
return {'packages': packages, 'apps': apps}
|
|
|
|
|
|
def tools_upgrade(auth, ignore_apps=False, ignore_packages=False):
|
|
"""
|
|
Update apps & package cache, then display changelog
|
|
|
|
Keyword arguments:
|
|
ignore_apps -- Ignore apps upgrade
|
|
ignore_packages -- Ignore APT packages upgrade
|
|
|
|
"""
|
|
failure = False
|
|
|
|
# Retrieve interface
|
|
is_api = True if msettings.get('interface') == 'api' else False
|
|
|
|
if not ignore_packages:
|
|
cache = apt.Cache()
|
|
cache.open(None)
|
|
cache.upgrade(True)
|
|
|
|
# If API call
|
|
if is_api:
|
|
critical_packages = ("moulinette", "yunohost",
|
|
"yunohost-admin", "ssowat", "python")
|
|
critical_upgrades = set()
|
|
|
|
for pkg in cache.get_changes():
|
|
if pkg.name in critical_packages:
|
|
critical_upgrades.add(pkg.name)
|
|
# Temporarily keep package ...
|
|
pkg.mark_keep()
|
|
|
|
# ... and set a hourly cron up to upgrade critical packages
|
|
if critical_upgrades:
|
|
logger.info(m18n.n('packages_upgrade_critical_later',
|
|
packages=', '.join(critical_upgrades)))
|
|
with open('/etc/cron.d/yunohost-upgrade', 'w+') as f:
|
|
f.write('00 * * * * root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin apt-get install %s -y && rm -f /etc/cron.d/yunohost-upgrade\n' % ' '.join(critical_upgrades))
|
|
|
|
if cache.get_changes():
|
|
logger.info(m18n.n('upgrading_packages'))
|
|
|
|
try:
|
|
# Apply APT changes
|
|
# TODO: Logs output for the API
|
|
cache.commit(apt.progress.text.AcquireProgress(),
|
|
apt.progress.base.InstallProgress())
|
|
except Exception as e:
|
|
failure = True
|
|
logging.warning('unable to upgrade packages: %s' % str(e))
|
|
logger.error(m18n.n('packages_upgrade_failed'))
|
|
else:
|
|
logger.info(m18n.n('done'))
|
|
else:
|
|
logger.info(m18n.n('packages_no_upgrade'))
|
|
|
|
if not ignore_apps:
|
|
try:
|
|
app_upgrade(auth)
|
|
except Exception as e:
|
|
failure = True
|
|
logging.warning('unable to upgrade apps: %s' % str(e))
|
|
logger.error(m18n.n('app_upgrade_failed'))
|
|
|
|
if not failure:
|
|
logger.success(m18n.n('system_upgraded'))
|
|
|
|
# Return API logs if it is an API call
|
|
if is_api:
|
|
return {"log": service_log('yunohost-api', number="100").values()[0]}
|
|
|
|
|
|
def tools_diagnosis(auth, private=False):
|
|
"""
|
|
Return global info about current yunohost instance to help debugging
|
|
|
|
"""
|
|
diagnosis = OrderedDict();
|
|
|
|
# Debian release
|
|
try:
|
|
with open('/etc/debian_version', 'r') as f:
|
|
debian_version = f.read().rstrip()
|
|
except IOError as e:
|
|
logger.warning(m18n.n('diagnosis_debian_version_error', error=format(e)), exc_info=1)
|
|
else:
|
|
diagnosis['host'] = "Debian %s" % debian_version
|
|
|
|
# Kernel version
|
|
try:
|
|
with open('/proc/sys/kernel/osrelease', 'r') as f:
|
|
kernel_version = f.read().rstrip()
|
|
except IOError as e:
|
|
logger.warning(m18n.n('diagnosis_kernel_version_error', error=format(e)), exc_info=1)
|
|
else:
|
|
diagnosis['kernel'] = kernel_version
|
|
|
|
# Packages version
|
|
diagnosis['packages'] = ynh_packages_version()
|
|
|
|
# Server basic monitoring
|
|
diagnosis['system'] = OrderedDict()
|
|
try:
|
|
disks = monitor_disk(units=['filesystem'], human_readable=True)
|
|
except MoulinetteError as e:
|
|
logger.warning(m18n.n('diagnosis_monitor_disk_error', error=format(e)), exc_info=1)
|
|
else:
|
|
diagnosis['system']['disks'] = {}
|
|
for disk in disks:
|
|
diagnosis['system']['disks'][disk] = 'Mounted on %s, %s (%s free)' % (
|
|
disks[disk]['mnt_point'],
|
|
disks[disk]['size'],
|
|
disks[disk]['avail']
|
|
)
|
|
|
|
try:
|
|
system = monitor_system(units=['cpu', 'memory'], human_readable=True)
|
|
except MoulinetteError as e:
|
|
logger.warning(m18n.n('diagnosis_monitor_system_error', error=format(e)), exc_info=1)
|
|
else:
|
|
diagnosis['system']['memory'] = {
|
|
'ram' : '%s (%s free)' % (system['memory']['ram']['total'], system['memory']['ram']['free']),
|
|
'swap' : '%s (%s free)' % (system['memory']['swap']['total'], system['memory']['swap']['free']),
|
|
}
|
|
|
|
# Services status
|
|
services = service_status()
|
|
diagnosis['services'] = {}
|
|
|
|
for service in services:
|
|
diagnosis['services'][service] = "%s (%s)" % (services[service]['status'], services[service]['loaded'])
|
|
|
|
# YNH Applications
|
|
try:
|
|
applications = app_list()['apps']
|
|
except MoulinetteError as e:
|
|
diagnosis['applications'] = m18n.n('diagnosis_no_apps')
|
|
else:
|
|
diagnosis['applications'] = {}
|
|
for application in applications:
|
|
if application['installed']:
|
|
diagnosis['applications'][application['id']] = application['label'] if application['label'] else application['name']
|
|
|
|
# Private data
|
|
if private:
|
|
diagnosis['private'] = OrderedDict()
|
|
# Public IP
|
|
diagnosis['private']['public_ip'] = {}
|
|
try:
|
|
diagnosis['private']['public_ip']['IPv4'] = get_public_ip(4)
|
|
except MoulinetteError as e:
|
|
pass
|
|
try:
|
|
diagnosis['private']['public_ip']['IPv6'] = get_public_ip(6)
|
|
except MoulinetteError as e:
|
|
pass
|
|
|
|
# Domains
|
|
diagnosis['private']['domains'] = domain_list(auth)['domains']
|
|
|
|
return diagnosis
|