mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Init
This commit is contained in:
commit
8394b3d428
17 changed files with 4206 additions and 0 deletions
0
__init__.py
Executable file
0
__init__.py
Executable file
52
backup.py
Normal file
52
backup.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# -*- 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_backup.py
|
||||||
|
|
||||||
|
Manage backups
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
import glob
|
||||||
|
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
|
def backup_init(helper=False):
|
||||||
|
"""
|
||||||
|
Init Tahoe-LAFS configuration
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
helper -- Init as a helper node rather than a "helped" one
|
||||||
|
|
||||||
|
"""
|
||||||
|
tahoe_cfg_dir = '/usr/share/yunohost/yunohost-config/backup'
|
||||||
|
if helper:
|
||||||
|
configure_cmd = '/configure_tahoe.sh helper'
|
||||||
|
else:
|
||||||
|
configure_cmd = '/configure_tahoe.sh'
|
||||||
|
|
||||||
|
os.system('tahoe create-client /home/yunohost.backup/tahoe')
|
||||||
|
os.system('/bin/bash %s%s' % (tahoe_cfg_dir, configure_cmd))
|
||||||
|
os.system('cp %s/tahoe.cfg /home/yunohost.backup/tahoe/' % tahoe_cfg_dir)
|
||||||
|
#os.system('update-rc.d tahoe-lafs defaults')
|
||||||
|
#os.system('service tahoe-lafs restart')
|
67
data/checkupdate
Normal file
67
data/checkupdate
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! -d /tmp/yunohost ];
|
||||||
|
then
|
||||||
|
mkdir /tmp/yunohost
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /tmp/yunohost/changelog ];
|
||||||
|
then
|
||||||
|
rm /tmp/yunohost/changelog
|
||||||
|
fi
|
||||||
|
|
||||||
|
apt-get update -y > /dev/null 2>&1
|
||||||
|
if [[ $? != 0 ]];
|
||||||
|
then
|
||||||
|
exit 2
|
||||||
|
else
|
||||||
|
echo OK > /tmp/yunohost/update_status
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set $DIRCACHE
|
||||||
|
eval `/usr/bin/apt-config shell DIRCACHE Dir::Cache`
|
||||||
|
|
||||||
|
|
||||||
|
# get the list of packages which are pending an upgrade
|
||||||
|
PKGNAMES=`/usr/bin/apt-get -q -y --ignore-hold --allow-unauthenticated -s dist-upgrade | \
|
||||||
|
/bin/grep ^Inst | /usr/bin/cut -d\ -f2 | /usr/bin/sort`
|
||||||
|
|
||||||
|
if [[ $PKGNAMES = "" ]];
|
||||||
|
then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$PKGNAMES" ] ; then
|
||||||
|
|
||||||
|
# do the upgrade downloads
|
||||||
|
/usr/bin/apt-get --ignore-hold -qq -d --allow-unauthenticated --force-yes dist-upgrade > /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
PKGPATH="/${DIRCACHE}archives/"
|
||||||
|
for PKG in $PKGNAMES ; do
|
||||||
|
VER=`LC_ALL=C /usr/bin/apt-cache policy $PKG |\
|
||||||
|
/bin/grep Candidate: | /usr/bin/cut -f 4 -d \ `
|
||||||
|
OLDVER=`LC_ALL=C /usr/bin/apt-cache policy $PKG |\
|
||||||
|
/bin/grep Installed: | /usr/bin/cut -f 4 -d \ `
|
||||||
|
VERFILE=`echo "$VER" | /bin/sed -e "s/:/%3a/g"`
|
||||||
|
if ls ${PKGPATH}${PKG}_${VERFILE}_*.deb >& /dev/null ; then
|
||||||
|
DEBS="$DEBS ${PKGPATH}${PKG}_${VERFILE}_*.deb"
|
||||||
|
fi
|
||||||
|
echo -e "$PKG $OLDVER -> $VER"
|
||||||
|
done
|
||||||
|
|
||||||
|
MISSING_DEBS=`apt-get -y --ignore-hold --allow-unauthenticated --print-uris dist-upgrade \
|
||||||
|
| grep "file:" \
|
||||||
|
| sed "s/'file:\(.*\)' .*/\1/g"`
|
||||||
|
|
||||||
|
DEBS=`echo $MISSING_DEBS $DEBS | /usr/bin/sort`
|
||||||
|
|
||||||
|
if [[ $DEBS = "" ]];
|
||||||
|
then
|
||||||
|
exit 3
|
||||||
|
else
|
||||||
|
if [ -x /usr/bin/apt-listchanges ] ; then
|
||||||
|
/usr/bin/apt-listchanges --which=both -f text $DEBS > /tmp/yunohost/changelog 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
10
data/firewall.yml
Normal file
10
data/firewall.yml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
uPnP:
|
||||||
|
enabled: false
|
||||||
|
TCP: [22, 25, 53, 80, 443, 465, 993, 5222, 5269, 5290]
|
||||||
|
UDP: [53]
|
||||||
|
ipv4:
|
||||||
|
TCP: [22, 25, 53, 80, 443, 465, 993, 5222, 5269, 5290]
|
||||||
|
UDP: [53]
|
||||||
|
ipv6:
|
||||||
|
TCP: [22, 25, 53, 80, 443, 465, 993, 5222, 5269, 5290]
|
||||||
|
UDP: [53]
|
56
data/ldap_scheme.yml
Normal file
56
data/ldap_scheme.yml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
parents:
|
||||||
|
ou=users:
|
||||||
|
ou: users
|
||||||
|
objectClass:
|
||||||
|
- organizationalUnit
|
||||||
|
- top
|
||||||
|
|
||||||
|
ou=domains:
|
||||||
|
ou: domains
|
||||||
|
objectClass:
|
||||||
|
- organizationalUnit
|
||||||
|
- top
|
||||||
|
|
||||||
|
ou=apps:
|
||||||
|
ou: apps
|
||||||
|
objectClass:
|
||||||
|
- organizationalUnit
|
||||||
|
- top
|
||||||
|
|
||||||
|
ou=groups:
|
||||||
|
ou: groups
|
||||||
|
objectClass:
|
||||||
|
- organizationalUnit
|
||||||
|
- top
|
||||||
|
ou=sudo:
|
||||||
|
ou: sudo
|
||||||
|
objectClass:
|
||||||
|
- organizationalUnit
|
||||||
|
- top
|
||||||
|
|
||||||
|
children:
|
||||||
|
cn=admins,ou=groups:
|
||||||
|
cn: admins
|
||||||
|
gidNumber: "4001"
|
||||||
|
memberUid: admin
|
||||||
|
objectClass:
|
||||||
|
- posixGroup
|
||||||
|
- top
|
||||||
|
|
||||||
|
cn=sftpusers,ou=groups:
|
||||||
|
cn: sftpusers
|
||||||
|
gidNumber: "4002"
|
||||||
|
memberUid: admin
|
||||||
|
objectClass:
|
||||||
|
- posixGroup
|
||||||
|
- top
|
||||||
|
|
||||||
|
cn=admin,ou=sudo:
|
||||||
|
cn: admin
|
||||||
|
sudoUser: admin
|
||||||
|
sudoHost: ALL
|
||||||
|
sudoCommand: ALL
|
||||||
|
sudoOption: "!authenticate"
|
||||||
|
objectClass:
|
||||||
|
- sudoRole
|
||||||
|
- top
|
38
data/services.yml
Normal file
38
data/services.yml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
nginx:
|
||||||
|
status: service
|
||||||
|
log: /var/log/nginx
|
||||||
|
bind9:
|
||||||
|
status: service
|
||||||
|
log: /var/log/daemon.log
|
||||||
|
dovecot:
|
||||||
|
status: service
|
||||||
|
log: [/var/log/mail.log,/var/log/mail.err]
|
||||||
|
postfix:
|
||||||
|
status: service
|
||||||
|
log: [/var/log/mail.log,/var/log/mail.err]
|
||||||
|
mysql:
|
||||||
|
status: service
|
||||||
|
log: [/var/log/mysql.log,/var/log/mysql.err]
|
||||||
|
glances:
|
||||||
|
status: service
|
||||||
|
tahoe-lafs:
|
||||||
|
status: ps aux | grep tahoe |grep -v grep
|
||||||
|
log: /home/yunohost.backup/tahoe/logs/twistd.log
|
||||||
|
ssh:
|
||||||
|
status: service
|
||||||
|
log: /var/log/auth.log
|
||||||
|
metronome:
|
||||||
|
status: metronomectl status
|
||||||
|
log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err]
|
||||||
|
slapd:
|
||||||
|
status: service
|
||||||
|
log: /var/log/syslog
|
||||||
|
php5-fpm:
|
||||||
|
status: service
|
||||||
|
log: /var/log/php5-fpm.log
|
||||||
|
yunohost-api:
|
||||||
|
status: cat /usr/share/pyshared/yunohost-cli/twistd.pid
|
||||||
|
log: /var/log/yunohost.log
|
||||||
|
postgrey:
|
||||||
|
status: service
|
||||||
|
log: /var/log/mail.log
|
5
data/upgrade
Normal file
5
data/upgrade
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/bin/bash
|
||||||
|
rm /tmp/yunohost/update_status
|
||||||
|
sudo apt-get upgrade -y > /tmp/yunohost/update_log 2>&1
|
||||||
|
if [ $(echo $?) = 0 ]; then echo "OK" > /tmp/yunohost/upgrade_status; else echo "NOK" > /tmp/yunohost/upgrade_status; fi
|
||||||
|
rm /tmp/yunohost/upgrade.run
|
308
domain.py
Normal file
308
domain.py
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
# -*- 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_domain.py
|
||||||
|
|
||||||
|
Manage domains
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
import errno
|
||||||
|
from urllib import urlopen
|
||||||
|
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
|
|
||||||
|
def domain_list(auth, filter=None, limit=None, offset=None):
|
||||||
|
"""
|
||||||
|
List domains
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
filter -- LDAP filter used to search
|
||||||
|
offset -- Starting number for domain fetching
|
||||||
|
limit -- Maximum number of domain fetched
|
||||||
|
|
||||||
|
"""
|
||||||
|
result_list = []
|
||||||
|
|
||||||
|
# Set default arguments values
|
||||||
|
if offset is None:
|
||||||
|
offset = 0
|
||||||
|
if limit is None:
|
||||||
|
limit = 1000
|
||||||
|
if filter is None:
|
||||||
|
filter = 'virtualdomain=*'
|
||||||
|
|
||||||
|
result = auth.search('ou=domains,dc=yunohost,dc=org', filter, ['virtualdomain'])
|
||||||
|
|
||||||
|
if len(result) > offset and limit > 0:
|
||||||
|
for domain in result[offset:offset+limit]:
|
||||||
|
result_list.append(domain['virtualdomain'][0])
|
||||||
|
return { 'domains': result_list }
|
||||||
|
|
||||||
|
|
||||||
|
def domain_add(auth, domains, main=False, dyndns=False):
|
||||||
|
"""
|
||||||
|
Create a custom domain
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
domains -- Domain name to add
|
||||||
|
main -- Is the main domain
|
||||||
|
dyndns -- Subscribe to DynDNS
|
||||||
|
|
||||||
|
"""
|
||||||
|
attr_dict = { 'objectClass' : ['mailDomain', 'top'] }
|
||||||
|
ip = str(urlopen('http://ip.yunohost.org').read())
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
timestamp = str(now.year) + str(now.month) + str(now.day)
|
||||||
|
result = []
|
||||||
|
|
||||||
|
if not isinstance(domains, list):
|
||||||
|
domains = [ domains ]
|
||||||
|
|
||||||
|
for domain in domains:
|
||||||
|
if domain in domain_list(auth)['domains']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# DynDNS domain
|
||||||
|
if dyndns:
|
||||||
|
if len(domain.split('.')) < 3:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_invalid'))
|
||||||
|
import requests
|
||||||
|
from yunohost.dyndns import dyndns_subscribe
|
||||||
|
|
||||||
|
r = requests.get('http://dyndns.yunohost.org/domains')
|
||||||
|
dyndomains = json.loads(r.text)
|
||||||
|
dyndomain = '.'.join(domain.split('.')[1:])
|
||||||
|
if dyndomain in dyndomains:
|
||||||
|
if os.path.exists('/etc/cron.d/yunohost-dyndns'):
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('domain_dyndns_already_subscribed'))
|
||||||
|
dyndns_subscribe(domain=domain)
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('domain_dyndns_root_unknown'))
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
|
||||||
|
ssl_domain_path = '/etc/yunohost/certs/%s' % domain
|
||||||
|
with open('%s/serial' % ssl_dir, 'r') as f:
|
||||||
|
serial = f.readline().rstrip()
|
||||||
|
try: os.listdir(ssl_domain_path)
|
||||||
|
except OSError: os.makedirs(ssl_domain_path)
|
||||||
|
|
||||||
|
command_list = [
|
||||||
|
'cp %s/openssl.cnf %s' % (ssl_dir, ssl_domain_path),
|
||||||
|
'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, ssl_domain_path),
|
||||||
|
'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch'
|
||||||
|
% (ssl_domain_path, ssl_dir, ssl_dir),
|
||||||
|
'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch'
|
||||||
|
% (ssl_domain_path, ssl_dir, ssl_dir),
|
||||||
|
'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % ssl_domain_path,
|
||||||
|
'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, ssl_domain_path),
|
||||||
|
'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, ssl_domain_path),
|
||||||
|
'chmod 755 %s' % ssl_domain_path,
|
||||||
|
'chmod 640 %s/key.pem' % ssl_domain_path,
|
||||||
|
'chmod 640 %s/crt.pem' % ssl_domain_path,
|
||||||
|
'chmod 600 %s/openssl.cnf' % ssl_domain_path,
|
||||||
|
'chown root:metronome %s/key.pem' % ssl_domain_path,
|
||||||
|
'chown root:metronome %s/crt.pem' % ssl_domain_path
|
||||||
|
]
|
||||||
|
|
||||||
|
for command in command_list:
|
||||||
|
if os.system(command) != 0:
|
||||||
|
raise MoulinetteError(errno.EIO,
|
||||||
|
m18n.n('domain_cert_gen_failed'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth.validate_uniqueness({ 'virtualdomain': domain })
|
||||||
|
except MoulinetteError:
|
||||||
|
raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists'))
|
||||||
|
|
||||||
|
|
||||||
|
attr_dict['virtualdomain'] = domain
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('/var/lib/bind/%s.zone' % domain) as f: pass
|
||||||
|
except IOError as e:
|
||||||
|
zone_lines = [
|
||||||
|
'$TTL 38400',
|
||||||
|
'%s. IN SOA ns.%s. root.%s. %s 10800 3600 604800 38400' % (domain, domain, domain, timestamp),
|
||||||
|
'%s. IN NS ns.%s.' % (domain, domain),
|
||||||
|
'%s. IN A %s' % (domain, ip),
|
||||||
|
'%s. IN MX 5 %s.' % (domain, domain),
|
||||||
|
'%s. IN TXT "v=spf1 mx a -all"' % domain,
|
||||||
|
'ns.%s. IN A %s' % (domain, ip),
|
||||||
|
'_xmpp-client._tcp.%s. IN SRV 0 5 5222 %s.' % (domain, domain),
|
||||||
|
'_xmpp-server._tcp.%s. IN SRV 0 5 5269 %s.' % (domain, domain),
|
||||||
|
'_jabber._tcp.%s. IN SRV 0 5 5269 %s.' % (domain, domain),
|
||||||
|
]
|
||||||
|
if main:
|
||||||
|
zone_lines.extend([
|
||||||
|
'pubsub.%s. IN A %s' % (domain, ip),
|
||||||
|
'muc.%s. IN A %s' % (domain, ip),
|
||||||
|
'vjud.%s. IN A %s' % (domain, ip)
|
||||||
|
])
|
||||||
|
with open('/var/lib/bind/%s.zone' % domain, 'w') as zone:
|
||||||
|
for line in zone_lines:
|
||||||
|
zone.write(line + '\n')
|
||||||
|
|
||||||
|
os.system('chown bind /var/lib/bind/%s.zone' % domain)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EEXIST,
|
||||||
|
m18n.n('domain_zone_exists'))
|
||||||
|
|
||||||
|
conf_lines = [
|
||||||
|
'zone "%s" {' % domain,
|
||||||
|
' type master;',
|
||||||
|
' file "/var/lib/bind/%s.zone";' % domain,
|
||||||
|
' allow-transfer {',
|
||||||
|
' 127.0.0.1;',
|
||||||
|
' localnets;',
|
||||||
|
' };',
|
||||||
|
'};'
|
||||||
|
]
|
||||||
|
with open('/etc/bind/named.conf.local', 'a') as conf:
|
||||||
|
for line in conf_lines:
|
||||||
|
conf.write(line + '\n')
|
||||||
|
|
||||||
|
os.system('service bind9 reload')
|
||||||
|
|
||||||
|
# XMPP
|
||||||
|
try:
|
||||||
|
with open('/etc/metronome/conf.d/%s.cfg.lua' % domain) as f: pass
|
||||||
|
except IOError as e:
|
||||||
|
conf_lines = [
|
||||||
|
'VirtualHost "%s"' % domain,
|
||||||
|
' ssl = {',
|
||||||
|
' key = "%s/key.pem";' % ssl_domain_path,
|
||||||
|
' certificate = "%s/crt.pem";' % ssl_domain_path,
|
||||||
|
' }',
|
||||||
|
' authentication = "ldap2"',
|
||||||
|
' ldap = {',
|
||||||
|
' hostname = "localhost",',
|
||||||
|
' user = {',
|
||||||
|
' basedn = "ou=users,dc=yunohost,dc=org",',
|
||||||
|
' filter = "(&(objectClass=posixAccount)(mail=*@%s))",' % domain,
|
||||||
|
' usernamefield = "mail",',
|
||||||
|
' namefield = "cn",',
|
||||||
|
' },',
|
||||||
|
' }',
|
||||||
|
]
|
||||||
|
with open('/etc/metronome/conf.d/%s.cfg.lua' % domain, 'w') as conf:
|
||||||
|
for line in conf_lines:
|
||||||
|
conf.write(line + '\n')
|
||||||
|
|
||||||
|
os.system('mkdir -p /var/lib/metronome/%s/pep' % domain.replace('.', '%2e'))
|
||||||
|
os.system('chown -R metronome: /var/lib/metronome/')
|
||||||
|
os.system('chown -R metronome: /etc/metronome/conf.d/')
|
||||||
|
os.system('service metronome restart')
|
||||||
|
|
||||||
|
|
||||||
|
# Nginx
|
||||||
|
os.system('cp /usr/share/yunohost/yunohost-config/nginx/template.conf /etc/nginx/conf.d/%s.conf' % domain)
|
||||||
|
os.system('mkdir /etc/nginx/conf.d/%s.d/' % domain)
|
||||||
|
os.system('sed -i s/yunohost.org/%s/g /etc/nginx/conf.d/%s.conf' % (domain, domain))
|
||||||
|
os.system('service nginx reload')
|
||||||
|
|
||||||
|
if auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict):
|
||||||
|
result.append(domain)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EIO, m18n.n('domain_creation_failed'))
|
||||||
|
|
||||||
|
|
||||||
|
os.system('yunohost app ssowatconf > /dev/null 2>&1')
|
||||||
|
|
||||||
|
msignals.display(m18n.n('domain_created'), 'success')
|
||||||
|
return { 'domains': result }
|
||||||
|
|
||||||
|
|
||||||
|
def domain_remove(auth, domains):
|
||||||
|
"""
|
||||||
|
Delete domains
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
domains -- Domain(s) to delete
|
||||||
|
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
domains_list = domain_list(auth)['domains']
|
||||||
|
|
||||||
|
if not isinstance(domains, list):
|
||||||
|
domains = [ domains ]
|
||||||
|
|
||||||
|
for domain in domains:
|
||||||
|
if domain not in domains_list:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown'))
|
||||||
|
|
||||||
|
# Check if apps are installed on the domain
|
||||||
|
for app in os.listdir('/etc/yunohost/apps/'):
|
||||||
|
with open('/etc/yunohost/apps/' + app +'/settings.yml') as f:
|
||||||
|
try:
|
||||||
|
app_domain = yaml.load(f)['domain']
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if app_domain == domain:
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('domain_uninstall_app_first'))
|
||||||
|
|
||||||
|
if auth.remove('virtualdomain=' + domain + ',ou=domains'):
|
||||||
|
try:
|
||||||
|
shutil.rmtree('/etc/yunohost/certs/%s' % domain)
|
||||||
|
os.remove('/var/lib/bind/%s.zone' % domain)
|
||||||
|
shutil.rmtree('/var/lib/metronome/%s' % domain.replace('.', '%2e'))
|
||||||
|
os.remove('/etc/metronome/conf.d/%s.cfg.lua' % domain)
|
||||||
|
shutil.rmtree('/etc/nginx/conf.d/%s.d' % domain)
|
||||||
|
os.remove('/etc/nginx/conf.d/%s.conf' % domain)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
with open('/etc/bind/named.conf.local', 'r') as conf:
|
||||||
|
conf_lines = conf.readlines()
|
||||||
|
with open('/etc/bind/named.conf.local', 'w') as conf:
|
||||||
|
in_block = False
|
||||||
|
for line in conf_lines:
|
||||||
|
if re.search(r'^zone "%s' % domain, line):
|
||||||
|
in_block = True
|
||||||
|
if in_block:
|
||||||
|
if re.search(r'^};$', line):
|
||||||
|
in_block = False
|
||||||
|
else:
|
||||||
|
conf.write(line)
|
||||||
|
result.append(domain)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EIO, m18n.n('domain_deletion_failed'))
|
||||||
|
|
||||||
|
os.system('yunohost app ssowatconf > /dev/null 2>&1')
|
||||||
|
os.system('service nginx reload')
|
||||||
|
os.system('service bind9 reload')
|
||||||
|
os.system('service metronome restart')
|
||||||
|
|
||||||
|
msignals.display(m18n.n('domain_deleted'), 'success')
|
||||||
|
return { 'domains': result }
|
171
dyndns.py
Normal file
171
dyndns.py
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
# -*- 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_dyndns.py
|
||||||
|
|
||||||
|
Subscribe and Update DynDNS Hosts
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import glob
|
||||||
|
import base64
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
|
|
||||||
|
def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None):
|
||||||
|
"""
|
||||||
|
Subscribe to a DynDNS service
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
domain -- Full domain to subscribe with
|
||||||
|
key -- Public DNS key
|
||||||
|
subscribe_host -- Dynette HTTP API to subscribe to
|
||||||
|
|
||||||
|
"""
|
||||||
|
if domain is None:
|
||||||
|
with open('/etc/yunohost/current_host', 'r') as f:
|
||||||
|
domain = f.readline().rstrip()
|
||||||
|
|
||||||
|
# Verify if domain is available
|
||||||
|
if requests.get('http://%s/test/%s' % (subscribe_host, domain)).status_code != 200:
|
||||||
|
raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable'))
|
||||||
|
|
||||||
|
if key is None:
|
||||||
|
if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0:
|
||||||
|
os.makedirs('/etc/yunohost/dyndns')
|
||||||
|
print(_("DNS key is being generated, it may take a while..."))
|
||||||
|
os.system('cd /etc/yunohost/dyndns && dnssec-keygen -a hmac-md5 -b 128 -n USER %s' % domain)
|
||||||
|
os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private')
|
||||||
|
|
||||||
|
key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0]
|
||||||
|
with open(key_file) as f:
|
||||||
|
key = f.readline().strip().split(' ')[-1]
|
||||||
|
|
||||||
|
# Send subscription
|
||||||
|
r = requests.post('http://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={ 'subdomain': domain })
|
||||||
|
if r.status_code != 201:
|
||||||
|
try: error = json.loads(r.text)['error']
|
||||||
|
except: error = "Server error"
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('dyndns_registration_failed') % error)
|
||||||
|
|
||||||
|
msignals.display(m18n.n('dyndns_registered'), 'success')
|
||||||
|
|
||||||
|
dyndns_installcron()
|
||||||
|
|
||||||
|
|
||||||
|
def dyndns_update(dyn_host="dynhost.yunohost.org", domain=None, key=None, ip=None):
|
||||||
|
"""
|
||||||
|
Update IP on DynDNS platform
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
domain -- Full domain to subscribe with
|
||||||
|
dyn_host -- Dynette DNS server to inform
|
||||||
|
key -- Public DNS key
|
||||||
|
ip -- IP address to send
|
||||||
|
|
||||||
|
"""
|
||||||
|
if domain is None:
|
||||||
|
with open('/etc/yunohost/current_host', 'r') as f:
|
||||||
|
domain = f.readline().rstrip()
|
||||||
|
|
||||||
|
if ip is None:
|
||||||
|
new_ip = requests.get('http://ip.yunohost.org').text
|
||||||
|
else:
|
||||||
|
new_ip = ip
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('/etc/yunohost/dyndns/old_ip', 'r') as f:
|
||||||
|
old_ip = f.readline().rstrip()
|
||||||
|
except IOError:
|
||||||
|
old_ip = '0.0.0.0'
|
||||||
|
|
||||||
|
if old_ip != new_ip:
|
||||||
|
host = domain.split('.')[1:]
|
||||||
|
host = '.'.join(host)
|
||||||
|
lines = [
|
||||||
|
'server %s' % dyn_host,
|
||||||
|
'zone %s' % host,
|
||||||
|
'update delete %s. A' % domain,
|
||||||
|
'update delete %s. MX' % domain,
|
||||||
|
'update delete %s. TXT' % domain,
|
||||||
|
'update delete pubsub.%s. A' % domain,
|
||||||
|
'update delete muc.%s. A' % domain,
|
||||||
|
'update delete vjud.%s. A' % domain,
|
||||||
|
'update delete _xmpp-client._tcp.%s. SRV' % domain,
|
||||||
|
'update delete _xmpp-server._tcp.%s. SRV' % domain,
|
||||||
|
'update add %s. 1800 A %s' % (domain, new_ip),
|
||||||
|
'update add %s. 14400 MX 5 %s.' % (domain, domain),
|
||||||
|
'update add %s. 14400 TXT "v=spf1 a mx -all"' % domain,
|
||||||
|
'update add pubsub.%s. 1800 A %s' % (domain, new_ip),
|
||||||
|
'update add muc.%s. 1800 A %s' % (domain, new_ip),
|
||||||
|
'update add vjud.%s. 1800 A %s' % (domain, new_ip),
|
||||||
|
'update add _xmpp-client._tcp.%s. 14400 SRV 0 5 5222 %s.' % (domain, domain),
|
||||||
|
'update add _xmpp-server._tcp.%s. 14400 SRV 0 5 5269 %s.' % (domain, domain),
|
||||||
|
'show',
|
||||||
|
'send'
|
||||||
|
]
|
||||||
|
with open('/etc/yunohost/dyndns/zone', 'w') as zone:
|
||||||
|
for line in lines:
|
||||||
|
zone.write(line + '\n')
|
||||||
|
|
||||||
|
if key is None:
|
||||||
|
private_key_file = glob.glob('/etc/yunohost/dyndns/*.private')[0]
|
||||||
|
else:
|
||||||
|
private_key_file = key
|
||||||
|
if os.system('/usr/bin/nsupdate -k %s /etc/yunohost/dyndns/zone' % private_key_file) == 0:
|
||||||
|
msignals.display(m18n.n('dyndns_ip_updated'), 'success')
|
||||||
|
with open('/etc/yunohost/dyndns/old_ip', 'w') as f:
|
||||||
|
f.write(new_ip)
|
||||||
|
else:
|
||||||
|
os.system('rm /etc/yunohost/dyndns/old_ip > /dev/null 2>&1')
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('dyndns_ip_update_failed'))
|
||||||
|
|
||||||
|
|
||||||
|
def dyndns_installcron():
|
||||||
|
"""
|
||||||
|
Install IP update cron
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
with open('/etc/cron.d/yunohost-dyndns', 'w+') as f:
|
||||||
|
f.write('*/2 * * * * root yunohost dyndns update >> /dev/null')
|
||||||
|
|
||||||
|
msignals.display(m18n.n('dyndns_cron_installed'), 'success')
|
||||||
|
|
||||||
|
|
||||||
|
def dyndns_removecron():
|
||||||
|
"""
|
||||||
|
Remove IP update cron
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
os.remove("/etc/cron.d/yunohost-dyndns")
|
||||||
|
except:
|
||||||
|
raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed'))
|
||||||
|
|
||||||
|
msignals.display(m18n.n('dyndns_cron_removed'), 'success')
|
274
firewall.py
Normal file
274
firewall.py
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
# -*- 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_firewall.py
|
||||||
|
|
||||||
|
Manage firewall rules
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
import errno
|
||||||
|
try:
|
||||||
|
import miniupnpc
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write('Error: Yunohost CLI Require miniupnpc lib\n')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
|
|
||||||
|
def firewall_allow(port=None, protocol='TCP', ipv6=False, no_upnp=False):
|
||||||
|
"""
|
||||||
|
Allow connection port/protocol
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
port -- Port to open
|
||||||
|
protocol -- Protocol associated with port
|
||||||
|
ipv6 -- ipv6
|
||||||
|
no_upnp -- Do not request for uPnP
|
||||||
|
|
||||||
|
"""
|
||||||
|
port = int(port)
|
||||||
|
ipv = "ipv4"
|
||||||
|
protocols = [protocol]
|
||||||
|
|
||||||
|
firewall = firewall_list(raw=True)
|
||||||
|
|
||||||
|
upnp = not no_upnp and firewall['uPnP']['enabled']
|
||||||
|
|
||||||
|
if ipv6:
|
||||||
|
ipv = "ipv6"
|
||||||
|
|
||||||
|
if protocol == "Both":
|
||||||
|
protocols = ['UDP', 'TCP']
|
||||||
|
|
||||||
|
for protocol in protocols:
|
||||||
|
if upnp and port not in firewall['uPnP'][protocol]:
|
||||||
|
firewall['uPnP'][protocol].append(port)
|
||||||
|
if port not in firewall[ipv][protocol]:
|
||||||
|
firewall[ipv][protocol].append(port)
|
||||||
|
else:
|
||||||
|
msignals.display(m18n.n('port_already_opened') % port, 'warning')
|
||||||
|
|
||||||
|
with open('/etc/yunohost/firewall.yml', 'w') as f:
|
||||||
|
yaml.safe_dump(firewall, f, default_flow_style=False)
|
||||||
|
|
||||||
|
return firewall_reload()
|
||||||
|
|
||||||
|
|
||||||
|
def firewall_disallow(port=None, protocol='TCP', ipv6=False):
|
||||||
|
"""
|
||||||
|
Allow connection port/protocol
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
port -- Port to open
|
||||||
|
protocol -- Protocol associated with port
|
||||||
|
ipv6 -- ipv6
|
||||||
|
|
||||||
|
"""
|
||||||
|
port = int(port)
|
||||||
|
ipv = "ipv4"
|
||||||
|
protocols = [protocol]
|
||||||
|
|
||||||
|
firewall = firewall_list(raw=True)
|
||||||
|
|
||||||
|
if ipv6:
|
||||||
|
ipv = "ipv6"
|
||||||
|
|
||||||
|
if protocol == "Both":
|
||||||
|
protocols = ['UDP', 'TCP']
|
||||||
|
|
||||||
|
for protocol in protocols:
|
||||||
|
if port in firewall['uPnP'][protocol]:
|
||||||
|
firewall['uPnP'][protocol].remove(port)
|
||||||
|
if port in firewall[ipv][protocol]:
|
||||||
|
firewall[ipv][protocol].remove(port)
|
||||||
|
else:
|
||||||
|
msignals.display(m18n.n('port_already_closed') % port, 'warning')
|
||||||
|
|
||||||
|
with open('/etc/yunohost/firewall.yml', 'w') as f:
|
||||||
|
yaml.safe_dump(firewall, f, default_flow_style=False)
|
||||||
|
|
||||||
|
return firewall_reload()
|
||||||
|
|
||||||
|
|
||||||
|
def firewall_list(raw=False):
|
||||||
|
"""
|
||||||
|
List all firewall rules
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
raw -- Return the complete YAML dict
|
||||||
|
|
||||||
|
"""
|
||||||
|
with open('/etc/yunohost/firewall.yml') as f:
|
||||||
|
firewall = yaml.load(f)
|
||||||
|
|
||||||
|
if raw:
|
||||||
|
return firewall
|
||||||
|
else:
|
||||||
|
return { "openned_ports": firewall['ipv4']['TCP'] }
|
||||||
|
|
||||||
|
|
||||||
|
def firewall_reload():
|
||||||
|
"""
|
||||||
|
Reload all firewall rules
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yunohost.hook import hook_callback
|
||||||
|
|
||||||
|
firewall = firewall_list(raw=True)
|
||||||
|
upnp = firewall['uPnP']['enabled']
|
||||||
|
|
||||||
|
# IPv4
|
||||||
|
if os.system("iptables -P INPUT ACCEPT") != 0:
|
||||||
|
raise MoulinetteError(errno.ESRCH, m18n.n('iptables_unavailable'))
|
||||||
|
if upnp:
|
||||||
|
try:
|
||||||
|
upnpc = miniupnpc.UPnP()
|
||||||
|
upnpc.discoverdelay = 200
|
||||||
|
if upnpc.discover() == 1:
|
||||||
|
upnpc.selectigd()
|
||||||
|
for protocol in ['TCP', 'UDP']:
|
||||||
|
for port in firewall['uPnP'][protocol]:
|
||||||
|
if upnpc.getspecificportmapping(port, protocol):
|
||||||
|
try: upnpc.deleteportmapping(port, protocol)
|
||||||
|
except: pass
|
||||||
|
upnpc.addportmapping(port, protocol, upnpc.lanaddr, port, 'yunohost firewall : port %d' % port, '')
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.ENXIO, m18n.n('upnp_dev_not_found'))
|
||||||
|
except:
|
||||||
|
msignals.display(m18n.n('upnp_port_open_failed'), 'warning')
|
||||||
|
|
||||||
|
os.system("iptables -F")
|
||||||
|
os.system("iptables -X")
|
||||||
|
os.system("iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT")
|
||||||
|
|
||||||
|
if 22 not in firewall['ipv4']['TCP']:
|
||||||
|
firewall_allow(22)
|
||||||
|
|
||||||
|
# Loop
|
||||||
|
for protocol in ['TCP', 'UDP']:
|
||||||
|
for port in firewall['ipv4'][protocol]:
|
||||||
|
os.system("iptables -A INPUT -p %s --dport %d -j ACCEPT" % (protocol, port))
|
||||||
|
|
||||||
|
hook_callback('post_iptable_rules', [upnp, os.path.exists("/proc/net/if_inet6")])
|
||||||
|
|
||||||
|
os.system("iptables -A INPUT -i lo -j ACCEPT")
|
||||||
|
os.system("iptables -A INPUT -p icmp -j ACCEPT")
|
||||||
|
os.system("iptables -P INPUT DROP")
|
||||||
|
|
||||||
|
# IPv6
|
||||||
|
if os.path.exists("/proc/net/if_inet6"):
|
||||||
|
os.system("ip6tables -P INPUT ACCEPT")
|
||||||
|
os.system("ip6tables -F")
|
||||||
|
os.system("ip6tables -X")
|
||||||
|
os.system("ip6tables -A INPUT -m state --state ESTABLISHED -j ACCEPT")
|
||||||
|
|
||||||
|
if 22 not in firewall['ipv6']['TCP']:
|
||||||
|
firewall_allow(22, ipv6=True)
|
||||||
|
|
||||||
|
# Loop v6
|
||||||
|
for protocol in ['TCP', 'UDP']:
|
||||||
|
for port in firewall['ipv6'][protocol]:
|
||||||
|
os.system("ip6tables -A INPUT -p %s --dport %d -j ACCEPT" % (protocol, port))
|
||||||
|
|
||||||
|
os.system("ip6tables -A INPUT -i lo -j ACCEPT")
|
||||||
|
os.system("ip6tables -A INPUT -p icmpv6 -j ACCEPT")
|
||||||
|
os.system("ip6tables -P INPUT DROP")
|
||||||
|
|
||||||
|
os.system("service fail2ban restart")
|
||||||
|
msignals.display(m18n.n('firewall_reloaded'), 'success')
|
||||||
|
|
||||||
|
return firewall_list()
|
||||||
|
|
||||||
|
|
||||||
|
def firewall_upnp(action=None):
|
||||||
|
"""
|
||||||
|
Add uPnP cron and enable uPnP in firewall.yml, or the opposite.
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
action -- enable/disable
|
||||||
|
|
||||||
|
"""
|
||||||
|
firewall = firewall_list(raw=True)
|
||||||
|
|
||||||
|
if action:
|
||||||
|
action = action[0]
|
||||||
|
|
||||||
|
if action == 'enable':
|
||||||
|
firewall['uPnP']['enabled'] = True
|
||||||
|
|
||||||
|
with open('/etc/cron.d/yunohost-firewall', 'w+') as f:
|
||||||
|
f.write('*/50 * * * * root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin yunohost firewall reload >>/dev/null')
|
||||||
|
|
||||||
|
msignals.display(m18n.n('upnp_enabled'), 'success')
|
||||||
|
|
||||||
|
if action == 'disable':
|
||||||
|
firewall['uPnP']['enabled'] = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
upnpc = miniupnpc.UPnP()
|
||||||
|
upnpc.discoverdelay = 200
|
||||||
|
if upnpc.discover() == 1:
|
||||||
|
upnpc.selectigd()
|
||||||
|
for protocol in ['TCP', 'UDP']:
|
||||||
|
for port in firewall['uPnP'][protocol]:
|
||||||
|
if upnpc.getspecificportmapping(port, protocol):
|
||||||
|
try: upnpc.deleteportmapping(port, protocol)
|
||||||
|
except: pass
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
|
||||||
|
try: os.remove('/etc/cron.d/yunohost-firewall')
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
msignals.display(m18n.n('upnp_disabled'), 'success')
|
||||||
|
|
||||||
|
if action:
|
||||||
|
os.system("cp /etc/yunohost/firewall.yml /etc/yunohost/firewall.yml.old")
|
||||||
|
with open('/etc/yunohost/firewall.yml', 'w') as f:
|
||||||
|
yaml.safe_dump(firewall, f, default_flow_style=False)
|
||||||
|
|
||||||
|
return { "enabled": firewall['uPnP']['enabled'] }
|
||||||
|
|
||||||
|
|
||||||
|
def firewall_stop():
|
||||||
|
"""
|
||||||
|
Stop iptables and ip6tables
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if os.system("iptables -P INPUT ACCEPT") != 0:
|
||||||
|
raise MoulinetteError(errno.ESRCH, m18n.n('iptables_unavailable'))
|
||||||
|
|
||||||
|
os.system("iptables -F")
|
||||||
|
os.system("iptables -X")
|
||||||
|
|
||||||
|
if os.path.exists("/proc/net/if_inet6"):
|
||||||
|
os.system("ip6tables -P INPUT ACCEPT")
|
||||||
|
os.system("ip6tables -F")
|
||||||
|
os.system("ip6tables -X")
|
||||||
|
|
||||||
|
if os.path.exists("/etc/cron.d/yunohost-firewall"):
|
||||||
|
firewall_upnp('disable')
|
183
hook.py
Normal file
183
hook.py
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
# -*- 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_hook.py
|
||||||
|
|
||||||
|
Manage hooks
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
|
hook_folder = '/usr/share/yunohost/hooks/'
|
||||||
|
|
||||||
|
def hook_add(app, file):
|
||||||
|
"""
|
||||||
|
Store hook script to filsystem
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
app -- App to link with
|
||||||
|
file -- Script to add (/path/priority-file)
|
||||||
|
|
||||||
|
"""
|
||||||
|
path, filename = os.path.split(file)
|
||||||
|
if '-' in filename:
|
||||||
|
priority, action = filename.split('-')
|
||||||
|
else:
|
||||||
|
priority = '50'
|
||||||
|
action = filename
|
||||||
|
|
||||||
|
try: os.listdir(hook_folder + action)
|
||||||
|
except OSError: os.makedirs(hook_folder + action)
|
||||||
|
|
||||||
|
finalpath = hook_folder + action +'/'+ priority +'-'+ app
|
||||||
|
print app
|
||||||
|
os.system('cp %s %s' % (file, finalpath))
|
||||||
|
os.system('chown -hR admin: %s' % hook_folder)
|
||||||
|
|
||||||
|
return { 'hook': finalpath }
|
||||||
|
|
||||||
|
|
||||||
|
def hook_remove(app):
|
||||||
|
"""
|
||||||
|
Remove hooks linked to a specific app
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
app -- Scripts related to app will be removed
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
for action in os.listdir(hook_folder):
|
||||||
|
for script in os.listdir(hook_folder + action):
|
||||||
|
if script.endswith(app):
|
||||||
|
os.remove(hook_folder + action +'/'+ script)
|
||||||
|
except OSError: pass
|
||||||
|
|
||||||
|
|
||||||
|
def hook_callback(action, args=None):
|
||||||
|
"""
|
||||||
|
Execute all scripts binded to an action
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
action -- Action name
|
||||||
|
args -- Ordered list of arguments to pass to the script
|
||||||
|
|
||||||
|
"""
|
||||||
|
try: os.listdir(hook_folder + action)
|
||||||
|
except OSError: pass
|
||||||
|
else:
|
||||||
|
if args is None:
|
||||||
|
args = []
|
||||||
|
elif not isinstance(args, list):
|
||||||
|
args = [args]
|
||||||
|
|
||||||
|
for hook in os.listdir(hook_folder + action):
|
||||||
|
try:
|
||||||
|
hook_exec(file=hook_folder + action +'/'+ hook, args=args)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
|
||||||
|
def hook_check(file):
|
||||||
|
"""
|
||||||
|
Parse the script file and get arguments
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
file -- File to check
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(file[:file.index('scripts/')] + 'manifest.json') as f:
|
||||||
|
manifest = json.loads(str(f.read()))
|
||||||
|
except:
|
||||||
|
raise MoulinetteError(errno.EIO, m18n.n('app_manifest_invalid'))
|
||||||
|
|
||||||
|
action = file[file.index('scripts/') + 8:]
|
||||||
|
if 'arguments' in manifest and action in manifest['arguments']:
|
||||||
|
return manifest['arguments'][action]
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def hook_exec(file, args=None):
|
||||||
|
"""
|
||||||
|
Execute hook from a file with arguments
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
file -- Script to execute
|
||||||
|
args -- Arguments to pass to the script
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(args, list):
|
||||||
|
arg_list = args
|
||||||
|
else:
|
||||||
|
required_args = hook_check(file)
|
||||||
|
if args is None:
|
||||||
|
args = {}
|
||||||
|
|
||||||
|
arg_list = []
|
||||||
|
for arg in required_args:
|
||||||
|
if arg['name'] in args:
|
||||||
|
if 'choices' in arg and args[arg['name']] not in arg['choices']:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('hook_choice_invalid')
|
||||||
|
% args[arg['name']])
|
||||||
|
arg_list.append(args[arg['name']])
|
||||||
|
else:
|
||||||
|
if os.isatty(1) and 'ask' in arg:
|
||||||
|
# Retrieve proper ask string
|
||||||
|
ask_string = None
|
||||||
|
for lang in [m18n.locale, m18n.default_locale]:
|
||||||
|
if lang in arg['ask']:
|
||||||
|
ask_string = arg['ask'][lang]
|
||||||
|
break
|
||||||
|
if not ask_string:
|
||||||
|
# Fallback to en
|
||||||
|
ask_string = arg['ask']['en']
|
||||||
|
|
||||||
|
# Append extra strings
|
||||||
|
if 'choices' in arg:
|
||||||
|
ask_string += ' (%s)' % '|'.join(arg['choices'])
|
||||||
|
if 'default' in arg:
|
||||||
|
ask_string += ' (default: %s)' % arg['default']
|
||||||
|
|
||||||
|
input_string = msignals.prompt(ask_string)
|
||||||
|
|
||||||
|
if input_string == '' and 'default' in arg:
|
||||||
|
input_string = arg['default']
|
||||||
|
|
||||||
|
arg_list.append(input_string)
|
||||||
|
elif 'default' in arg:
|
||||||
|
arg_list.append(arg['default'])
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('hook_argument_missing')
|
||||||
|
% arg['name'])
|
||||||
|
|
||||||
|
file_path = "./"
|
||||||
|
if "/" in file and file[0:2] != file_path:
|
||||||
|
file_path = os.path.dirname(file)
|
||||||
|
file = file.replace(file_path +"/", "")
|
||||||
|
return os.system('su - admin -c "cd \\"%s\\" && bash \\"%s\\" %s"' % (file_path, file, ' '.join(arg_list)))
|
||||||
|
#TODO: Allow python script
|
128
locales/en.json
Normal file
128
locales/en.json
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
{
|
||||||
|
"yunohost" : "YunoHost",
|
||||||
|
"yunohost_not_installed" : "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.",
|
||||||
|
|
||||||
|
"upgrade_complete" : "Upgrade complete",
|
||||||
|
"installation_complete" : "Installation complete",
|
||||||
|
"installation_failed" : "Installation failed",
|
||||||
|
|
||||||
|
"no_list_found" : "No list found",
|
||||||
|
"custom_list_name_required" : "You must provide a name for your custom list",
|
||||||
|
"list_retrieve_error" : "Unable to retrieve the remote list",
|
||||||
|
"list_feteched" : "List successfully fetched",
|
||||||
|
"list_unknown" : "Unknown list",
|
||||||
|
"list_removed" : "List successfully removed",
|
||||||
|
"app_unknown" : "Unknown app",
|
||||||
|
"app_no_upgrade" : "No app to upgrade",
|
||||||
|
"app_not_installed" : "%s is not installed",
|
||||||
|
"custom_app_url_required" : "You must provide an URL to upgrade your custom app %s",
|
||||||
|
"app_recent_version_required" : "%s requires a more recent version of the moulinette",
|
||||||
|
"app_upgraded" : "%s successfully upgraded",
|
||||||
|
"app_id_invalid" : "Invalid app id",
|
||||||
|
"app_already_installed" : "%s is already installed",
|
||||||
|
"app_removed" : "%s successfully removed",
|
||||||
|
"app_location_already_used" : "An app is already installed on this location",
|
||||||
|
"app_location_install_failed" : "Unable to install the app on this location",
|
||||||
|
"app_extraction_failed" : "Unable to extract installation files",
|
||||||
|
"app_install_files_invalid" : "Invalid installation files",
|
||||||
|
"app_manifest_invalid" : "Invalid app manifest",
|
||||||
|
"app_sources_fetch_failed" : "Unable to fetch sources files",
|
||||||
|
"ssowat_conf_updated" : "SSOwat persistent configuration successfully updated",
|
||||||
|
"ssowat_conf_generated" : "SSOwat configuration successfully generated",
|
||||||
|
"mysql_db_creation_failed" : "MySQL database creation failed",
|
||||||
|
"mysql_db_init_failed" : "MySQL database init failed",
|
||||||
|
"mysql_db_initialized" : "MySQL database successfully initialized",
|
||||||
|
"extracting" : "Extracting...",
|
||||||
|
"downloading" : "Downloading...",
|
||||||
|
"done" : "Done.",
|
||||||
|
|
||||||
|
"domain_unknown" : "Unknown domain",
|
||||||
|
"domain_dyndns_invalid" : "Invalid domain to use with DynDNS",
|
||||||
|
"domain_dyndns_already_subscribed" : "You already have subscribed to a DynDNS domain",
|
||||||
|
"domain_dyndns_root_unknown" : "Unknown DynDNS root domain",
|
||||||
|
"domain_cert_gen_failed" : "Unable to generate certificate",
|
||||||
|
"domain_exists" : "Domain already exists",
|
||||||
|
"domain_zone_exists" : "Zone file already exists",
|
||||||
|
"domain_creation_failed" : "Unable to create domain",
|
||||||
|
"domain_created" : "Domain successfully created",
|
||||||
|
"domain_uninstall_app_first" : "One or more apps are installed on this domain. Please uninstall them before proceed to domain removal.",
|
||||||
|
"domain_deletion_failed" : "Unable to delete domain",
|
||||||
|
"domain_deleted" : "Domain successfully deleted",
|
||||||
|
|
||||||
|
"dyndns_unavailable" : "Unavailable DynDNS subdomain",
|
||||||
|
"dyndns_registration_failed" : "Unable to register DynDNS domain: %s",
|
||||||
|
"dyndns_registered" : "DynDNS domain successfully registered",
|
||||||
|
"dyndns_ip_update_failed" : "Unable to update IP address on DynDNS",
|
||||||
|
"dyndns_ip_updated" : "IP address successfully updated on DynDNS",
|
||||||
|
"dyndns_cron_installed" : "DynDNS cron job successfully installed",
|
||||||
|
"dyndns_cron_remove_failed" : "Unable to remove DynDNS cron job",
|
||||||
|
"dyndns_cron_removed" : "DynDNS cron job successfully removed",
|
||||||
|
|
||||||
|
"port_available" : "Port %d is available",
|
||||||
|
"port_unavailable" : "Port %d is not available",
|
||||||
|
"port_already_opened" : "Port %d is already opened",
|
||||||
|
"port_already_closed" : "Port %d is already closed",
|
||||||
|
"iptables_unavailable" : "You cannot play with iptables here. You are either in a container or your kernel does not support it.",
|
||||||
|
"upnp_dev_not_found" : "No uPnP device found",
|
||||||
|
"upnp_port_open_failed" : "Unable to open uPnP ports",
|
||||||
|
"upnp_enabled" : "uPnP successfully enabled",
|
||||||
|
"upnp_disabled" : "uPnP successfully disabled",
|
||||||
|
"firewall_reloaded" : "Firewall successfully reloaded",
|
||||||
|
|
||||||
|
"hook_choice_invalid" : "Invalid choice '%s'",
|
||||||
|
"hook_argument_missing" : "Missing argument '%s'",
|
||||||
|
|
||||||
|
"mountpoint_unknown" : "Unknown mountpoint",
|
||||||
|
"unit_unknown" : "Unknown unit '%s'",
|
||||||
|
"monitor_period_invalid" : "Invalid time period",
|
||||||
|
"monitor_stats_no_update" : "No monitoring statistics to update",
|
||||||
|
"monitor_stats_file_not_found" : "Statistics file not found",
|
||||||
|
"monitor_stats_period_unavailable" : "No available statistics for the period",
|
||||||
|
"monitor_enabled" : "Server monitoring successfully enabled",
|
||||||
|
"monitor_disabled" : "Server monitoring successfully disabled",
|
||||||
|
"monitor_not_enabled" : "Server monitoring is not enabled",
|
||||||
|
"monitor_glances_con_failed" : "Unable to connect to Glances server",
|
||||||
|
|
||||||
|
"service_unknown" : "Unknown service '%s'",
|
||||||
|
"service_start_failed" : "Unable to start service '%s'",
|
||||||
|
"service_already_started" : "Service '%s' is already started",
|
||||||
|
"service_started" : "Service '%s' successfully started",
|
||||||
|
"service_stop_failed" : "Unable to stop service '%s'",
|
||||||
|
"service_already_stopped" : "Service '%s' is already stopped",
|
||||||
|
"service_stopped" : "Service '%s' successfully stopped",
|
||||||
|
"service_enable_failed" : "Unable to enable service '%s'",
|
||||||
|
"service_enabled" : "Service '%s' successfully enabled",
|
||||||
|
"service_disable_failed" : "Unable to disable service '%s'",
|
||||||
|
"service_disabled" : "Service '%s' successfully disabled",
|
||||||
|
"service_status_failed" : "Unable to determine status of service '%s'",
|
||||||
|
"service_no_log" : "No log to display for service '%s'",
|
||||||
|
"service_cmd_exec_failed" : "Unable to execute command '%s'",
|
||||||
|
|
||||||
|
"ldap_initialized" : "LDAP successfully initialized",
|
||||||
|
"password_too_short" : "Password is too short",
|
||||||
|
"admin_password_change_failed" : "Unable to change password",
|
||||||
|
"admin_password_changed" : "Administration password successfully changed",
|
||||||
|
"maindomain_change_failed" : "Unable to change main domain",
|
||||||
|
"maindomain_changed" : "Main domain successfully changed",
|
||||||
|
"yunohost_installing" : "Installing YunoHost...",
|
||||||
|
"yunohost_already_installed" : "YunoHost is already installed",
|
||||||
|
"yunohost_ca_creation_failed" : "Unable to create certificate authority",
|
||||||
|
"yunohost_configured" : "YunoHost successfully configured",
|
||||||
|
"update_cache_failed" : "Unable to update APT cache",
|
||||||
|
"system_no_upgrade" : "There is no packages to upgrade",
|
||||||
|
"system_upgraded" : "System successfully upgraded",
|
||||||
|
|
||||||
|
"field_invalid" : "Invalid field '%s'",
|
||||||
|
"mail_domain_unknown" : "Unknown mail address domain '%s'",
|
||||||
|
"mail_alias_remove_failed" : "Unable to remove mail alias '%s'",
|
||||||
|
"mail_forward_remove_failed" : "Unable to remove mail forward '%s'",
|
||||||
|
"user_unknown" : "Unknown user",
|
||||||
|
"user_creation_failed" : "Unable to create user",
|
||||||
|
"user_created" : "User successfully created",
|
||||||
|
"user_deletion_failed" : "Unable to delete user",
|
||||||
|
"user_deleted" : "User successfully deleted",
|
||||||
|
"user_update_failed" : "Unable to update user",
|
||||||
|
"user_updated" : "User successfully updated",
|
||||||
|
"user_info_failed" : "Unable to retrieve user information"
|
||||||
|
}
|
||||||
|
|
710
monitor.py
Normal file
710
monitor.py
Normal file
|
@ -0,0 +1,710 @@
|
||||||
|
# -*- 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_monitor.py
|
||||||
|
|
||||||
|
Monitoring functions
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import psutil
|
||||||
|
import calendar
|
||||||
|
import subprocess
|
||||||
|
import xmlrpclib
|
||||||
|
import os.path
|
||||||
|
import errno
|
||||||
|
import cPickle as pickle
|
||||||
|
from urllib import urlopen
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
|
glances_uri = 'http://127.0.0.1:61209'
|
||||||
|
stats_path = '/var/lib/yunohost/stats'
|
||||||
|
crontab_path = '/etc/cron.d/yunohost-monitor'
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_disk(units=None, mountpoint=None, human_readable=False):
|
||||||
|
"""
|
||||||
|
Monitor disk space and usage
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
units -- Unit(s) to monitor
|
||||||
|
mountpoint -- Device mountpoint
|
||||||
|
human_readable -- Print sizes in human readable format
|
||||||
|
|
||||||
|
"""
|
||||||
|
glances = _get_glances_api()
|
||||||
|
result_dname = None
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if units is None:
|
||||||
|
units = ['io', 'filesystem']
|
||||||
|
|
||||||
|
_format_dname = lambda d: (os.path.realpath(d)).replace('/dev/', '')
|
||||||
|
|
||||||
|
# Get mounted devices
|
||||||
|
devices = {}
|
||||||
|
for p in psutil.disk_partitions(all=True):
|
||||||
|
if not p.device.startswith('/dev/') or not p.mountpoint:
|
||||||
|
continue
|
||||||
|
if mountpoint is None:
|
||||||
|
devices[_format_dname(p.device)] = p.mountpoint
|
||||||
|
elif mountpoint == p.mountpoint:
|
||||||
|
dn = _format_dname(p.device)
|
||||||
|
devices[dn] = p.mountpoint
|
||||||
|
result_dname = dn
|
||||||
|
if len(devices) == 0:
|
||||||
|
if mountpoint is not None:
|
||||||
|
raise MoulinetteError(errno.ENODEV, m18n.n('mountpoint_unknown'))
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Retrieve monitoring for unit(s)
|
||||||
|
for u in units:
|
||||||
|
if u == 'io':
|
||||||
|
## Define setter
|
||||||
|
if len(units) > 1:
|
||||||
|
def _set(dn, dvalue):
|
||||||
|
try:
|
||||||
|
result[dn][u] = dvalue
|
||||||
|
except KeyError:
|
||||||
|
result[dn] = { u: dvalue }
|
||||||
|
else:
|
||||||
|
def _set(dn, dvalue):
|
||||||
|
result[dn] = dvalue
|
||||||
|
|
||||||
|
# Iterate over values
|
||||||
|
devices_names = devices.keys()
|
||||||
|
for d in json.loads(glances.getDiskIO()):
|
||||||
|
dname = d.pop('disk_name')
|
||||||
|
try:
|
||||||
|
devices_names.remove(dname)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
_set(dname, d)
|
||||||
|
for dname in devices_names:
|
||||||
|
_set(dname, 'not-available')
|
||||||
|
elif u == 'filesystem':
|
||||||
|
## Define setter
|
||||||
|
if len(units) > 1:
|
||||||
|
def _set(dn, dvalue):
|
||||||
|
try:
|
||||||
|
result[dn][u] = dvalue
|
||||||
|
except KeyError:
|
||||||
|
result[dn] = { u: dvalue }
|
||||||
|
else:
|
||||||
|
def _set(dn, dvalue):
|
||||||
|
result[dn] = dvalue
|
||||||
|
|
||||||
|
# Iterate over values
|
||||||
|
devices_names = devices.keys()
|
||||||
|
for d in json.loads(glances.getFs()):
|
||||||
|
dname = _format_dname(d.pop('device_name'))
|
||||||
|
try:
|
||||||
|
devices_names.remove(dname)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if human_readable:
|
||||||
|
for i in ['used', 'avail', 'size']:
|
||||||
|
d[i] = _binary_to_human(d[i]) + 'B'
|
||||||
|
_set(dname, d)
|
||||||
|
for dname in devices_names:
|
||||||
|
_set(dname, 'not-available')
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown') % u)
|
||||||
|
|
||||||
|
if result_dname is not None:
|
||||||
|
return result[result_dname]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_network(units=None, human_readable=False):
|
||||||
|
"""
|
||||||
|
Monitor network interfaces
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
units -- Unit(s) to monitor
|
||||||
|
human_readable -- Print sizes in human readable format
|
||||||
|
|
||||||
|
"""
|
||||||
|
glances = _get_glances_api()
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if units is None:
|
||||||
|
units = ['usage', 'infos']
|
||||||
|
|
||||||
|
# Get network devices and their addresses
|
||||||
|
devices = {}
|
||||||
|
output = subprocess.check_output('ip addr show'.split())
|
||||||
|
for d in re.split('^(?:[0-9]+: )', output, flags=re.MULTILINE):
|
||||||
|
d = re.sub('\n[ ]+', ' % ', d) # Replace new lines by %
|
||||||
|
m = re.match('([a-z]+[0-9]?): (.*)', d) # Extract device name (1) and its addresses (2)
|
||||||
|
if m:
|
||||||
|
devices[m.group(1)] = m.group(2)
|
||||||
|
|
||||||
|
# Retrieve monitoring for unit(s)
|
||||||
|
for u in units:
|
||||||
|
if u == 'usage':
|
||||||
|
result[u] = {}
|
||||||
|
for i in json.loads(glances.getNetwork()):
|
||||||
|
iname = i['interface_name']
|
||||||
|
if iname in devices.keys():
|
||||||
|
del i['interface_name']
|
||||||
|
if human_readable:
|
||||||
|
for k in i.keys():
|
||||||
|
if k != 'time_since_update':
|
||||||
|
i[k] = _binary_to_human(i[k]) + 'B'
|
||||||
|
result[u][iname] = i
|
||||||
|
elif u == 'infos':
|
||||||
|
try:
|
||||||
|
p_ip = str(urlopen('http://ip.yunohost.org').read())
|
||||||
|
except:
|
||||||
|
p_ip = 'unknown'
|
||||||
|
|
||||||
|
l_ip = 'unknown'
|
||||||
|
for name, addrs in devices.items():
|
||||||
|
if name == 'lo':
|
||||||
|
continue
|
||||||
|
if not isinstance(l_ip, dict):
|
||||||
|
l_ip = {}
|
||||||
|
l_ip[name] = _extract_inet(addrs)
|
||||||
|
|
||||||
|
gateway = 'unknown'
|
||||||
|
output = subprocess.check_output('ip route show'.split())
|
||||||
|
m = re.search('default via (.*) dev ([a-z]+[0-9]?)', output)
|
||||||
|
if m:
|
||||||
|
addr = _extract_inet(m.group(1), True)
|
||||||
|
if len(addr) == 1:
|
||||||
|
proto, gateway = addr.popitem()
|
||||||
|
|
||||||
|
result[u] = {
|
||||||
|
'public_ip': p_ip,
|
||||||
|
'local_ip': l_ip,
|
||||||
|
'gateway': gateway
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown') % u)
|
||||||
|
|
||||||
|
if len(units) == 1:
|
||||||
|
return result[units[0]]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_system(units=None, human_readable=False):
|
||||||
|
"""
|
||||||
|
Monitor system informations and usage
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
units -- Unit(s) to monitor
|
||||||
|
human_readable -- Print sizes in human readable format
|
||||||
|
|
||||||
|
"""
|
||||||
|
glances = _get_glances_api()
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if units is None:
|
||||||
|
units = ['memory', 'cpu', 'process', 'uptime', 'infos']
|
||||||
|
|
||||||
|
# Retrieve monitoring for unit(s)
|
||||||
|
for u in units:
|
||||||
|
if u == 'memory':
|
||||||
|
ram = json.loads(glances.getMem())
|
||||||
|
swap = json.loads(glances.getMemSwap())
|
||||||
|
if human_readable:
|
||||||
|
for i in ram.keys():
|
||||||
|
if i != 'percent':
|
||||||
|
ram[i] = _binary_to_human(ram[i]) + 'B'
|
||||||
|
for i in swap.keys():
|
||||||
|
if i != 'percent':
|
||||||
|
swap[i] = _binary_to_human(swap[i]) + 'B'
|
||||||
|
result[u] = {
|
||||||
|
'ram': ram,
|
||||||
|
'swap': swap
|
||||||
|
}
|
||||||
|
elif u == 'cpu':
|
||||||
|
result[u] = {
|
||||||
|
'load': json.loads(glances.getLoad()),
|
||||||
|
'usage': json.loads(glances.getCpu())
|
||||||
|
}
|
||||||
|
elif u == 'process':
|
||||||
|
result[u] = json.loads(glances.getProcessCount())
|
||||||
|
elif u == 'uptime':
|
||||||
|
result[u] = (str(datetime.now() - datetime.fromtimestamp(psutil.BOOT_TIME)).split('.')[0])
|
||||||
|
elif u == 'infos':
|
||||||
|
result[u] = json.loads(glances.getSystem())
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown') % u)
|
||||||
|
|
||||||
|
if len(units) == 1 and type(result[units[0]]) is not str:
|
||||||
|
return result[units[0]]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_update_stats(period):
|
||||||
|
"""
|
||||||
|
Update monitoring statistics
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
period -- Time period to update (day, week, month)
|
||||||
|
|
||||||
|
"""
|
||||||
|
if period not in ['day', 'week', 'month']:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('monitor_period_invalid'))
|
||||||
|
|
||||||
|
stats = _retrieve_stats(period)
|
||||||
|
if not stats:
|
||||||
|
stats = { 'disk': {}, 'network': {}, 'system': {}, 'timestamp': [] }
|
||||||
|
|
||||||
|
monitor = None
|
||||||
|
# Get monitoring stats
|
||||||
|
if period == 'day':
|
||||||
|
monitor = _monitor_all('day')
|
||||||
|
else:
|
||||||
|
t = stats['timestamp']
|
||||||
|
p = 'day' if period == 'week' else 'week'
|
||||||
|
if len(t) > 0:
|
||||||
|
monitor = _monitor_all(p, t[len(t) - 1])
|
||||||
|
else:
|
||||||
|
monitor = _monitor_all(p, 0)
|
||||||
|
if not monitor:
|
||||||
|
raise MoulinetteError(errno.ENODATA, m18n.n('monitor_stats_no_update'))
|
||||||
|
|
||||||
|
stats['timestamp'].append(time.time())
|
||||||
|
|
||||||
|
# Append disk stats
|
||||||
|
for dname, units in monitor['disk'].items():
|
||||||
|
disk = {}
|
||||||
|
# Retrieve current stats for disk name
|
||||||
|
if dname in stats['disk'].keys():
|
||||||
|
disk = stats['disk'][dname]
|
||||||
|
|
||||||
|
for unit, values in units.items():
|
||||||
|
# Continue if unit doesn't contain stats
|
||||||
|
if not isinstance(values, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Retrieve current stats for unit and append new ones
|
||||||
|
curr = disk[unit] if unit in disk.keys() else {}
|
||||||
|
if unit == 'io':
|
||||||
|
disk[unit] = _append_to_stats(curr, values, 'time_since_update')
|
||||||
|
elif unit == 'filesystem':
|
||||||
|
disk[unit] = _append_to_stats(curr, values, ['fs_type', 'mnt_point'])
|
||||||
|
stats['disk'][dname] = disk
|
||||||
|
|
||||||
|
# Append network stats
|
||||||
|
net_usage = {}
|
||||||
|
for iname, values in monitor['network']['usage'].items():
|
||||||
|
# Continue if units doesn't contain stats
|
||||||
|
if not isinstance(values, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Retrieve current stats and append new ones
|
||||||
|
curr = {}
|
||||||
|
if 'usage' in stats['network'] and iname in stats['network']['usage']:
|
||||||
|
curr = stats['network']['usage'][iname]
|
||||||
|
net_usage[iname] = _append_to_stats(curr, values, 'time_since_update')
|
||||||
|
stats['network'] = { 'usage': net_usage, 'infos': monitor['network']['infos'] }
|
||||||
|
|
||||||
|
# Append system stats
|
||||||
|
for unit, values in monitor['system'].items():
|
||||||
|
# Continue if units doesn't contain stats
|
||||||
|
if not isinstance(values, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Set static infos unit
|
||||||
|
if unit == 'infos':
|
||||||
|
stats['system'][unit] = values
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Retrieve current stats and append new ones
|
||||||
|
curr = stats['system'][unit] if unit in stats['system'].keys() else {}
|
||||||
|
stats['system'][unit] = _append_to_stats(curr, values)
|
||||||
|
|
||||||
|
_save_stats(stats, period)
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_show_stats(period, date=None):
|
||||||
|
"""
|
||||||
|
Show monitoring statistics
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
period -- Time period to show (day, week, month)
|
||||||
|
|
||||||
|
"""
|
||||||
|
if period not in ['day', 'week', 'month']:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('monitor_period_invalid'))
|
||||||
|
|
||||||
|
result = _retrieve_stats(period, date)
|
||||||
|
if result is False:
|
||||||
|
raise MoulinetteError(errno.ENOENT,
|
||||||
|
m18n.n('monitor_stats_file_not_found'))
|
||||||
|
elif result is None:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('monitor_stats_period_unavailable'))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_enable(no_stats=False):
|
||||||
|
"""
|
||||||
|
Enable server monitoring
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
no_stats -- Disable monitoring statistics
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yunohost.service import (service_status, service_enable,
|
||||||
|
service_start)
|
||||||
|
|
||||||
|
glances = service_status('glances')
|
||||||
|
if glances['status'] != 'running':
|
||||||
|
service_start('glances')
|
||||||
|
if glances['loaded'] != 'enabled':
|
||||||
|
service_enable('glances')
|
||||||
|
|
||||||
|
# Install crontab
|
||||||
|
if not no_stats:
|
||||||
|
cmd = 'yunohost monitor update-stats'
|
||||||
|
# day: every 5 min # week: every 1 h # month: every 4 h #
|
||||||
|
rules = ('*/5 * * * * root %(cmd)s day --no-ldap >> /dev/null\n' + \
|
||||||
|
'3 * * * * root %(cmd)s week --no-ldap >> /dev/null\n' + \
|
||||||
|
'6 */4 * * * root %(cmd)s month --no-ldap >> /dev/null') % {'cmd': cmd}
|
||||||
|
os.system("touch %s" % crontab_path)
|
||||||
|
os.system("echo '%s' >%s" % (rules, crontab_path))
|
||||||
|
|
||||||
|
msignals.display(m18n.n('monitor_enabled'), 'success')
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_disable():
|
||||||
|
"""
|
||||||
|
Disable server monitoring
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yunohost.service import (service_status, service_disable,
|
||||||
|
service_stop)
|
||||||
|
|
||||||
|
glances = service_status('glances')
|
||||||
|
if glances['status'] != 'inactive':
|
||||||
|
service_stop('glances')
|
||||||
|
if glances['loaded'] != 'disabled':
|
||||||
|
try:
|
||||||
|
service_disable('glances')
|
||||||
|
except MoulinetteError as e:
|
||||||
|
msignals.display(e.strerror, 'warning')
|
||||||
|
|
||||||
|
# Remove crontab
|
||||||
|
try:
|
||||||
|
os.remove(crontab_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
msignals.display(m18n.n('monitor_disabled'), 'success')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_glances_api():
|
||||||
|
"""
|
||||||
|
Retrieve Glances API running on the local server
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
p = xmlrpclib.ServerProxy(glances_uri)
|
||||||
|
p.system.methodHelp('getAll')
|
||||||
|
except (xmlrpclib.ProtocolError, IOError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return p
|
||||||
|
|
||||||
|
from yunohost.service import service_status
|
||||||
|
|
||||||
|
if service_status('glances')['status'] != 'running':
|
||||||
|
raise MoulinetteError(errno.EPERM, m18n.n('monitor_not_enabled'))
|
||||||
|
raise MoulinetteError(errno.EIO, m18n.n('monitor_glances_con_failed'))
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_inet(string, skip_netmask=False, skip_loopback=True):
|
||||||
|
"""
|
||||||
|
Extract IP addresses (v4 and/or v6) from a string limited to one
|
||||||
|
address by protocol
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
string -- String to search in
|
||||||
|
skip_netmask -- True to skip subnet mask extraction
|
||||||
|
skip_loopback -- False to include addresses reserved for the
|
||||||
|
loopback interface
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict of {protocol: address} with protocol one of 'ipv4' or 'ipv6'
|
||||||
|
|
||||||
|
"""
|
||||||
|
ip4_pattern = '((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
|
||||||
|
ip6_pattern = '(((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)'
|
||||||
|
ip4_pattern += '/[0-9]{1,2})' if not skip_netmask else ')'
|
||||||
|
ip6_pattern += '/[0-9]{1,3})' if not skip_netmask else ')'
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for m in re.finditer(ip4_pattern, string):
|
||||||
|
addr = m.group(1)
|
||||||
|
if skip_loopback and addr.startswith('127.'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Limit to only one result
|
||||||
|
result['ipv4'] = addr
|
||||||
|
break
|
||||||
|
|
||||||
|
for m in re.finditer(ip6_pattern, string):
|
||||||
|
addr = m.group(1)
|
||||||
|
if skip_loopback and addr == '::1':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Limit to only one result
|
||||||
|
result['ipv6'] = addr
|
||||||
|
break
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _binary_to_human(n, customary=False):
|
||||||
|
"""
|
||||||
|
Convert bytes or bits into human readable format with binary prefix
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
n -- Number to convert
|
||||||
|
customary -- Use customary symbol instead of IEC standard
|
||||||
|
|
||||||
|
"""
|
||||||
|
symbols = ('Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi')
|
||||||
|
if customary:
|
||||||
|
symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
|
||||||
|
prefix = {}
|
||||||
|
for i, s in enumerate(symbols):
|
||||||
|
prefix[s] = 1 << (i+1)*10
|
||||||
|
for s in reversed(symbols):
|
||||||
|
if n >= prefix[s]:
|
||||||
|
value = float(n) / prefix[s]
|
||||||
|
return '%.1f%s' % (value, s)
|
||||||
|
return "%s" % n
|
||||||
|
|
||||||
|
|
||||||
|
def _retrieve_stats(period, date=None):
|
||||||
|
"""
|
||||||
|
Retrieve statistics from pickle file
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
period -- Time period to retrieve (day, week, month)
|
||||||
|
date -- Date of stats to retrieve
|
||||||
|
|
||||||
|
"""
|
||||||
|
pkl_file = None
|
||||||
|
|
||||||
|
# Retrieve pickle file
|
||||||
|
if date is not None:
|
||||||
|
timestamp = calendar.timegm(date)
|
||||||
|
pkl_file = '%s/%d_%s.pkl' % (stats_path, timestamp, period)
|
||||||
|
else:
|
||||||
|
pkl_file = '%s/%s.pkl' % (stats_path, period)
|
||||||
|
if not os.path.isfile(pkl_file):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Read file and process its content
|
||||||
|
with open(pkl_file, 'r') as f:
|
||||||
|
result = pickle.load(f)
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
return None
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _save_stats(stats, period, date=None):
|
||||||
|
"""
|
||||||
|
Save statistics to pickle file
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
stats -- Stats dict to save
|
||||||
|
period -- Time period of stats (day, week, month)
|
||||||
|
date -- Date of stats
|
||||||
|
|
||||||
|
"""
|
||||||
|
pkl_file = None
|
||||||
|
|
||||||
|
# Set pickle file name
|
||||||
|
if date is not None:
|
||||||
|
timestamp = calendar.timegm(date)
|
||||||
|
pkl_file = '%s/%d_%s.pkl' % (stats_path, timestamp, period)
|
||||||
|
else:
|
||||||
|
pkl_file = '%s/%s.pkl' % (stats_path, period)
|
||||||
|
if not os.path.isdir(stats_path):
|
||||||
|
os.makedirs(stats_path)
|
||||||
|
|
||||||
|
# Limit stats
|
||||||
|
if date is None:
|
||||||
|
t = stats['timestamp']
|
||||||
|
limit = { 'day': 86400, 'week': 604800, 'month': 2419200 }
|
||||||
|
if (t[len(t) - 1] - t[0]) > limit[period]:
|
||||||
|
begin = t[len(t) - 1] - limit[period]
|
||||||
|
stats = _filter_stats(stats, begin)
|
||||||
|
|
||||||
|
# Write file content
|
||||||
|
with open(pkl_file, 'w') as f:
|
||||||
|
pickle.dump(stats, f)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _monitor_all(period=None, since=None):
|
||||||
|
"""
|
||||||
|
Monitor all units (disk, network and system) for the given period
|
||||||
|
If since is None, real-time monitoring is returned. Otherwise, the
|
||||||
|
mean of stats since this timestamp is calculated and returned.
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
period -- Time period to monitor (day, week, month)
|
||||||
|
since -- Timestamp of the stats beginning
|
||||||
|
|
||||||
|
"""
|
||||||
|
result = { 'disk': {}, 'network': {}, 'system': {} }
|
||||||
|
|
||||||
|
# Real-time stats
|
||||||
|
if period == 'day' and since is None:
|
||||||
|
result['disk'] = monitor_disk()
|
||||||
|
result['network'] = monitor_network()
|
||||||
|
result['system'] = monitor_system()
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Retrieve stats and calculate mean
|
||||||
|
stats = _retrieve_stats(period)
|
||||||
|
if not stats:
|
||||||
|
return None
|
||||||
|
stats = _filter_stats(stats, since)
|
||||||
|
if not stats:
|
||||||
|
return None
|
||||||
|
result = _calculate_stats_mean(stats)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_stats(stats, t_begin=None, t_end=None):
|
||||||
|
"""
|
||||||
|
Filter statistics by beginning and/or ending timestamp
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
stats -- Dict stats to filter
|
||||||
|
t_begin -- Beginning timestamp
|
||||||
|
t_end -- Ending timestamp
|
||||||
|
|
||||||
|
"""
|
||||||
|
if t_begin is None and t_end is None:
|
||||||
|
return stats
|
||||||
|
|
||||||
|
i_begin = i_end = None
|
||||||
|
# Look for indexes of timestamp interval
|
||||||
|
for i, t in enumerate(stats['timestamp']):
|
||||||
|
if t_begin and i_begin is None and t >= t_begin:
|
||||||
|
i_begin = i
|
||||||
|
if t_end and i != 0 and i_end is None and t > t_end:
|
||||||
|
i_end = i
|
||||||
|
# Check indexes
|
||||||
|
if i_begin is None:
|
||||||
|
if t_begin and t_begin > stats['timestamp'][0]:
|
||||||
|
return None
|
||||||
|
i_begin = 0
|
||||||
|
if i_end is None:
|
||||||
|
if t_end and t_end < stats['timestamp'][0]:
|
||||||
|
return None
|
||||||
|
i_end = len(stats['timestamp'])
|
||||||
|
if i_begin == 0 and i_end == len(stats['timestamp']):
|
||||||
|
return stats
|
||||||
|
|
||||||
|
# Filter function
|
||||||
|
def _filter(s, i, j):
|
||||||
|
for k, v in s.items():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
s[k] = _filter(v, i, j)
|
||||||
|
elif isinstance(v, list):
|
||||||
|
s[k] = v[i:j]
|
||||||
|
return s
|
||||||
|
|
||||||
|
stats = _filter(stats, i_begin, i_end)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_stats_mean(stats):
|
||||||
|
"""
|
||||||
|
Calculate the weighted mean for each statistic
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
stats -- Stats dict to process
|
||||||
|
|
||||||
|
"""
|
||||||
|
timestamp = stats['timestamp']
|
||||||
|
t_sum = sum(timestamp)
|
||||||
|
del stats['timestamp']
|
||||||
|
|
||||||
|
# Weighted mean function
|
||||||
|
def _mean(s, t, ts):
|
||||||
|
for k, v in s.items():
|
||||||
|
if isinstance(v, dict):
|
||||||
|
s[k] = _mean(v, t, ts)
|
||||||
|
elif isinstance(v, list):
|
||||||
|
try:
|
||||||
|
nums = [ float(x * t[i]) for i, x in enumerate(v) ]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
s[k] = sum(nums) / float(ts)
|
||||||
|
return s
|
||||||
|
|
||||||
|
stats = _mean(stats, timestamp, t_sum)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def _append_to_stats(stats, monitor, statics=[]):
|
||||||
|
"""
|
||||||
|
Append monitoring statistics to current statistics
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
stats -- Current stats dict
|
||||||
|
monitor -- Monitoring statistics
|
||||||
|
statics -- List of stats static keys
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(statics, str):
|
||||||
|
statics = [statics]
|
||||||
|
|
||||||
|
# Appending function
|
||||||
|
def _append(s, m, st):
|
||||||
|
for k, v in m.items():
|
||||||
|
if k in st:
|
||||||
|
s[k] = v
|
||||||
|
elif isinstance(v, dict):
|
||||||
|
if k not in s:
|
||||||
|
s[k] = {}
|
||||||
|
s[k] = _append(s[k], v, st)
|
||||||
|
else:
|
||||||
|
if k not in s:
|
||||||
|
s[k] = []
|
||||||
|
if isinstance(v, list):
|
||||||
|
s[k].extend(v)
|
||||||
|
else:
|
||||||
|
s[k].append(v)
|
||||||
|
return s
|
||||||
|
|
||||||
|
stats = _append(stats, monitor, statics)
|
||||||
|
return stats
|
271
service.py
Normal file
271
service.py
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
# -*- 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_service.py
|
||||||
|
|
||||||
|
Manage services
|
||||||
|
"""
|
||||||
|
import yaml
|
||||||
|
import glob
|
||||||
|
import subprocess
|
||||||
|
import errno
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
|
|
||||||
|
def service_start(names):
|
||||||
|
"""
|
||||||
|
Start one or more services
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
names -- Services name to start
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(names, str):
|
||||||
|
names = [names]
|
||||||
|
for name in names:
|
||||||
|
if _run_service_command('start', name):
|
||||||
|
msignals.display(m18n.n('service_started') % name, 'success')
|
||||||
|
else:
|
||||||
|
if service_status(name)['status'] != 'running':
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('service_start_failed') % name)
|
||||||
|
msignals.display(m18n.n('service_already_started') % name)
|
||||||
|
|
||||||
|
|
||||||
|
def service_stop(names):
|
||||||
|
"""
|
||||||
|
Stop one or more services
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
name -- Services name to stop
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(names, str):
|
||||||
|
names = [names]
|
||||||
|
for name in names:
|
||||||
|
if _run_service_command('stop', name):
|
||||||
|
msignals.display(m18n.n('service_stopped') % name, 'success')
|
||||||
|
else:
|
||||||
|
if service_status(name)['status'] != 'inactive':
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('service_stop_failed') % name)
|
||||||
|
msignals.display(m18n.n('service_already_stopped') % name)
|
||||||
|
|
||||||
|
|
||||||
|
def service_enable(names):
|
||||||
|
"""
|
||||||
|
Enable one or more services
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
names -- Services name to enable
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(names, str):
|
||||||
|
names = [names]
|
||||||
|
for name in names:
|
||||||
|
if _run_service_command('enable', name):
|
||||||
|
msignals.display(m18n.n('service_enabled') % name, 'success')
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('service_enable_failed') % name)
|
||||||
|
|
||||||
|
|
||||||
|
def service_disable(names):
|
||||||
|
"""
|
||||||
|
Disable one or more services
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
names -- Services name to disable
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(names, str):
|
||||||
|
names = [names]
|
||||||
|
for name in names:
|
||||||
|
if _run_service_command('disable', name):
|
||||||
|
msignals.display(m18n.n('service_disabled') % name, 'success')
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('service_disable_failed') % name)
|
||||||
|
|
||||||
|
|
||||||
|
def service_status(names=[]):
|
||||||
|
"""
|
||||||
|
Show status information about one or more services (all by default)
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
names -- Services name to show
|
||||||
|
|
||||||
|
"""
|
||||||
|
services = _get_services()
|
||||||
|
check_names = True
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if isinstance(names, str):
|
||||||
|
names = [names]
|
||||||
|
elif len(names) == 0:
|
||||||
|
names = services.keys()
|
||||||
|
check_names = False
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
if check_names and name not in services.keys():
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('service_unknown') % name)
|
||||||
|
|
||||||
|
status = None
|
||||||
|
if services[name]['status'] == 'service':
|
||||||
|
status = 'service %s status' % name
|
||||||
|
else:
|
||||||
|
status = str(services[name]['status'])
|
||||||
|
|
||||||
|
runlevel = 5
|
||||||
|
if 'runlevel' in services[name].keys():
|
||||||
|
runlevel = int(services[name]['runlevel'])
|
||||||
|
|
||||||
|
result[name] = { 'status': 'unknown', 'loaded': 'unknown' }
|
||||||
|
|
||||||
|
# Retrieve service status
|
||||||
|
try:
|
||||||
|
ret = subprocess.check_output(status.split(), stderr=subprocess.STDOUT)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
if 'usage:' not in e.output.lower():
|
||||||
|
result[name]['status'] = 'inactive'
|
||||||
|
else:
|
||||||
|
# TODO: Log output?
|
||||||
|
msignals.display(m18n.n('service_status_failed') % name,
|
||||||
|
'warning')
|
||||||
|
else:
|
||||||
|
result[name]['status'] = 'running'
|
||||||
|
|
||||||
|
# Retrieve service loading
|
||||||
|
rc_path = glob.glob("/etc/rc%d.d/S[0-9][0-9]%s" % (runlevel, name))
|
||||||
|
if len(rc_path) == 1 and os.path.islink(rc_path[0]):
|
||||||
|
result[name]['loaded'] = 'enabled'
|
||||||
|
elif os.path.isfile("/etc/init.d/%s" % name):
|
||||||
|
result[name]['loaded'] = 'disabled'
|
||||||
|
else:
|
||||||
|
result[name]['loaded'] = 'not-found'
|
||||||
|
|
||||||
|
if len(names) == 1:
|
||||||
|
return result[names[0]]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def service_log(name, number=50):
|
||||||
|
"""
|
||||||
|
Log every log files of a service
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
name -- Service name to log
|
||||||
|
number -- Number of lines to display
|
||||||
|
|
||||||
|
"""
|
||||||
|
services = _get_services()
|
||||||
|
|
||||||
|
if name not in services.keys():
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown') % name)
|
||||||
|
|
||||||
|
if 'log' in services[name]:
|
||||||
|
log_list = services[name]['log']
|
||||||
|
result = {}
|
||||||
|
if not isinstance(log_list, list):
|
||||||
|
log_list = [log_list]
|
||||||
|
|
||||||
|
for log_path in log_list:
|
||||||
|
if os.path.isdir(log_path):
|
||||||
|
for log in [ f for f in os.listdir(log_path) if os.path.isfile(os.path.join(log_path, f)) and f[-4:] == '.log' ]:
|
||||||
|
result[os.path.join(log_path, log)] = _tail(os.path.join(log_path, log), int(number))
|
||||||
|
else:
|
||||||
|
result[log_path] = _tail(log_path, int(number))
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EPERM, m18n.n('service_no_log') % name)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _run_service_command(action, service):
|
||||||
|
"""
|
||||||
|
Run services management command (start, stop, enable, disable)
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
service -- Service name
|
||||||
|
action -- Action to perform
|
||||||
|
|
||||||
|
"""
|
||||||
|
if service not in _get_services().keys():
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown') % name)
|
||||||
|
|
||||||
|
cmd = None
|
||||||
|
if action in ['start', 'stop']:
|
||||||
|
cmd = 'service %s %s' % (service, action)
|
||||||
|
elif action in ['enable', 'disable']:
|
||||||
|
arg = 'defaults' if action == 'enable' else 'remove'
|
||||||
|
cmd = 'update-rc.d %s %s' % (service, arg)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown action '%s'" % action)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ret = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
# TODO: Log output?
|
||||||
|
msignals.display(m18n.n('service_cmd_exec_failed') % ' '.join(e.cmd),
|
||||||
|
'warning')
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _get_services():
|
||||||
|
"""
|
||||||
|
Get a dict of managed services with their parameters
|
||||||
|
|
||||||
|
"""
|
||||||
|
with open('/etc/yunohost/services.yml', 'r') as f:
|
||||||
|
services = yaml.load(f)
|
||||||
|
return services
|
||||||
|
|
||||||
|
|
||||||
|
def _tail(file, n, offset=None):
|
||||||
|
"""
|
||||||
|
Reads a n lines from f with an offset of offset lines. The return
|
||||||
|
value is a tuple in the form ``(lines, has_more)`` where `has_more` is
|
||||||
|
an indicator that is `True` if there are more lines in the file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
avg_line_length = 74
|
||||||
|
to_read = n + (offset or 0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file, 'r') as f:
|
||||||
|
while 1:
|
||||||
|
try:
|
||||||
|
f.seek(-(avg_line_length * to_read), 2)
|
||||||
|
except IOError:
|
||||||
|
# woops. apparently file is smaller than what we want
|
||||||
|
# to step back, go to the beginning instead
|
||||||
|
f.seek(0)
|
||||||
|
pos = f.tell()
|
||||||
|
lines = f.read().splitlines()
|
||||||
|
if len(lines) >= to_read or pos == 0:
|
||||||
|
return lines[-to_read:offset and -offset or None]
|
||||||
|
avg_line_length *= 1.3
|
||||||
|
|
||||||
|
except IOError: return []
|
388
tools.py
Normal file
388
tools.py
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
# -*- 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 sys
|
||||||
|
import yaml
|
||||||
|
import re
|
||||||
|
import getpass
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import errno
|
||||||
|
import apt
|
||||||
|
import apt.progress
|
||||||
|
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
|
apps_setting_path= '/etc/yunohost/apps/'
|
||||||
|
|
||||||
|
def tools_ldapinit(auth):
|
||||||
|
"""
|
||||||
|
YunoHost LDAP initialization
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
with open('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)
|
||||||
|
|
||||||
|
msignals.display(m18n.n('ldap_ initialized'), 'success')
|
||||||
|
|
||||||
|
|
||||||
|
def tools_adminpw(old_password, new_password):
|
||||||
|
"""
|
||||||
|
Change admin password
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
new_password
|
||||||
|
old_password
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Validate password length
|
||||||
|
if len(new_password) < 4:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('password_too_short'))
|
||||||
|
|
||||||
|
old_password.replace('"', '\\"')
|
||||||
|
old_password.replace('&', '\\&')
|
||||||
|
new_password.replace('"', '\\"')
|
||||||
|
new_password.replace('&', '\\&')
|
||||||
|
result = os.system('ldappasswd -h localhost -D cn=admin,dc=yunohost,dc=org -w "%s" -a "%s" -s "%s"' % (old_password, old_password, new_password))
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
msignals.display(m18n.n('admin_password_changed'), 'success')
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('admin_password_change_failed'))
|
||||||
|
|
||||||
|
|
||||||
|
def tools_maindomain(auth, old_domain=None, new_domain=None, dyndns=False):
|
||||||
|
"""
|
||||||
|
Main domain change tool
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
new_domain
|
||||||
|
old_domain
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yunohost.domain import domain_add
|
||||||
|
from yunohost.dyndns import dyndns_subscribe
|
||||||
|
|
||||||
|
if not old_domain:
|
||||||
|
with open('/etc/yunohost/current_host', 'r') as f:
|
||||||
|
old_domain = f.readline().rstrip()
|
||||||
|
|
||||||
|
if not new_domain:
|
||||||
|
return { 'current_main_domain': old_domain }
|
||||||
|
|
||||||
|
config_files = [
|
||||||
|
'/etc/postfix/main.cf',
|
||||||
|
'/etc/metronome/metronome.cfg.lua',
|
||||||
|
'/etc/dovecot/dovecot.conf',
|
||||||
|
'/usr/share/yunohost/yunohost-config/others/startup',
|
||||||
|
'/home/yunohost.backup/tahoe/tahoe.cfg',
|
||||||
|
'/etc/amavis/conf.d/05-node_id',
|
||||||
|
'/etc/amavis/conf.d/50-user'
|
||||||
|
]
|
||||||
|
|
||||||
|
config_dir = []
|
||||||
|
|
||||||
|
for dir in config_dir:
|
||||||
|
for file in os.listdir(dir):
|
||||||
|
config_files.append(dir + '/' + file)
|
||||||
|
|
||||||
|
for file in config_files:
|
||||||
|
with open(file, "r") as sources:
|
||||||
|
lines = sources.readlines()
|
||||||
|
with open(file, "w") as sources:
|
||||||
|
for line in lines:
|
||||||
|
sources.write(re.sub(r''+ old_domain +'', new_domain, line))
|
||||||
|
|
||||||
|
domain_add(auth, [new_domain], main=True)
|
||||||
|
|
||||||
|
os.system('rm /etc/ssl/private/yunohost_key.pem')
|
||||||
|
os.system('rm /etc/ssl/certs/yunohost_crt.pem')
|
||||||
|
|
||||||
|
command_list = [
|
||||||
|
'ln -s /etc/yunohost/certs/%s/key.pem /etc/ssl/private/yunohost_key.pem' % new_domain,
|
||||||
|
'ln -s /etc/yunohost/certs/%s/crt.pem /etc/ssl/certs/yunohost_crt.pem' % new_domain,
|
||||||
|
'echo %s > /etc/yunohost/current_host' % new_domain,
|
||||||
|
'service nginx restart',
|
||||||
|
'service metronome restart',
|
||||||
|
'service postfix restart',
|
||||||
|
'service dovecot restart',
|
||||||
|
'service amavis restart'
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('/etc/yunohost/light') as f: pass
|
||||||
|
except IOError:
|
||||||
|
command_list.append('service amavis restart')
|
||||||
|
#command_list.append('service tahoe-lafs restart')
|
||||||
|
|
||||||
|
for command in command_list:
|
||||||
|
if os.system(command) != 0:
|
||||||
|
raise MoulinetteError(errno.EPERM,
|
||||||
|
m18n.n('maindomain_change_failed'))
|
||||||
|
|
||||||
|
if dyndns: dyndns_subscribe(domain=new_domain)
|
||||||
|
elif len(new_domain.split('.')) >= 3:
|
||||||
|
r = requests.get('http://dyndns.yunohost.org/domains')
|
||||||
|
dyndomains = json.loads(r.text)
|
||||||
|
dyndomain = '.'.join(new_domain.split('.')[1:])
|
||||||
|
if dyndomain in dyndomains:
|
||||||
|
dyndns_subscribe(domain=new_domain)
|
||||||
|
|
||||||
|
msignals.display(m18n.n('maindomain_changed'), 'success')
|
||||||
|
|
||||||
|
|
||||||
|
def tools_postinstall(domain, password, dyndns=False):
|
||||||
|
"""
|
||||||
|
YunoHost post-install
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
domain -- YunoHost main domain
|
||||||
|
dyndns -- Subscribe domain to a DynDNS service
|
||||||
|
password -- YunoHost admin password
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yunohost.backup import backup_init
|
||||||
|
from yunohost.app import app_ssowatconf
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('/etc/yunohost/installed') as f: pass
|
||||||
|
except IOError:
|
||||||
|
msignals.display(m18n.n('yunohost_installing'))
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed'))
|
||||||
|
|
||||||
|
if len(domain.split('.')) >= 3:
|
||||||
|
r = requests.get('http://dyndns.yunohost.org/domains')
|
||||||
|
dyndomains = json.loads(r.text)
|
||||||
|
dyndomain = '.'.join(domain.split('.')[1:])
|
||||||
|
if dyndomain in dyndomains:
|
||||||
|
if requests.get('http://dyndns.yunohost.org/test/%s' % domain).status_code == 200:
|
||||||
|
dyndns=True
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EEXIST,
|
||||||
|
m18n.n('dyndns_unavailable'))
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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 IOError:
|
||||||
|
ssowat_conf = {}
|
||||||
|
|
||||||
|
if 'redirected_urls' not in ssowat_conf:
|
||||||
|
ssowat_conf['redirected_urls'] = {}
|
||||||
|
|
||||||
|
ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin'
|
||||||
|
|
||||||
|
with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
|
||||||
|
json.dump(ssowat_conf, f, sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
os.system('chmod 644 /etc/ssowat/conf.json.persistent')
|
||||||
|
|
||||||
|
# Create SSL CA
|
||||||
|
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'))
|
||||||
|
|
||||||
|
# Initialize YunoHost LDAP base
|
||||||
|
tools_ldapinit(auth)
|
||||||
|
|
||||||
|
# Initialize backup system
|
||||||
|
backup_init()
|
||||||
|
|
||||||
|
# New domain config
|
||||||
|
tools_maindomain(auth, old_domain='yunohost.org', new_domain=domain, dyndns=dyndns)
|
||||||
|
|
||||||
|
# Generate SSOwat configuration file
|
||||||
|
app_ssowatconf(auth)
|
||||||
|
|
||||||
|
# Change LDAP admin password
|
||||||
|
tools_adminpw(old_password='yunohost', new_password=password)
|
||||||
|
|
||||||
|
os.system('touch /etc/yunohost/installed')
|
||||||
|
os.system('service yunohost-api restart &')
|
||||||
|
|
||||||
|
msignals.display(m18n.n('yunohost_configured'), 'success')
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yunohost.app import app_fetchlist, app_info
|
||||||
|
|
||||||
|
packages = []
|
||||||
|
if not ignore_packages:
|
||||||
|
cache = apt.Cache()
|
||||||
|
# Update APT cache
|
||||||
|
if not cache.update():
|
||||||
|
raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed'))
|
||||||
|
|
||||||
|
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:
|
||||||
|
app_fetchlist()
|
||||||
|
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 '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:
|
||||||
|
msignals.display(m18n.n('system_no_upgrade'), 'success')
|
||||||
|
|
||||||
|
return { 'packages': packages, 'apps': apps }
|
||||||
|
|
||||||
|
|
||||||
|
def tools_upgrade(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
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yunohost.app import app_upgrade
|
||||||
|
|
||||||
|
if not ignore_packages:
|
||||||
|
cache = apt.Cache()
|
||||||
|
cache.open(None)
|
||||||
|
cache.upgrade(True)
|
||||||
|
|
||||||
|
# If API call
|
||||||
|
if not os.isatty(1):
|
||||||
|
critical_packages = ["yunohost-cli", "yunohost-admin", "yunohost-config-nginx", "ssowat", "python"]
|
||||||
|
for pkg in cache.get_changes():
|
||||||
|
if pkg.name in critical_packages:
|
||||||
|
# Temporarily keep package ...
|
||||||
|
pkg.mark_keep()
|
||||||
|
# ... and set a hourly cron up to upgrade critical packages
|
||||||
|
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 '+ ' '.join(critical_packages) + ' -y && rm -f /etc/cron.d/yunohost-upgrade')
|
||||||
|
try:
|
||||||
|
# Apply APT changes
|
||||||
|
cache.commit(apt.progress.text.AcquireProgress(), apt.progress.base.InstallProgress())
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
if not ignore_apps:
|
||||||
|
try:
|
||||||
|
app_upgrade()
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
msignals.display(m18n.n('system_upgraded'), 'success')
|
||||||
|
|
||||||
|
# Return API logs if it is an API call
|
||||||
|
if not os.isatty(1):
|
||||||
|
return { "log": service_log('yunohost-api', number="100").values()[0] }
|
364
user.py
Normal file
364
user.py
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
""" License
|
||||||
|
|
||||||
|
Copyright (C) 2014 YUNOHOST.ORG
|
||||||
|
|
||||||
|
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_user.py
|
||||||
|
|
||||||
|
Manage users
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import crypt
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import json
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
|
|
||||||
|
|
||||||
|
def user_list(auth, fields=None, filter=None, limit=None, offset=None):
|
||||||
|
"""
|
||||||
|
List users
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
filter -- LDAP filter used to search
|
||||||
|
offset -- Starting number for user fetching
|
||||||
|
limit -- Maximum number of user fetched
|
||||||
|
fields -- fields to fetch
|
||||||
|
|
||||||
|
"""
|
||||||
|
user_attrs = { 'uid': 'username',
|
||||||
|
'cn': 'fullname',
|
||||||
|
'mail': 'mail',
|
||||||
|
'maildrop': 'mail-forward' }
|
||||||
|
attrs = []
|
||||||
|
result_list = []
|
||||||
|
|
||||||
|
# Set default arguments values
|
||||||
|
if offset is None:
|
||||||
|
offset = 0
|
||||||
|
if limit is None:
|
||||||
|
limit = 1000
|
||||||
|
if filter is None:
|
||||||
|
filter = '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))'
|
||||||
|
if fields:
|
||||||
|
keys = user_attrs.keys()
|
||||||
|
for attr in fields:
|
||||||
|
if attr in keys:
|
||||||
|
attrs.append(attr)
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('field_invalid') % attr)
|
||||||
|
else:
|
||||||
|
attrs = [ 'uid', 'cn', 'mail' ]
|
||||||
|
|
||||||
|
result = auth.search('ou=users,dc=yunohost,dc=org', filter, attrs)
|
||||||
|
|
||||||
|
if len(result) > offset and limit > 0:
|
||||||
|
for user in result[offset:offset+limit]:
|
||||||
|
entry = {}
|
||||||
|
for attr, values in user.items():
|
||||||
|
try:
|
||||||
|
entry[user_attrs[attr]] = values[0]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
result_list.append(entry)
|
||||||
|
return { 'users' : result_list }
|
||||||
|
|
||||||
|
|
||||||
|
def user_create(auth, username, firstname, lastname, mail, password):
|
||||||
|
"""
|
||||||
|
Create user
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
firstname
|
||||||
|
lastname
|
||||||
|
username -- Must be unique
|
||||||
|
mail -- Main mail address must be unique
|
||||||
|
password
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yunohost.domain import domain_list
|
||||||
|
from yunohost.hook import hook_callback
|
||||||
|
|
||||||
|
# Validate password length
|
||||||
|
if len(password) < 4:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('password_too_short'))
|
||||||
|
|
||||||
|
auth.validate_uniqueness({
|
||||||
|
'uid' : username,
|
||||||
|
'mail' : mail
|
||||||
|
})
|
||||||
|
|
||||||
|
if mail[mail.find('@')+1:] not in domain_list(auth)['domains']:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('mail_domain_unknown')
|
||||||
|
% mail[mail.find('@')+1:])
|
||||||
|
|
||||||
|
# Get random UID/GID
|
||||||
|
uid_check = gid_check = 0
|
||||||
|
while uid_check == 0 and gid_check == 0:
|
||||||
|
uid = str(random.randint(200, 99999))
|
||||||
|
uid_check = os.system("getent passwd %s" % uid)
|
||||||
|
gid_check = os.system("getent group %s" % uid)
|
||||||
|
|
||||||
|
# Adapt values for LDAP
|
||||||
|
fullname = '%s %s' % (firstname, lastname)
|
||||||
|
rdn = 'uid=%s,ou=users' % username
|
||||||
|
char_set = string.ascii_uppercase + string.digits
|
||||||
|
salt = ''.join(random.sample(char_set,8))
|
||||||
|
salt = '$1$' + salt + '$'
|
||||||
|
pwd = '{CRYPT}' + crypt.crypt(str(password), salt)
|
||||||
|
attr_dict = {
|
||||||
|
'objectClass' : ['mailAccount', 'inetOrgPerson', 'posixAccount'],
|
||||||
|
'givenName' : firstname,
|
||||||
|
'sn' : lastname,
|
||||||
|
'displayName' : fullname,
|
||||||
|
'cn' : fullname,
|
||||||
|
'uid' : username,
|
||||||
|
'mail' : mail,
|
||||||
|
'maildrop' : username,
|
||||||
|
'userPassword' : pwd,
|
||||||
|
'gidNumber' : uid,
|
||||||
|
'uidNumber' : uid,
|
||||||
|
'homeDirectory' : '/home/' + username,
|
||||||
|
'loginShell' : '/bin/false'
|
||||||
|
}
|
||||||
|
|
||||||
|
# If it is the first user, add some aliases
|
||||||
|
if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'):
|
||||||
|
with open('/etc/yunohost/current_host') as f:
|
||||||
|
main_domain = f.readline().rstrip()
|
||||||
|
aliases = [
|
||||||
|
'root@'+ main_domain,
|
||||||
|
'admin@'+ main_domain,
|
||||||
|
'webmaster@'+ main_domain,
|
||||||
|
'postmaster@'+ main_domain,
|
||||||
|
]
|
||||||
|
attr_dict['mail'] = [ attr_dict['mail'] ] + aliases
|
||||||
|
|
||||||
|
# If exists, remove the redirection from the SSO
|
||||||
|
try:
|
||||||
|
with open('/etc/ssowat/conf.json.persistent') as json_conf:
|
||||||
|
ssowat_conf = json.loads(str(json_conf.read()))
|
||||||
|
|
||||||
|
if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']:
|
||||||
|
del ssowat_conf['redirected_urls']['/']
|
||||||
|
|
||||||
|
with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
|
||||||
|
json.dump(ssowat_conf, f, sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
except IOError: pass
|
||||||
|
|
||||||
|
|
||||||
|
if auth.add(rdn, attr_dict):
|
||||||
|
# Update SFTP user group
|
||||||
|
memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid']
|
||||||
|
memberlist.append(username)
|
||||||
|
if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }):
|
||||||
|
os.system("su - %s -c ''" % username)
|
||||||
|
os.system('yunohost app ssowatconf > /dev/null 2>&1')
|
||||||
|
#TODO: Send a welcome mail to user
|
||||||
|
msignals.display(m18n.n('user_created'), 'success')
|
||||||
|
hook_callback('post_user_create', [username, mail, password, firstname, lastname])
|
||||||
|
|
||||||
|
return { 'fullname' : fullname, 'username' : username, 'mail' : mail }
|
||||||
|
|
||||||
|
raise MoulinetteError(169, m18n.n('user_creation_failed'))
|
||||||
|
|
||||||
|
|
||||||
|
def user_delete(auth, users, purge=False):
|
||||||
|
"""
|
||||||
|
Delete user
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
users -- Username of users to delete
|
||||||
|
purge
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isinstance(users, list):
|
||||||
|
users = [ users ]
|
||||||
|
deleted = []
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
if auth.remove('uid=%s,ou=users' % user):
|
||||||
|
# Update SFTP user group
|
||||||
|
memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid']
|
||||||
|
try: memberlist.remove(user)
|
||||||
|
except: pass
|
||||||
|
if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }):
|
||||||
|
if purge:
|
||||||
|
os.system('rm -rf /home/%s' % user)
|
||||||
|
deleted.append(user)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(169, m18n.n('user_deletion_failed'))
|
||||||
|
|
||||||
|
os.system('yunohost app ssowatconf > /dev/null 2>&1')
|
||||||
|
msignals.display(m18n.n('user_deleted'), 'success')
|
||||||
|
return { 'users': deleted }
|
||||||
|
|
||||||
|
|
||||||
|
def user_update(auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None):
|
||||||
|
"""
|
||||||
|
Update user informations
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
lastname
|
||||||
|
mail
|
||||||
|
firstname
|
||||||
|
add_mailalias -- Mail aliases to add
|
||||||
|
remove_mailforward -- Mailforward addresses to remove
|
||||||
|
username -- Username of user to update
|
||||||
|
add_mailforward -- Mailforward addresses to add
|
||||||
|
change_password -- New password to set
|
||||||
|
remove_mailalias -- Mail aliases to remove
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yunohost.domain import domain_list
|
||||||
|
|
||||||
|
attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop']
|
||||||
|
new_attr_dict = {}
|
||||||
|
domains = domain_list(auth)['domains']
|
||||||
|
|
||||||
|
# Populate user informations
|
||||||
|
result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch)
|
||||||
|
if not result:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown'))
|
||||||
|
user = result[0]
|
||||||
|
|
||||||
|
# Get modifications from arguments
|
||||||
|
if firstname:
|
||||||
|
new_attr_dict['givenName'] = firstname # TODO: Validate
|
||||||
|
new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + user['sn'][0]
|
||||||
|
|
||||||
|
if lastname:
|
||||||
|
new_attr_dict['sn'] = lastname # TODO: Validate
|
||||||
|
new_attr_dict['cn'] = new_attr_dict['displayName'] = user['givenName'][0] + ' ' + lastname
|
||||||
|
|
||||||
|
if lastname and firstname:
|
||||||
|
new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname
|
||||||
|
|
||||||
|
if change_password:
|
||||||
|
char_set = string.ascii_uppercase + string.digits
|
||||||
|
salt = ''.join(random.sample(char_set,8))
|
||||||
|
salt = '$1$' + salt + '$'
|
||||||
|
new_attr_dict['userPassword'] = '{CRYPT}' + crypt.crypt(str(change_password), salt)
|
||||||
|
|
||||||
|
if mail:
|
||||||
|
auth.validate_uniqueness({ 'mail': mail })
|
||||||
|
if mail[mail.find('@')+1:] not in domains:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('mail_domain_unknown')
|
||||||
|
% mail[mail.find('@')+1:])
|
||||||
|
del user['mail'][0]
|
||||||
|
new_attr_dict['mail'] = [mail] + user['mail']
|
||||||
|
|
||||||
|
if add_mailalias:
|
||||||
|
if not isinstance(add_mailalias, list):
|
||||||
|
add_mailalias = [ add_mailalias ]
|
||||||
|
for mail in add_mailalias:
|
||||||
|
auth.validate_uniqueness({ 'mail': mail })
|
||||||
|
if mail[mail.find('@')+1:] not in domains:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('mail_domain_unknown')
|
||||||
|
% mail[mail.find('@')+1:])
|
||||||
|
user['mail'].append(mail)
|
||||||
|
new_attr_dict['mail'] = user['mail']
|
||||||
|
|
||||||
|
if remove_mailalias:
|
||||||
|
if not isinstance(remove_mailalias, list):
|
||||||
|
remove_mailalias = [ remove_mailalias ]
|
||||||
|
for mail in remove_mailalias:
|
||||||
|
if len(user['mail']) > 1 and mail in user['mail'][1:]:
|
||||||
|
user['mail'].remove(mail)
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('mail_alias_remove_failed') % mail)
|
||||||
|
new_attr_dict['mail'] = user['mail']
|
||||||
|
|
||||||
|
if add_mailforward:
|
||||||
|
if not isinstance(add_mailforward, list):
|
||||||
|
add_mailforward = [ add_mailforward ]
|
||||||
|
for mail in add_mailforward:
|
||||||
|
if mail in user['maildrop'][1:]:
|
||||||
|
continue
|
||||||
|
user['maildrop'].append(mail)
|
||||||
|
new_attr_dict['maildrop'] = user['maildrop']
|
||||||
|
|
||||||
|
if remove_mailforward:
|
||||||
|
if not isinstance(remove_mailforward, list):
|
||||||
|
remove_mailforward = [ remove_mailforward ]
|
||||||
|
for mail in remove_mailforward:
|
||||||
|
if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]:
|
||||||
|
user['maildrop'].remove(mail)
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL,
|
||||||
|
m18n.n('mail_forward_remove_failed') % mail)
|
||||||
|
new_attr_dict['maildrop'] = user['maildrop']
|
||||||
|
|
||||||
|
if auth.update('uid=%s,ou=users' % username, new_attr_dict):
|
||||||
|
msignals.display(m18n.n('user_updated'), 'success')
|
||||||
|
return user_info(auth, username)
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(169, m18n.n('user_update_failed'))
|
||||||
|
|
||||||
|
|
||||||
|
def user_info(auth, username):
|
||||||
|
"""
|
||||||
|
Get user informations
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
username -- Username or mail to get informations
|
||||||
|
|
||||||
|
"""
|
||||||
|
user_attrs = ['cn', 'mail', 'uid', 'maildrop', 'givenName', 'sn']
|
||||||
|
|
||||||
|
if len(username.split('@')) is 2:
|
||||||
|
filter = 'mail='+ username
|
||||||
|
else:
|
||||||
|
filter = 'uid='+ username
|
||||||
|
|
||||||
|
result = auth.search('ou=users,dc=yunohost,dc=org', filter, user_attrs)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
user = result[0]
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown'))
|
||||||
|
|
||||||
|
result_dict = {
|
||||||
|
'username': user['uid'][0],
|
||||||
|
'fullname': user['cn'][0],
|
||||||
|
'firstname': user['givenName'][0],
|
||||||
|
'lastname': user['sn'][0],
|
||||||
|
'mail': user['mail'][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user['mail']) > 1:
|
||||||
|
result_dict['mail-aliases'] = user['mail'][1:]
|
||||||
|
|
||||||
|
if len(user['maildrop']) > 1:
|
||||||
|
result_dict['mail-forward'] = user['maildrop'][1:]
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result_dict
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(167, m18n.n('user_info_failed'))
|
Loading…
Add table
Reference in a new issue