moulinette/src/yunohost/tools.py
2016-12-07 14:32:42 -05:00

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