def domain_add(auth, domain, dyndns=False): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS """ attr_dict = {'objectClass': ['mailDomain', 'top']} try: ip = str(urlopen('http://ip.yunohost.org').read()) except IOError: ip = "127.0.0.1" now = datetime.datetime.now() timestamp = str(now.year) + str(now.month) + str(now.day) if domain in domain_list(auth)['domains']: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) # 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 try: r = requests.get('https://dyndns.yunohost.org/domains') except ConnectionError: pass else: 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')) try: # 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 dnsmasq_config_path = '/etc/dnsmasq.d' try: os.listdir(dnsmasq_config_path) except OSError: msignals.display(m18n.n('dnsmasq_isnt_installed'), 'warning') os.makedirs(dnsmasq_config_path) try: with open('%s/%s' % (dnsmasq_config_path, domain)) as f: pass except IOError as e: zone_lines = [ 'address=/%s/%s' % (domain, ip), 'txt-record=%s,"v=spf1 mx a -all"' % domain, 'mx-host=%s,%s,5' % (domain, domain), 'srv-host=_xmpp-client._tcp.%s,%s,5222,0,5' % (domain, domain), 'srv-host=_xmpp-server._tcp.%s,%s,5269,0,5' % (domain, domain), 'srv-host=_jabber._tcp.%s,%s,5269,0,5' % (domain, domain), ] with open('%s/%s' % (dnsmasq_config_path, domain), 'w') as zone: for line in zone_lines: zone.write(line + '\n') os.system('service dnsmasq restart') else: msignals.display(m18n.n('domain_zone_exists'), 'warning') # 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 not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): raise MoulinetteError(errno.EIO, m18n.n('domain_creation_failed')) os.system('yunohost app ssowatconf > /dev/null 2>&1') except: # Force domain removal silently try: domain_remove(auth, domain, True) except: pass raise msignals.display(m18n.n('domain_created'), 'success')
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 try: if requests.get('https://%s/test/%s' % (subscribe_host, domain)).status_code != 200: raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) if key is None: if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0: os.makedirs('/etc/yunohost/dyndns') logger.info(m18n.n('dyndns_key_generating')) os.system( 'cd /etc/yunohost/dyndns && ' 'dnssec-keygen -a hmac-md5 -b 128 -r /dev/urandom -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 try: r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}) except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) 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=error)) logger.success(m18n.n('dyndns_registered')) dyndns_installcron()
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', 'mailuserquota' ] 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', user=username)) 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 'mailuserquota' in user: userquota = user['mailuserquota'][0] if isinstance(userquota, int): userquota = str(userquota) # Test if userquota is '0' or '0M' ( quota pattern is ^(\d+[bkMGT])|0$ ) is_limited = not re.match('0[bkMGT]?', userquota) storage_use = '?' if service_status("dovecot")["status"] != "running": logger.warning(m18n.n('mailbox_used_space_dovecot_down')) else: cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] cmd_result = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) # Exemple of return value for cmd: # """Quota name=User quota Type=STORAGE Value=0 Limit=- %=0 # Quota name=User quota Type=MESSAGE Value=0 Limit=- %=0""" has_value = re.search(r'Value=(\d+)', cmd_result) if has_value: storage_use = int(has_value.group(1)) storage_use = _convertSize(storage_use) if is_limited: has_percent = re.search(r'%=(\d+)', cmd_result) if has_percent: percentage = int(has_percent.group(1)) storage_use += ' (%s%%)' % percentage result_dict['mailbox-quota'] = { 'limit': userquota if is_limited else m18n.n('unlimit'), 'use': storage_use } if result: return result_dict else: raise MoulinetteError(167, m18n.n('user_info_failed'))
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_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 backup_info(name, with_details=False, human_readable=False): """ Get info about a local backup archive Keyword arguments: name -- Name of the local backup archive with_details -- Show additional backup information human_readable -- Print sizes in human readable format """ archive_file = '%s/%s.tar.gz' % (archives_path, name) # Check file exist (even if it's a broken symlink) if not os.path.lexists(archive_file): raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown', name=name)) # If symlink, retrieve the real path if os.path.islink(archive_file): archive_file = os.path.realpath(archive_file) # Raise exception if link is broken (e.g. on unmounted external storage) if not os.path.exists(archive_file): raise MoulinetteError( errno.EIO, m18n.n('backup_archive_broken_link', path=archive_file)) info_file = "%s/%s.info.json" % (archives_path, name) try: with open(info_file) as f: # Retrieve backup info info = json.load(f) except: # TODO: Attempt to extract backup info file from tarball logger.debug("unable to load '%s'", info_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) # Retrieve backup size size = info.get('size', 0) if not size: tar = tarfile.open(archive_file, "r:gz") size = reduce( lambda x, y: getattr(x, 'size', x) + getattr(y, 'size', y), tar.getmembers()) tar.close() if human_readable: size = binary_to_human(size) + 'B' result = { 'path': archive_file, 'created_at': time.strftime(m18n.n('format_datetime_short'), time.gmtime(info['created_at'])), 'description': info['description'], 'size': size, } if with_details: for d in ['apps', 'hooks']: result[d] = info[d] return result
def tools_update(ignore_apps=False, ignore_packages=False): """ Update apps & package cache, then display changelog Keyword arguments: ignore_apps -- Ignore app list update and changelog ignore_packages -- Ignore apt cache update and changelog """ packages = [] if not ignore_packages: cache = apt.Cache() # Update APT cache logger.info(m18n.n('updating_apt_cache')) if not cache.update(): raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed')) logger.info(m18n.n('done')) cache.open(None) cache.upgrade(True) # Add changelogs to the result for pkg in cache.get_changes(): packages.append({ 'name': pkg.name, 'fullname': pkg.fullname, 'changelog': pkg.get_changelog() }) apps = [] if not ignore_apps: try: app_fetchlist() except MoulinetteError: pass app_list = os.listdir(apps_setting_path) if len(app_list) > 0: for app_id in app_list: if '__' in app_id: original_app_id = app_id[:app_id.index('__')] else: original_app_id = app_id current_app_dict = app_info(app_id, raw=True) new_app_dict = app_info(original_app_id, raw=True) # Custom app if new_app_dict is None or 'lastUpdate' not in new_app_dict or 'git' not in new_app_dict: continue if (new_app_dict['lastUpdate'] > current_app_dict['lastUpdate']) \ or ('update_time' not in current_app_dict['settings'] \ and (new_app_dict['lastUpdate'] > current_app_dict['settings']['install_time'])) \ or ('update_time' in current_app_dict['settings'] \ and (new_app_dict['lastUpdate'] > current_app_dict['settings']['update_time'])): apps.append({ 'id': app_id, 'label': current_app_dict['settings']['label'] }) if len(apps) == 0 and len(packages) == 0: logger.info(m18n.n('packages_no_upgrade')) return {'packages': packages, 'apps': apps}
def backup_create(name=None, description=None, output_directory=None, no_compress=False, ignore_apps=False): """ Create a backup local archive Keyword arguments: name -- Name of the backup archive description -- Short description of the backup output_directory -- Output directory for the backup no_compress -- Do not create an archive file ignore_apps -- Do not backup apps """ # TODO: Add a 'clean' argument to clean output directory from yunohost.hook import hook_add from yunohost.hook import hook_callback tmp_dir = None # Validate and define backup name timestamp = int(time.time()) if not name: name = str(timestamp) if name in backup_list()['archives']: raise MoulinetteError(errno.EINVAL, m18n.n('backup_archive_name_exists')) # Validate additional arguments if no_compress and not output_directory: raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_required')) if output_directory: output_directory = os.path.abspath(output_directory) # Check for forbidden folders if output_directory.startswith(archives_path) or \ re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', output_directory): logger.error("forbidden output directory '%'", output_directory) raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_forbidden')) # Create the output directory if not os.path.isdir(output_directory): logger.info("creating output directory '%s'", output_directory) os.makedirs(output_directory, 0750) # Check that output directory is empty elif no_compress and os.listdir(output_directory): logger.error("not empty output directory '%'", output_directory) raise MoulinetteError(errno.EIO, m18n.n('backup_output_directory_not_empty')) # Define temporary directory if no_compress: tmp_dir = output_directory else: output_directory = archives_path # Create temporary directory if not tmp_dir: tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.warning("temporary directory for backup '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) try: os.mkdir(tmp_dir, 0750) except OSError: # Create temporary directory recursively os.makedirs(tmp_dir, 0750) os.system('chown -hR admin: %s' % backup_path) else: os.system('chown -hR admin: %s' % tmp_dir) # Initialize backup info info = { 'description': description or '', 'created_at': timestamp, 'apps': {}, } # Add apps backup hook if not ignore_apps: from yunohost.app import app_info try: for app_id in os.listdir('/etc/yunohost/apps'): hook = '/etc/yunohost/apps/%s/scripts/backup' % app_id if os.path.isfile(hook): hook_add(app_id, hook) # Add app info i = app_info(app_id) info['apps'][app_id] = { 'version': i['version'], } else: logger.warning("unable to find app's backup hook '%s'", hook) msignals.display(m18n.n('unbackup_app', app_id), 'warning') except IOError as e: logger.info("unable to add apps backup hook: %s", str(e)) # Run hooks msignals.display(m18n.n('backup_running_hooks')) hook_callback('backup', [tmp_dir]) # Create backup info file with open("%s/info.json" % tmp_dir, 'w') as f: f.write(json.dumps(info)) # Create the archive if not no_compress: msignals.display(m18n.n('backup_creating_archive')) archive_file = "%s/%s.tar.gz" % (output_directory, name) try: tar = tarfile.open(archive_file, "w:gz") except: tar = None # Create the archives directory and retry if not os.path.isdir(archives_path): os.mkdir(archives_path, 0750) try: tar = tarfile.open(archive_file, "w:gz") except: logger.exception("unable to open the archive '%s' for writing " "after creating directory '%s'", archive_file, archives_path) tar = None else: logger.exception("unable to open the archive '%s' for writing", archive_file) if tar is None: raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) tar.add(tmp_dir, arcname='') tar.close() # Copy info file os.system('mv %s/info.json %s/%s.info.json' % (tmp_dir, archives_path, name)) # Clean temporary directory if tmp_dir != output_directory: os.system('rm -rf %s' % tmp_dir) msignals.display(m18n.n('backup_complete'), 'success')
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: try: new_ip = requests.get('http://ip.yunohost.org').text except ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) 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' # IPv6 # TODO: Put global IPv6 in the DNS zone instead of ULA new_ipv6 = None try: with open('/etc/yunohost/ipv6') as f: old_ipv6 = f.readline().rstrip() except IOError: old_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000' try: # Get the interface with open('/etc/yunohost/interface') as f: interface = f.readline().rstrip() # Get the ULA with open('/etc/yunohost/ula') as f: ula = f.readline().rstrip() # Get the IPv6 address given by radvd and sanitize it with open('/proc/net/if_inet6') as f: plain_ula = '' for hextet in ula.split(':')[0:3]: if len(hextet) < 4: hextet = '0000' + hextet hextet = hextet[-4:] plain_ula = plain_ula + hextet for line in f.readlines(): if interface in line and plain_ula == line[0:12]: new_ipv6 = ':'.join( [line[0:32][i:i + 4] for i in range(0, 32, 4)]) with open('/etc/yunohost/ipv6', 'w+') as f: f.write(new_ipv6) break except IOError: pass if new_ipv6 is None: new_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000' if old_ip != new_ip or old_ipv6 != new_ipv6 and new_ipv6 is not None: host = domain.split('.')[1:] host = '.'.join(host) lines = [ 'server %s' % dyn_host, 'zone %s' % host, 'update delete %s. A' % domain, 'update delete %s. AAAA' % 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. 1800 AAAA %s' % (domain, new_ipv6), '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 pubsub.%s. 1800 AAAA %s' % (domain, new_ipv6), 'update add muc.%s. 1800 A %s' % (domain, new_ip), 'update add muc.%s. 1800 AAAA %s' % (domain, new_ipv6), 'update add vjud.%s. 1800 A %s' % (domain, new_ip), 'update add vjud.%s. 1800 AAAA %s' % (domain, new_ipv6), '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 hook_exec(path, args=None, raise_on_error=False, no_trace=False, chdir=None, env=None): """ Execute hook from a file with arguments Keyword argument: path -- Path of the script to execute args -- Ordered list of arguments to pass to the script raise_on_error -- Raise if the script returns a non-zero exit code no_trace -- Do not print each command that will be executed chdir -- The directory from where the script will be executed env -- Dictionnary of environment variables to export """ from moulinette.utils.process import call_async_output from yunohost.app import _value_for_locale # Validate hook path if path[0] != '/': path = os.path.realpath(path) if not os.path.isfile(path): raise MoulinetteError(errno.EIO, m18n.g('file_not_exist', path=path)) # Construct command variables cmd_args = '' if args and isinstance(args, list): # Concatenate escaped arguments cmd_args = ' '.join(shell_quote(s) for s in args) if not chdir: # use the script directory as current one chdir, cmd_script = os.path.split(path) cmd_script = './{0}'.format(cmd_script) else: cmd_script = path # Construct command to execute command = ['sudo', '-n', '-u', 'admin', '-H', 'sh', '-c'] if no_trace: cmd = '/bin/bash "{script}" {args}' else: # use xtrace on fd 7 which is redirected to stdout cmd = 'BASH_XTRACEFD=7 /bin/bash -x "{script}" {args} 7>&1' if env: # prepend environment variables cmd = '{0} {1}'.format( ' '.join(['{0}={1}'.format(k, shell_quote(v)) \ for k, v in env.items()]), cmd) command.append(cmd.format(script=cmd_script, args=cmd_args)) if logger.isEnabledFor(log.DEBUG): logger.info(m18n.n('executing_command', command=' '.join(command))) else: logger.info(m18n.n('executing_script', script=path)) # Define output callbacks and call command callbacks = ( lambda l: logger.info(l.rstrip()), lambda l: logger.warning(l.rstrip()), ) returncode = call_async_output(command, callbacks, shell=False, cwd=chdir) # Check and return process' return code if returncode is None: if raise_on_error: raise MoulinetteError( errno.EIO, m18n.n('hook_exec_not_terminated', path=path)) else: logger.error(m18n.n('hook_exec_not_terminated', path=path)) return 1 elif raise_on_error and returncode != 0: raise MoulinetteError(errno.EIO, m18n.n('hook_exec_failed', path=path)) return returncode
def backup_restore(name, ignore_apps=False, force=False): """ Restore from a local backup archive Keyword argument: name -- Name of the local backup archive ignore_apps -- Do not restore apps force -- Force restauration on an already installed system """ from yunohost.hook import hook_add from yunohost.hook import hook_callback # Retrieve and open the archive archive_file = backup_info(name)['path'] try: tar = tarfile.open(archive_file, "r:gz") except: logger.exception("unable to open the archive '%s' for reading", archive_file) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Check temporary directory tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.warning("temporary directory for restoration '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) # Extract the tarball msignals.display(m18n.n('backup_extracting_archive')) tar.extractall(tmp_dir) tar.close() # Retrieve backup info try: with open("%s/info.json" % tmp_dir, 'r') as f: info = json.load(f) except IOError: logger.error("unable to retrieve backup info from '%s/info.json'", tmp_dir) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) else: logger.info("restoring from backup '%s' created on %s", name, time.ctime(info['created_at'])) # Retrieve domain from the backup try: with open("%s/yunohost/current_host" % tmp_dir, 'r') as f: domain = f.readline().rstrip() except IOError: logger.error("unable to retrieve domain from '%s/yunohost/current_host'", tmp_dir) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) # Check if YunoHost is installed if os.path.isfile('/etc/yunohost/installed'): msignals.display(m18n.n('yunohost_already_installed'), 'warning') if not force: try: # Ask confirmation for restoring i = msignals.prompt(m18n.n('restore_confirm_yunohost_installed', answers='y/N')) except NotImplemented: pass else: if i == 'y' or i == 'Y': force = True if not force: raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed')) else: from yunohost.tools import tools_postinstall logger.info("executing the post-install...") tools_postinstall(domain, 'yunohost', True) # Add apps restore hook if not ignore_apps: for app_id in info['apps'].keys(): hook = "/etc/yunohost/apps/%s/scripts/restore" % app_id if os.path.isfile(hook): hook_add(app_id, hook) logger.info("app '%s' will be restored", app_id) else: msignals.display(m18n.n('unrestore_app', app_id), 'warning') # Run hooks msignals.display(m18n.n('restore_running_hooks')) hook_callback('restore', [tmp_dir]) # Remove temporary directory os.system('rm -rf %s' % tmp_dir) msignals.display(m18n.n('restore_complete'), 'success')
def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, env=None, pre_callback=None, post_callback=None): """ Execute all scripts binded to an action Keyword argument: action -- Action name hooks -- List of hooks names to execute args -- Ordered list of arguments to pass to the scripts no_trace -- Do not print each command that will be executed chdir -- The directory from where the scripts will be executed env -- Dictionnary of environment variables to export pre_callback -- An object to call before each script execution with (name, priority, path, args) as arguments and which must return the arguments to pass to the script post_callback -- An object to call after each script execution with (name, priority, path, succeed) as arguments """ result = {'succeed': {}, 'failed': {}} hooks_dict = {} # Retrieve hooks if not hooks: hooks_dict = hook_list(action, list_by='priority', show_info=True)['hooks'] else: hooks_names = hook_list(action, list_by='name', show_info=True)['hooks'] # Add similar hooks to the list # For example: Having a 16-postfix hook in the list will execute a # xx-postfix_dkim as well all_hooks = [] for n in hooks: for key in hooks_names.keys(): if key == n or key.startswith("%s_" % n) \ and key not in all_hooks: all_hooks.append(key) # Iterate over given hooks names list for n in all_hooks: try: hl = hooks_names[n] except KeyError: raise MoulinetteError(errno.EINVAL, m18n.n('hook_name_unknown', n)) # Iterate over hooks with this name for h in hl: # Update hooks dict d = hooks_dict.get(h['priority'], dict()) d.update({n: {'path': h['path']}}) hooks_dict[h['priority']] = d if not hooks_dict: return result # Validate callbacks if not callable(pre_callback): pre_callback = lambda name, priority, path, args: args if not callable(post_callback): post_callback = lambda name, priority, path, succeed: None # Iterate over hooks and execute them for priority in sorted(hooks_dict): for name, info in iter(hooks_dict[priority].items()): state = 'succeed' path = info['path'] try: hook_args = pre_callback(name=name, priority=priority, path=path, args=args) hook_exec(path, args=hook_args, chdir=chdir, env=env, no_trace=no_trace, raise_on_error=True) except MoulinetteError as e: state = 'failed' logger.error(e.strerror, exc_info=1) post_callback(name=name, priority=priority, path=path, succeed=False) else: post_callback(name=name, priority=priority, path=path, succeed=True) try: result[state][name].append(path) except KeyError: result[state][name] = [path] return result
def hook_list(action, list_by='name', show_info=False): """ List available hooks for an action Keyword argument: action -- Action name list_by -- Property to list hook by show_info -- Show hook information """ result = {} # Process the property to list hook by if list_by == 'priority': if show_info: def _append_hook(d, priority, name, path): # Use the priority as key and a dict of hooks names # with their info as value value = {'path': path} try: d[priority][name] = value except KeyError: d[priority] = {name: value} else: def _append_hook(d, priority, name, path): # Use the priority as key and the name as value try: d[priority].add(name) except KeyError: d[priority] = set([name]) elif list_by == 'name' or list_by == 'folder': if show_info: def _append_hook(d, priority, name, path): # Use the name as key and a list of hooks info - the # executed ones with this name - as value l = d.get(name, list()) for h in l: # Only one priority for the hook is accepted if h['priority'] == priority: # Custom hooks overwrite system ones and they # are appended at the end - so overwite it if h['path'] != path: h['path'] = path return l.append({'priority': priority, 'path': path}) d[name] = l else: if list_by == 'name': result = set() def _append_hook(d, priority, name, path): # Add only the name d.add(name) else: raise MoulinetteError(errno.EINVAL, m18n.n('hook_list_by_invalid')) def _append_folder(d, folder): # Iterate over and add hook from a folder for f in os.listdir(folder + action): if f[0] == '.' or f[-1] == '~': continue path = '%s%s/%s' % (folder, action, f) priority, name = _extract_filename_parts(f) _append_hook(d, priority, name, path) try: # Append system hooks first if list_by == 'folder': result['system'] = dict() if show_info else set() _append_folder(result['system'], hook_folder) else: _append_folder(result, hook_folder) except OSError: logger.debug("system hook folder not found for action '%s' in %s", action, hook_folder) try: # Append custom hooks if list_by == 'folder': result['custom'] = dict() if show_info else set() _append_folder(result['custom'], custom_hook_folder) else: _append_folder(result, custom_hook_folder) except OSError: logger.debug("custom hook folder not found for action '%s' in %s", action, custom_hook_folder) return {'hooks': 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 = ['check', '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): # Extract device name (1) and its addresses (2) m = re.match('([^\s@]+)(?:@[\S]+)?: (.*)', d, flags=re.DOTALL) if m: devices[m.group(1)] = m.group(2) # Retrieve monitoring for unit(s) for u in units: if u == 'check': result[u] = {} with open('/etc/yunohost/current_host', 'r') as f: domain = f.readline().rstrip() cmd_check_smtp = os.system('/bin/nc -z -w1 yunohost.org 25') if cmd_check_smtp == 0: smtp_check = m18n.n('network_check_smtp_ok') else: smtp_check = m18n.n('network_check_smtp_ko') try: answers = dns.resolver.query(domain, 'MX') mx_check = {} i = 0 for server in answers: mx_id = 'mx%s' % i mx_check[mx_id] = server i = i + 1 except: mx_check = m18n.n('network_check_mx_ko') result[u] = {'smtp_check': smtp_check, 'mx_check': mx_check} elif 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 else: logger.debug('interface name %s was not found', iname) elif u == 'infos': try: p_ipv4 = get_public_ip() except: p_ipv4 = '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_ipv4, 'local_ip': l_ip, 'gateway': gateway, } else: raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', unit=u)) if len(units) == 1: return result[units[0]] return result
def backup_restore(auth, name, hooks=[], ignore_hooks=False, apps=[], ignore_apps=False, force=False): """ Restore from a local backup archive Keyword argument: name -- Name of the local backup archive hooks -- List of restoration hooks names to execute ignore_hooks -- Do not execute backup hooks apps -- List of application names to restore ignore_apps -- Do not restore apps force -- Force restauration on an already installed system """ # Validate what to restore if ignore_hooks and ignore_apps: raise MoulinetteError(errno.EINVAL, m18n.n('restore_action_required')) # Retrieve and open the archive info = backup_info(name) archive_file = info['path'] try: tar = tarfile.open(archive_file, "r:gz") except: logger.debug("cannot open backup archive '%s'", archive_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Check temporary directory tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for restoration '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) # Check available disk space statvfs = os.statvfs(backup_path) free_space = statvfs.f_frsize * statvfs.f_bavail if free_space < info['size']: logger.debug("%dB left but %dB is needed", free_space, info['size']) raise MoulinetteError( errno.EIO, m18n.n('not_enough_disk_space', path=backup_path)) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_restore', args=[tmp_dir, retcode]) if not ret['failed']: filesystem.rm(tmp_dir, True, True) else: logger.warning(m18n.n('restore_cleaning_failed')) # Extract the tarball logger.info(m18n.n('backup_extracting_archive')) tar.extractall(tmp_dir) tar.close() # Retrieve backup info info_file = "%s/info.json" % tmp_dir try: with open(info_file, 'r') as f: info = json.load(f) except IOError: logger.debug("unable to load '%s'", info_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) else: logger.debug("restoring from backup '%s' created on %s", name, time.ctime(info['created_at'])) # Initialize restauration summary result result = { 'apps': [], 'hooks': {}, } # Check if YunoHost is installed if os.path.isfile('/etc/yunohost/installed'): logger.warning(m18n.n('yunohost_already_installed')) if not force: try: # Ask confirmation for restoring i = msignals.prompt( m18n.n('restore_confirm_yunohost_installed', answers='y/N')) except NotImplemented: pass else: if i == 'y' or i == 'Y': force = True if not force: _clean_tmp_dir() raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed')) else: # Retrieve the domain from the backup try: with open("%s/conf/ynh/current_host" % tmp_dir, 'r') as f: domain = f.readline().rstrip() except IOError: logger.debug("unable to retrieve current_host from the backup", exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) logger.debug("executing the post-install...") tools_postinstall(domain, 'yunohost', True) # Run system hooks if not ignore_hooks: # Filter hooks to execute hooks_list = set(info['hooks'].keys()) _is_hook_in_backup = lambda h: True if hooks: def _is_hook_in_backup(h): if h in hooks_list: return True logger.error(m18n.n('backup_archive_hook_not_exec', hook=h)) return False else: hooks = hooks_list # Check hooks availibility hooks_filtered = set() for h in hooks: if not _is_hook_in_backup(h): continue try: hook_info('restore', h) except: tmp_hooks = glob('{:s}/hooks/restore/*-{:s}'.format( tmp_dir, h)) if not tmp_hooks: logger.exception(m18n.n('restore_hook_unavailable', hook=h)) continue # Add restoration hook from the backup to the system # FIXME: Refactor hook_add and use it instead restore_hook_folder = custom_hook_folder + 'restore' filesystem.mkdir(restore_hook_folder, 755, True) for f in tmp_hooks: logger.debug( "adding restoration hook '%s' to the system " "from the backup archive '%s'", f, archive_file) shutil.copy(f, restore_hook_folder) hooks_filtered.add(h) if hooks_filtered: logger.info(m18n.n('restore_running_hooks')) ret = hook_callback('restore', hooks_filtered, args=[tmp_dir]) result['hooks'] = ret['succeed'] # Add apps restore hook if not ignore_apps: # Filter applications to restore apps_list = set(info['apps'].keys()) apps_filtered = set() if apps: for a in apps: if a not in apps_list: logger.error(m18n.n('backup_archive_app_not_found', app=a)) else: apps_filtered.add(a) else: apps_filtered = apps_list for app_instance_name in apps_filtered: tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) tmp_app_bkp_dir = tmp_app_dir + '/backup' # Parse app instance name and id # TODO: Use app_id to check if app is installed? app_id, app_instance_nb = _parse_app_instance_name( app_instance_name) # Check if the app is not already installed if _is_installed(app_instance_name): logger.error( m18n.n('restore_already_installed_app', app=app_instance_name)) continue # Check if the app has a restore script app_script = tmp_app_dir + '/settings/scripts/restore' if not os.path.isfile(app_script): logger.warning(m18n.n('unrestore_app', app=app_instance_name)) continue tmp_script = '/tmp/restore_' + app_instance_name app_setting_path = '/etc/yunohost/apps/' + app_instance_name logger.info( m18n.n('restore_running_app_script', app=app_instance_name)) try: # Copy app settings and set permissions # TODO: Copy app hooks too shutil.copytree(tmp_app_dir + '/settings', app_setting_path) filesystem.chmod(app_setting_path, 0555, 0444, True) filesystem.chmod(app_setting_path + '/settings.yml', 0400) # Copy restore script in a tmp file subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Prepare env. var. to pass to script env_dict = {} env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir # Execute app restore script hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) except: logger.exception( m18n.n('restore_app_failed', app=app_instance_name)) # Copy remove script in a tmp file filesystem.rm(tmp_script, force=True) app_script = tmp_app_dir + '/settings/scripts/remove' tmp_script = '/tmp/remove_' + app_instance_name subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Setup environment for remove script env_dict_remove = {} env_dict_remove["YNH_APP_ID"] = app_id env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str( app_instance_nb) # Execute remove script # TODO: call app_remove instead if hook_exec(tmp_script, args=[app_instance_name], env=env_dict_remove) != 0: logger.warning( m18n.n('app_not_properly_removed', app=app_instance_name)) # Cleaning app directory shutil.rmtree(app_setting_path, ignore_errors=True) else: result['apps'].append(app_instance_name) finally: filesystem.rm(tmp_script, force=True) # Check if something has been restored if not result['hooks'] and not result['apps']: _clean_tmp_dir(1) raise MoulinetteError(errno.EINVAL, m18n.n('restore_nothings_done')) if result['apps']: app_ssowatconf(auth) _clean_tmp_dir() logger.success(m18n.n('restore_complete')) return result
def domain_add(auth, domain, dyndns=False): """ Create a custom domain Keyword argument: domain -- Domain name to add dyndns -- Subscribe to DynDNS """ from yunohost.hook import hook_callback attr_dict = {'objectClass': ['mailDomain', 'top']} now = datetime.datetime.now() timestamp = str(now.year) + str(now.month) + str(now.day) if domain in domain_list(auth)['domains']: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) # DynDNS domain if dyndns: if len(domain.split('.')) < 3: raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_invalid')) from yunohost.dyndns import dyndns_subscribe try: r = requests.get('https://dyndns.yunohost.org/domains') except requests.ConnectionError: pass else: dyndomains = json.loads(r.text) dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: if 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')) try: # 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, 'cat %s/ca.pem >> %s/crt.pem' % (ssl_domain_path, 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 if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): raise MoulinetteError(errno.EIO, m18n.n('domain_creation_failed')) try: with open('/etc/yunohost/installed', 'r') as f: service_regen_conf( names=['nginx', 'metronome', 'dnsmasq', 'rmilter']) os.system('yunohost app ssowatconf > /dev/null 2>&1') except IOError: pass except: # Force domain removal silently try: domain_remove(auth, domain, True) except: pass raise hook_callback('post_domain_add', args=[domain]) logger.success(m18n.n('domain_created'))
def backup_create(name=None, description=None, output_directory=None, no_compress=False, ignore_hooks=False, hooks=[], ignore_apps=False, apps=[]): """ Create a backup local archive Keyword arguments: name -- Name of the backup archive description -- Short description of the backup output_directory -- Output directory for the backup no_compress -- Do not create an archive file hooks -- List of backup hooks names to execute ignore_hooks -- Do not execute backup hooks apps -- List of application names to backup ignore_apps -- Do not backup apps """ # TODO: Add a 'clean' argument to clean output directory tmp_dir = None env_var = {} # Validate what to backup if ignore_hooks and ignore_apps: raise MoulinetteError(errno.EINVAL, m18n.n('backup_action_required')) # Validate and define backup name timestamp = int(time.time()) if not name: name = time.strftime('%Y%m%d-%H%M%S') if name in backup_list()['archives']: raise MoulinetteError(errno.EINVAL, m18n.n('backup_archive_name_exists')) # Validate additional arguments if no_compress and not output_directory: raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_required')) if output_directory: output_directory = os.path.abspath(output_directory) # Check for forbidden folders if output_directory.startswith(archives_path) or \ re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', output_directory): raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_forbidden')) # Create the output directory if not os.path.isdir(output_directory): logger.debug("creating output directory '%s'", output_directory) os.makedirs(output_directory, 0750) # Check that output directory is empty elif no_compress and os.listdir(output_directory): raise MoulinetteError(errno.EIO, m18n.n('backup_output_directory_not_empty')) # Do not compress, so set temporary directory to output one and # disable bind mounting to prevent data loss in case of a rm # See: https://dev.yunohost.org/issues/298 if no_compress: logger.debug('bind mounting will be disabled') tmp_dir = output_directory env_var['CAN_BIND'] = 0 else: output_directory = archives_path # Create archives directory if it does not exists if not os.path.isdir(archives_path): os.mkdir(archives_path, 0750) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_create', args=[tmp_dir, retcode]) if not ret['failed']: filesystem.rm(tmp_dir, True, True) return True else: logger.warning(m18n.n('backup_cleaning_failed')) return False # Create temporary directory if not tmp_dir: tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for backup '%s' already exists", tmp_dir) if not _clean_tmp_dir(): raise MoulinetteError( errno.EIO, m18n.n('backup_output_directory_not_empty')) filesystem.mkdir(tmp_dir, 0750, parents=True, uid='admin') # Initialize backup info info = { 'description': description or '', 'created_at': timestamp, 'apps': {}, 'hooks': {}, } # Run system hooks if not ignore_hooks: # Check hooks availibility hooks_filtered = set() if hooks: for hook in hooks: try: hook_info('backup', hook) except: logger.error(m18n.n('backup_hook_unknown', hook=hook)) else: hooks_filtered.add(hook) if not hooks or hooks_filtered: logger.info(m18n.n('backup_running_hooks')) ret = hook_callback('backup', hooks_filtered, args=[tmp_dir], env=env_var) if ret['succeed']: info['hooks'] = ret['succeed'] # Save relevant restoration hooks tmp_hooks_dir = tmp_dir + '/hooks/restore' filesystem.mkdir(tmp_hooks_dir, 0750, True, uid='admin') for h in ret['succeed'].keys(): try: i = hook_info('restore', h) except: logger.warning(m18n.n('restore_hook_unavailable', hook=h), exc_info=1) else: for f in i['hooks']: shutil.copy(f['path'], tmp_hooks_dir) # Backup apps if not ignore_apps: # Filter applications to backup apps_list = set(os.listdir('/etc/yunohost/apps')) apps_filtered = set() if apps: for a in apps: if a not in apps_list: logger.warning(m18n.n('unbackup_app', app=a)) else: apps_filtered.add(a) else: apps_filtered = apps_list # Run apps backup scripts tmp_script = '/tmp/backup_' + str(timestamp) for app_instance_name in apps_filtered: app_setting_path = '/etc/yunohost/apps/' + app_instance_name # Check if the app has a backup and restore script app_script = app_setting_path + '/scripts/backup' app_restore_script = app_setting_path + '/scripts/restore' if not os.path.isfile(app_script): logger.warning(m18n.n('unbackup_app', app=app_instance_name)) continue elif not os.path.isfile(app_restore_script): logger.warning(m18n.n('unrestore_app', app=app_instance_name)) tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) tmp_app_bkp_dir = tmp_app_dir + '/backup' logger.info( m18n.n('backup_running_app_script', app=app_instance_name)) try: # Prepare backup directory for the app filesystem.mkdir(tmp_app_bkp_dir, 0750, True, uid='admin') shutil.copytree(app_setting_path, tmp_app_dir + '/settings') # Copy app backup script in a temporary folder and execute it subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Prepare env. var. to pass to script app_id, app_instance_nb = _parse_app_instance_name( app_instance_name) env_dict = env_var.copy() env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) except: logger.exception( m18n.n('backup_app_failed', app=app_instance_name)) # Cleaning app backup directory shutil.rmtree(tmp_app_dir, ignore_errors=True) else: # Add app info i = app_info(app_instance_name) info['apps'][app_instance_name] = { 'version': i['version'], 'name': i['name'], 'description': i['description'], } finally: filesystem.rm(tmp_script, force=True) # Check if something has been saved if not info['hooks'] and not info['apps']: _clean_tmp_dir(1) raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done')) # Calculate total size backup_size = int( subprocess.check_output(['du', '-sb', tmp_dir]).split()[0].decode('utf-8')) info['size'] = backup_size # Create backup info file with open("%s/info.json" % tmp_dir, 'w') as f: f.write(json.dumps(info)) # Create the archive if not no_compress: logger.info(m18n.n('backup_creating_archive')) # Check free space in output directory at first avail_output = subprocess.check_output( ['df', '--block-size=1', '--output=avail', tmp_dir]).split() if len(avail_output) < 2 or int(avail_output[1]) < backup_size: logger.debug('not enough space at %s (free: %s / needed: %d)', output_directory, avail_output[1], backup_size) _clean_tmp_dir(3) raise MoulinetteError( errno.EIO, m18n.n('not_enough_disk_space', path=output_directory)) # Open archive file for writing archive_file = "%s/%s.tar.gz" % (output_directory, name) try: tar = tarfile.open(archive_file, "w:gz") except: logger.debug("unable to open '%s' for writing", archive_file, exc_info=1) _clean_tmp_dir(2) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Add files to the archive try: tar.add(tmp_dir, arcname='') tar.close() except IOError as e: logger.error(m18n.n('backup_archive_writing_error'), exc_info=1) _clean_tmp_dir(3) raise MoulinetteError(errno.EIO, m18n.n('backup_creation_failed')) # FIXME : it looks weird that the "move info file" is not enabled if # user activated "no_compress" ... or does it really means # "dont_keep_track_of_this_backup_in_history" ? # Move info file shutil.move(tmp_dir + '/info.json', '{:s}/{:s}.info.json'.format(archives_path, name)) # If backuped to a non-default location, keep a symlink of the archive # to that location if output_directory != archives_path: link = "%s/%s.tar.gz" % (archives_path, name) os.symlink(archive_file, link) # Clean temporary directory if tmp_dir != output_directory: _clean_tmp_dir() logger.success(m18n.n('backup_created')) # Return backup info info['name'] = name return {'archive': info}
def firewall_reload(skip_upnp=False): """ Reload all firewall rules Keyword arguments: skip_upnp -- Do not refresh port forwarding using UPnP """ from yunohost.hook import hook_callback reloaded = False errors = False # Check if SSH port is allowed ssh_port = _get_ssh_port() if ssh_port not in firewall_list()['opened_ports']: firewall_allow('TCP', ssh_port, no_reload=True) # Retrieve firewall rules and UPnP status firewall = firewall_list(raw=True) upnp = firewall_upnp()['enabled'] if not skip_upnp else False # IPv4 try: process.check_output("iptables -L") except process.CalledProcessError as e: logger.debug('iptables seems to be not available, it outputs:\n%s', prependlines(e.output.rstrip(), '> ')) logger.warning(m18n.n('iptables_unavailable')) else: rules = [ "iptables -F", "iptables -X", "iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT", ] # Iterate over ports and add rule for protocol in ['TCP', 'UDP']: for port in firewall['ipv4'][protocol]: rules.append("iptables -A INPUT -p %s --dport %s -j ACCEPT" \ % (protocol, process.quote(str(port)))) rules += [ "iptables -A INPUT -i lo -j ACCEPT", "iptables -A INPUT -p icmp -j ACCEPT", "iptables -P INPUT DROP", ] # Execute each rule if process.check_commands(rules, callback=_on_rule_command_error): errors = True reloaded = True # IPv6 try: process.check_output("ip6tables -L") except process.CalledProcessError as e: logger.debug('ip6tables seems to be not available, it outputs:\n%s', prependlines(e.output.rstrip(), '> ')) logger.warning(m18n.n('ip6tables_unavailable')) else: rules = [ "ip6tables -F", "ip6tables -X", "ip6tables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT", ] # Iterate over ports and add rule for protocol in ['TCP', 'UDP']: for port in firewall['ipv6'][protocol]: rules.append("ip6tables -A INPUT -p %s --dport %s -j ACCEPT" \ % (protocol, process.quote(str(port)))) rules += [ "ip6tables -A INPUT -i lo -j ACCEPT", "ip6tables -A INPUT -p icmpv6 -j ACCEPT", "ip6tables -P INPUT DROP", ] # Execute each rule if process.check_commands(rules, callback=_on_rule_command_error): errors = True reloaded = True if not reloaded: raise MoulinetteError(errno.ESRCH, m18n.n('firewall_reload_failed')) hook_callback('post_iptable_rules', args=[upnp, os.path.exists("/proc/net/if_inet6")]) if upnp: # Refresh port forwarding with UPnP firewall_upnp(no_refresh=False) # TODO: Use service_restart os.system("service fail2ban restart") if errors: logger.warning(m18n.n('firewall_rules_cmd_failed')) else: logger.success(m18n.n('firewall_reloaded')) return firewall_list()
def tools_postinstall(domain, password, ignore_dyndns=False): """ YunoHost post-install Keyword argument: domain -- YunoHost main domain ignore_dyndns -- Do not subscribe domain to a DynDNS service (only needed for nohost.me, noho.st domains) password -- YunoHost admin password """ dyndns = not ignore_dyndns # Do some checks at first if os.path.isfile('/etc/yunohost/installed'): raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed')) if len(domain.split('.')) >= 3 and not ignore_dyndns: try: r = requests.get('https://dyndns.yunohost.org/domains') except requests.ConnectionError: pass else: dyndomains = json.loads(r.text) dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: dyndns = True else: raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) else: dyndns = False else: dyndns = False logger.info(m18n.n('yunohost_installing')) # Initialize LDAP for YunoHost # TODO: Improve this part by integrate ldapinit into conf_regen hook auth = tools_ldapinit() # Create required folders folders_to_create = [ '/etc/yunohost/apps', '/etc/yunohost/certs', '/var/cache/yunohost/repo', '/home/yunohost.backup', '/home/yunohost.app' ] for folder in folders_to_create: try: os.listdir(folder) except OSError: os.makedirs(folder) # Change folders permissions os.system('chmod 755 /home/yunohost.app') # Set hostname to avoid amavis bug if os.system('hostname -d') != 0: os.system('hostname yunohost.yunohost.org') # Add a temporary SSOwat rule to redirect SSO to admin page try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: raise MoulinetteError( errno.EINVAL, m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) except IOError: ssowat_conf = {} if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} ssowat_conf['redirected_urls']['/'] = domain + '/yunohost/admin' try: with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: raise MoulinetteError( errno.EPERM, m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) os.system('chmod 644 /etc/ssowat/conf.json.persistent') # Create SSL CA service_regen_conf(['ssl'], force=True) ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' command_list = [ 'echo "01" > %s/serial' % ssl_dir, 'rm %s/index.txt' % ssl_dir, 'touch %s/index.txt' % ssl_dir, 'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir), 'sed -i "s/yunohost.org/%s/g" %s/openssl.ca.cnf ' % (domain, ssl_dir), 'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch' % (ssl_dir, ssl_dir, ssl_dir), 'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir, 'update-ca-certificates' ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EPERM, m18n.n('yunohost_ca_creation_failed')) # New domain config domain_add(auth, domain, dyndns) tools_maindomain(auth, domain) # Generate SSOwat configuration file app_ssowatconf(auth) # Change LDAP admin password tools_adminpw(auth, password) # Enable UPnP silently and reload firewall firewall_upnp('enable', no_refresh=True) os.system('touch /etc/yunohost/installed') # Enable and start YunoHost firewall at boot time os.system('update-rc.d yunohost-firewall enable') os.system('service yunohost-firewall start') service_regen_conf(force=True) logger.success(m18n.n('yunohost_configured'))
def firewall_upnp(action='status', no_refresh=False): """ Manage port forwarding using UPnP Note: 'reload' action is deprecated and will be removed in the near future. You should use 'status' instead - which retrieve UPnP status and automatically refresh port forwarding if 'no_refresh' is False. Keyword argument: action -- Action to perform no_refresh -- Do not refresh port forwarding """ firewall = firewall_list(raw=True) enabled = firewall['uPnP']['enabled'] # Compatibility with previous version if action == 'reload': logger.info("'reload' action is deprecated and will be removed") try: # Remove old cron job os.remove('/etc/cron.d/yunohost-firewall') except: pass action = 'status' no_refresh = False if action == 'status' and no_refresh: # Only return current state return {'enabled': enabled} elif action == 'enable' or (enabled and action == 'status'): # Add cron job with open(upnp_cron_job, 'w+') as f: f.write('*/50 * * * * root ' '/usr/bin/yunohost firewall upnp status >>/dev/null\n') # Open port 1900 to receive discovery message if 1900 not in firewall['ipv4']['UDP']: firewall_allow('UDP', 1900, no_upnp=True, no_reload=True) if not enabled: firewall_reload(skip_upnp=True) enabled = True elif action == 'disable' or (not enabled and action == 'status'): try: # Remove cron job os.remove(upnp_cron_job) except: pass enabled = False if action == 'status': no_refresh = True else: raise MoulinetteError(errno.EINVAL, m18n.n('action_invalid', action=action)) # Refresh port mapping using UPnP if not no_refresh: upnpc = miniupnpc.UPnP() upnpc.discoverdelay = 3000 # Discover UPnP device(s) logger.debug('discovering UPnP devices...') nb_dev = upnpc.discover() logger.debug('found %d UPnP device(s)', int(nb_dev)) if nb_dev < 1: logger.error(m18n.n('upnp_dev_not_found')) enabled = False else: try: # Select UPnP device upnpc.selectigd() except: logger.info('unable to select UPnP device', exc_info=1) enabled = False else: # Iterate over ports for protocol in ['TCP', 'UDP']: for port in firewall['uPnP'][protocol]: # Clean the mapping of this port if upnpc.getspecificportmapping(port, protocol): try: upnpc.deleteportmapping(port, protocol) except: pass if not enabled: continue try: # Add new port mapping upnpc.addportmapping( port, protocol, upnpc.lanaddr, port, 'yunohost firewall: port %d' % port, '') except: logger.info('unable to add port %d using UPnP', port, exc_info=1) enabled = False if enabled != firewall['uPnP']['enabled']: firewall = firewall_list(raw=True) firewall['uPnP']['enabled'] = enabled # Make a backup and update firewall file os.system("cp {0} {0}.old".format(firewall_file)) with open(firewall_file, 'w') as f: yaml.safe_dump(firewall, f, default_flow_style=False) if not no_refresh: # Display success message if needed if action == 'enable' and enabled: logger.success(m18n.n('upnp_enabled')) elif action == 'disable' and not enabled: logger.success(m18n.n('upnp_disabled')) # Make sure to disable UPnP elif action != 'disable' and not enabled: firewall_upnp('disable', no_refresh=True) if not enabled and (action == 'enable' or 1900 in firewall['ipv4']['UDP']): # Close unused port 1900 firewall_disallow('UDP', 1900, no_reload=True) if not no_refresh: firewall_reload(skip_upnp=True) if action == 'enable' and not enabled: raise MoulinetteError(errno.ENXIO, m18n.n('upnp_port_open_failed')) return {'enabled': enabled}
def tools_ldapinit(): """ YunoHost LDAP initialization """ # Instantiate LDAP Authenticator auth = init_authenticator( ('ldap', 'default'), { 'uri': "ldap://localhost:389", 'base_dn': "dc=yunohost,dc=org", 'user_rdn': "cn=admin" }) auth.authenticate('yunohost') with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml' ) as f: ldap_map = yaml.load(f) for rdn, attr_dict in ldap_map['parents'].items(): try: auth.add(rdn, attr_dict) except: pass for rdn, attr_dict in ldap_map['children'].items(): try: auth.add(rdn, attr_dict) except: pass admin_dict = { 'cn': 'admin', 'uid': 'admin', 'description': 'LDAP Administrator', 'gidNumber': '1007', 'uidNumber': '1007', 'homeDirectory': '/home/admin', 'loginShell': '/bin/bash', 'objectClass': ['organizationalRole', 'posixAccount', 'simpleSecurityObject'], 'userPassword': '******' } auth.update('cn=admin', admin_dict) # Force nscd to refresh cache to take admin creation into account subprocess.call(['nscd', '-i', 'passwd']) # Check admin actually exists now try: pwd.getpwnam("admin") except KeyError: logger.error(m18n.n('ldap_init_failed_to_create_admin')) raise MoulinetteError(errno.EINVAL, m18n.n('installation_failed')) logger.success(m18n.n('ldap_initialized')) return auth
def 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: firewall_upnp(action=['reload']) os.system("iptables -F") os.system("iptables -X") os.system( "iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT") if ssh_port not in firewall['ipv4']['TCP']: firewall_allow(ssh_port) # 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 RELATED,ESTABLISHED -j ACCEPT" ) if ssh_port not in firewall['ipv6']['TCP']: firewall_allow(ssh_port, 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 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 firewall_upnp(action=None): """ Add uPnP cron and enable uPnP in firewall.yml, or the opposite. Keyword argument: action -- enable/disable/reload """ 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( 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ \n*/50 * * * * root yunohost firewall upnp reload >>/dev/null') msignals.display(m18n.n('upnp_enabled'), 'success') if action == 'disable': firewall['uPnP']['enabled'] = False try: upnpc = miniupnpc.UPnP() upnpc.discoverdelay = 3000 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 == 'reload': upnp = firewall['uPnP']['enabled'] if upnp: try: upnpc = miniupnpc.UPnP() upnpc.discoverdelay = 3000 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') 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 dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None): """ Update IP on DynDNS platform Keyword argument: domain -- Full domain to update dyn_host -- Dynette DNS server to inform key -- Public DNS key ipv4 -- IP address to send ipv6 -- IPv6 address to send """ # IPv4 if ipv4 is None: ipv4 = get_public_ip() try: with open('/etc/yunohost/dyndns/old_ip', 'r') as f: old_ip = f.readline().rstrip() except IOError: old_ip = '0.0.0.0' # IPv6 if ipv6 is None: try: ip_route_out = subprocess.check_output( ['ip', 'route', 'get', '2000::']).split('\n') if len(ip_route_out) > 0: route = IPRouteLine(ip_route_out[0]) if not route.unreachable: ipv6 = route.src_addr except (OSError, ValueError) as e: # Unlikely case "ip route" does not return status 0 # or produces unexpected output raise MoulinetteError(errno.EBADMSG, "ip route cmd error : {}".format(e)) if ipv6 is None: logger.info(m18n.n('no_ipv6_connectivity')) try: with open('/etc/yunohost/dyndns/old_ipv6', 'r') as f: old_ipv6 = f.readline().rstrip() except IOError: old_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000' if old_ip != ipv4 or old_ipv6 != ipv6: if domain is None: # Retrieve the first registered domain for path in glob.iglob('/etc/yunohost/dyndns/K*.private'): match = re_dyndns_private_key.match(path) if not match: continue _domain = match.group('domain') try: # Check if domain is registered if requests.get('https://{0}/test/{1}'.format( dyn_host, _domain)).status_code == 200: continue except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) domain = _domain key = path break if not domain: raise MoulinetteError(errno.EINVAL, m18n.n('dyndns_no_domain_registered')) if key is None: keys = glob.glob( '/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) if len(keys) > 0: key = keys[0] if not key: raise MoulinetteError(errno.EIO, m18n.n('dyndns_key_not_found')) host = domain.split('.')[1:] host = '.'.join(host) lines = [ 'server %s' % dyn_host, 'zone %s' % host, 'update delete %s. A' % domain, 'update delete %s. AAAA' % domain, 'update delete %s. MX' % domain, 'update delete %s. TXT' % domain, 'update delete pubsub.%s. A' % domain, 'update delete pubsub.%s. AAAA' % domain, 'update delete muc.%s. A' % domain, 'update delete muc.%s. AAAA' % domain, 'update delete vjud.%s. A' % domain, 'update delete vjud.%s. AAAA' % domain, 'update delete _xmpp-client._tcp.%s. SRV' % domain, 'update delete _xmpp-server._tcp.%s. SRV' % domain, 'update add %s. 1800 A %s' % (domain, ipv4), '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, ipv4), 'update add muc.%s. 1800 A %s' % (domain, ipv4), 'update add vjud.%s. 1800 A %s' % (domain, ipv4), '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) ] if ipv6 is not None: lines += [ 'update add %s. 1800 AAAA %s' % (domain, ipv6), 'update add pubsub.%s. 1800 AAAA %s' % (domain, ipv6), 'update add muc.%s. 1800 AAAA %s' % (domain, ipv6), 'update add vjud.%s. 1800 AAAA %s' % (domain, ipv6), ] lines += ['show', 'send'] with open('/etc/yunohost/dyndns/zone', 'w') as zone: for line in lines: zone.write(line + '\n') if os.system('/usr/bin/nsupdate -k %s /etc/yunohost/dyndns/zone' % key) == 0: logger.success(m18n.n('dyndns_ip_updated')) with open('/etc/yunohost/dyndns/old_ip', 'w') as f: f.write(ipv4) if ipv6 is not None: with open('/etc/yunohost/dyndns/old_ipv6', 'w') as f: f.write(ipv6) else: os.system('rm -f /etc/yunohost/dyndns/old_ip') os.system('rm -f /etc/yunohost/dyndns/old_ipv6') raise MoulinetteError(errno.EPERM, m18n.n('dyndns_ip_update_failed'))
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, stderr=subprocess.STDOUT, shell=True) except subprocess.CalledProcessError as e: if 'usage:' in e.output.lower(): msignals.display(m18n.n('service_status_failed', name), 'warning') else: result[name]['status'] = 'inactive' 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 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, mailbox_quota=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 from yunohost.app import app_ssowatconf 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=username)) 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( 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', domain=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', domain=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=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=mail)) new_attr_dict['maildrop'] = user['maildrop'] if mailbox_quota is not None: new_attr_dict['mailuserquota'] = mailbox_quota if auth.update('uid=%s,ou=users' % username, new_attr_dict): logger.success(m18n.n('user_updated')) app_ssowatconf(auth) return user_info(auth, username) else: raise MoulinetteError(169, m18n.n('user_update_failed'))
def _authenticate_credentials(self, credentials=None): if not credentials == self.name: raise MoulinetteError("invalid_password", raw_msg=True) return
def user_create(auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ Create user Keyword argument: firstname lastname username -- Must be unique mail -- Main mail address must be unique password mailbox_quota -- Mailbox size quota """ import pwd from yunohost.domain import domain_list from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({'uid': username, 'mail': mail}) # Validate uniqueness of username in system users try: pwd.getpwnam(username) except KeyError: pass else: raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) # Check that the mail domain exists if mail[mail.find('@') + 1:] not in domain_list(auth)['domains']: raise MoulinetteError( errno.EINVAL, m18n.n('mail_domain_unknown', domain=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 + '$' user_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, 'mailuserquota': mailbox_quota, 'userPassword': user_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())) except ValueError as e: raise MoulinetteError( errno.EINVAL, m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) except IOError: ssowat_conf = {} if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf[ 'redirected_urls']: del ssowat_conf['redirected_urls']['/'] try: with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: raise MoulinetteError( errno.EPERM, m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) if auth.add(rdn, attr_dict): # Invalidate passwd to take user creation into account subprocess.call(['nscd', '-i', 'passwd']) # 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}): try: # Attempt to create user home folder subprocess.check_call(['su', '-', username, '-c', "''"]) except subprocess.CalledProcessError: if not os.path.isdir('/home/{0}'.format(username)): logger.warning(m18n.n('user_home_creation_failed'), exc_info=1) app_ssowatconf(auth) # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) hook_callback('post_user_create', args=[username, mail, password, firstname, lastname]) return {'fullname': fullname, 'username': username, 'mail': mail} raise MoulinetteError(169, m18n.n('user_creation_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, domain_list 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 } if not new_domain: raise MoulinetteError(errno.EINVAL, m18n.n('new_domain_required')) if new_domain not in domain_list(auth)['domains']: domain_add(auth, new_domain) config_files = [ '/etc/postfix/main.cf', '/etc/metronome/metronome.cfg.lua', '/etc/dovecot/dovecot.conf', '/usr/share/yunohost/yunohost-config/others/startup', '/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)) ## Update DNS zone file for old and new domains main_subdomains = ['pubsub', 'muc', 'vjud'] try: with open('/var/lib/bind/%s.zone' % old_domain, 'r') as f: old_zone = f.read() except IOError: pass else: # Remove unneeded subdomains entries for sub in main_subdomains: old_zone = re.sub( r'^({sub}.{domain}.|{sub})[\ \t]+(IN).*$[\n]?'.format( sub=sub, domain=old_domain), '', old_zone, 1, re.MULTILINE) with open('/var/lib/bind/%s.zone' % old_domain, 'w') as f: f.write(old_zone) try: with open('/var/lib/bind/%s.zone' % new_domain, 'r') as f: new_zone = f.read() except IOError: msignals.display(m18n.n('domain_zone_not_found', new_domain), 'warning') else: # Add main subdomains entries for sub in main_subdomains: new_zone += '{sub} IN CNAME {domain}.\n'.format( sub=sub, domain=new_domain) with open('/var/lib/bind/%s.zone' % new_domain, 'w') as f: f.write(new_zone) os.system('rm /etc/ssl/private/yunohost_key.pem') os.system('rm /etc/ssl/certs/yunohost_crt.pem') command_list = [ 'rm -f /etc/nginx/conf.d/%s.d/yunohost_local.conf' % old_domain, 'cp /usr/share/yunohost/yunohost-config/nginx/yunohost_local.conf /etc/nginx/conf.d/%s.d/' % new_domain, '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 metronome restart', 'service postfix restart', 'service dovecot restart', 'service amavis restart', 'service nginx restart', ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) if dyndns and len(new_domain.split('.')) >= 3: try: r = requests.get('https://dyndns.yunohost.org/domains') except ConnectionError: pass else: 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')