Hacked By AnonymousFox

Current Path : /usr/share/cagefs/
Upload File :
Current File : //usr/share/cagefs/cagefsctl.py

#!/opt/cloudlinux/venv/bin/python3 -bb
# -*- coding: utf-8 -*-
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from future import standard_library
from typing import Dict, Optional, List
standard_library.install_aliases()
from builtins import *
from future.utils import native_str
import copy
import errno
import os
import locale
import psutil
import grp
import sys
import configparser
import getopt
import string
import shutil
import subprocess
import secureio
import stat
import time
import fcntl
import random
import struct
import signal
import pickle
import cagefs_da_lib
import re
import yaml
import glob
from collections import defaultdict
from enum import Enum
from cldetectlib import is_plesk, is_cpanel
from clcommon.utils import (
    ExternalProgramFailed,
    create_symlink,
    is_socket_file,
    is_may_detach_mounts_enabled,
    mod_makedirs,
)
from clcommon.clproc import ProcLve
from clcommon import ClPwd, reload_processes, clconfpars, clcaptain
from clcommon.clfunc import unicodeify, byteify
from cagefslib import (
    stripslash,
    CageFSException,
    SYSTEMD_JOURNAL_SOCKET,
    is_new_syslog_socket_used,
    relative_symlink,
    is_running_without_lve,
)
from logs import logger
import cagefshooks
import tempfile
import unshare
import cagefs_without_lve_lib
import cagefs_universal_hook_lib


LVECTL = '/usr/sbin/lvectl'
if is_running_without_lve():
    # mock lvectl, because it will not be used in containers probably
    LVECTL = '/bin/true'

UMOUNT = '/bin/umount'
MOUNT = '/bin/mount'
LVE_UMOUNT = "/bin/lve_umount"
BASEDIR = '/var/cagefs'
BASEDIR_UID = '/var/cagefs.uid'
SKELETON = '/usr/share/cagefs-skeleton'
SKELETON_NAME = '/cagefs-skeleton/'
LIBDIR = '/usr/share/cagefs'
INIPREFIX = '/etc/cagefs/'
MP_PREFIX = '/usr/share/cagefs/'
CONFIG_DIR = '/etc/cagefs/conf.d/'
ETC_MPFILE = INIPREFIX + 'cagefs.mp'
PREV_MPFILE = MP_PREFIX + 'cagefs.mp.prev'
LOCKNAME = '/usr/share/cagefs/.lock'
FUSE_WHITE_LIST = '/etc/cagefs/etc.safe/etc.system'
FUSE_SAFE_LIST = '/etc/cagefs/etc.safe/etc.safe'
FUSE_DIR = '/etc/cagefs/etc.safe'
FILES_LIST = '/usr/share/cagefs/skeleton.files.list'
LIBS_LIST = '/usr/share/cagefs/skeleton.libs.list'
PASSWD_CACHE = '/usr/share/cagefs/passwd.cache'
WORK_CONFIG_DIR = "/usr/share/cagefs/conf.d"
EXCLUDE_PATH = '/etc/cagefs/exclude'
EXCLUDE_SAVE_PATH = '/usr/share/cagefs/exclude'
MIN_UID = 500
MIN_UID_FILENAME = "/etc/cagefs/cagefs.min.uid"
SERVICE_CAGEFS_LOCK = "/var/lock/subsys/cagefs"
DISABLE_ETCFS = "/etc/cagefs/etc.safe/disable.etcfs"
DIFF = "/usr/bin/diff"
PROXYEXEC_SOCKET_DIR_OLD = "/var/run/proxyexec/cagefs.sock"
PROXYEXEC_SOCKET_DIR = "/var/lib/proxyexec/cagefs.sock"
BLACK_LIST_FILE = "/etc/cagefs/black.list"
PLUGIN_STATE = "/usr/share/cagefs-plugins/install-cagefs-plugin.py"
EMPTY_DIR = '/usr/share/cagefs/.cagefs.empty'
STD_PACKAGES_FILE = "/usr/share/cagefs/exclude.packages"
PROXY_COMMANDS = "/etc/cagefs/proxy.commands"
REMOUNT_FLAG = '/usr/share/cagefs/need.remount'
INFO_LOG_FILE = "/var/log/cagefs.log"
LICENSE_TIMESTAMP_FILE = '/var/lve/lveinfo.ver'
SELECTOR_CONF_DIR_TEMPLATE = '/usr/share/l.v.e-manager/cl.{}'
DEV_SHM_OPTIONS = "/etc/cagefs/dev.shm.options"
DEBUG_CAGEFS_MARKER = '/etc/cagefs/enabled_debug'

disabled_dir = INIPREFIX + 'users.disabled'
enabled_dir = INIPREFIX + 'users.enabled'

SKELETON_INITIALIZED = 'Initialized'
SKELETON_NOT_INITIALIZED = 'Not initialized'

kernel_header = os.uname().release


# Default RPM packages
class DefaultPackages(Enum):
    """Default packages, used in cagefs --init"""
    common_packages = [
        'tcl',
        'cpp',
        'gcc',
        'automake',
        'autoconf',
        'm4',
        'mc',
        'ghostscript',
        'fontconfig',
        'aspell',
        'aspell-en',
        'hunspell',
        'coreutils',
        'python3-virtualenv',
        'libxml2',
        'recode',
        'crypto-policies',
        'snmptrapd',
        'unixodbc',
        'openssl',
        'alt-libicu',
        'enchant',
        'curl',
        'cpanel-git',  # git +
        'git',
    ]

    ubuntu = [
        'imagemagick',
        'libmagick++-dev',
        'perlmagick',
        'expat',
        'libexpat1-dev',
        'libltdl7',
        'libnss3',
        'build-essential',
        f'linux-headers-{kernel_header}',
        'gfortran',
        'lib32gcc-10-dev',
        'g++',
        'libtext-pdf-perl',
        'libedit2',
        'hunspell-en-us',
        'libcogl-pango-dev',
        'python3.8',
        'libc-client2007e',
        'libodbc1',
        'libmhash2',
        'libmcrypt4',
        'libxslt1.1',
        'libtidy5deb1',
        'libicu66',
        'libicu-dev',
        'tmpreaper',
        'libgpg-error0',
        'postgresql',
        'postgresql-contrib',
        'libpng-dev',
        'libgmp3-dev',
        'libpam-modules',
        'bzip2',
        'libpam-cracklib',
        'ncdu',
        'libidn11',
        'libc-client2007e',
        'db5.3-util',
        'libncurses6',
        'slapd',
        'libxpm4',
        'libgcrypt20',
        'libsasl2-2',
        'zlib1g',
        'snmpd',
        'snmp',
        'libsnmp-dev',
        'libmm-dev',
        'libfreetype6',
        'libfreetype6-dev',
        'libssh2-1',
        'geoip-database',
        'ffmpeg',
        'dnsutils',
        'libgs9',
        'libgs-dev',
        'libgs9-common',
    ]

    centos = [
        'ImageMagick',
        'ImageMagick-c++',
        'ImageMagick-c++-devel',
        'ImageMagick-devel',
        'ImageMagick-perl',
        'cloudlinux-ImageMagick',
        'cloudlinux-ImageMagick-c++',
        'cloudlinux-ImageMagick-c++-devel',
        'cloudlinux-ImageMagick-devel',
        'expat',
        'expat-devel',
        'libtool-ltdl',
        'nss',
        'nss-softokn',  # -
        'compat-glibc-headers',  # not exist in centos
        'glibc-headers',
        'kernel-headers',
        'compat-libgcc-296',  # -
        'gcc-gfortran',
        'compat-gcc-34-c++',  # -
        'compat-gcc-34-g77',  # -
        'libgcc',
        'gcc-c++',
        'compat-gcc-34',  # -
        'redhat-rpm-config',
        'fontpackages-filesystem',  # -
        'perl-Text-PDF',
        'pdf-tools',  # -
        'perl-PDF-Reuse',  # -
        'libedit',
        'hunspell-en',
        'git-core',
        'pango',
        'mktemp',
        'scl-utils',  # -
        'python36',  # + python3.8
        'libc-client-2007e',
        'unixODBC-libs',
        'mhash',
        'tcp_wrappers',  # -
        'compat-libstdc++',  # -
        'libmcrypt',
        'libxslt',
        'libtidy',
        'libicu',
        'libicu-devel',
        'tmpwatch',
        'net-snmp',
        'libgpg-error',
        'postgresql-libs',
        'libpng',
        'gmp',
        'pam',
        'bzip2-libs',
        'cracklib',
        'ncurses',
        'libidn',
        'libc-client-2004g',
        'db4',
        'ncurses-libs',
        'openldap',
        'libXpm',
        'libgcrypt',
        'cyrus-sasl-lib',
        'zlib',
        'net-snmp-libs',
        'libmm',
        'freetype',
        'freetype-devel',
        'curl-devel',  # -
        'libssh2',
        'GeoIP',
        'cyrus-sasl',
        'ffmpeg-libs',
        'termcap',  # -
        'bind-utils',
        'libgs',
        'libgs-devel'
    ]


if 'ubuntu' in os.uname().version.lower():
    STD_PACKAGES = DefaultPackages.common_packages.value + DefaultPackages.ubuntu.value
else:
    STD_PACKAGES = DefaultPackages.common_packages.value + DefaultPackages.centos.value

# This line is changed in %install section of securelve.spec
cagefs_version = '7.6.12-1.el8.cloudlinux'


sys.path.append(LIBDIR)
import cagefslib
import repair_homes
from signals_handlers import sigterm_check
from cagefslib import is_ea4_enabled

cagefslib.FUSE_WHITE_LIST = FUSE_WHITE_LIST
cagefslib.FUSE_SAFE_LIST = FUSE_SAFE_LIST
cagefslib.SKELETON = SKELETON

# List of spamassasin dirs for add to Cagefs
SPAMASSASSIN_DIRS_FOR_CAGEFS = ['/usr/local/cpanel/3rdparty/bin', '/var/lib/spamassassin']


def save_passwd_cache(pw=None):
    if pw == None:
        pw = secureio.get_pwd_dict()
    try:
        umask_saved = os.umask(0o77)
        pf = open(PASSWD_CACHE, 'wb')
        # byteify because python2 uses bytes
        # and we in python3 use unicode
        pickle.dump(byteify(pw), pf, protocol=2)
        pf.close()
        os.umask(umask_saved)
        os.chmod(PASSWD_CACHE, 0o600)
    except Exception as err:
        secureio.print_error("saving", PASSWD_CACHE, '-', err)


def load_passwd_cache():
    pw = {}
    if os.path.isfile(PASSWD_CACHE):
        try:
            pf = open(PASSWD_CACHE, 'rb')
            # unicodeify because python2 saves bytes
            # into this file and we use unicode in python3
            pw = unicodeify(pickle.load(pf, encoding=locale.getpreferredencoding()))
            pf.close()
        except Exception as err:
            secureio.print_error("loading", PASSWD_CACHE, '-', err)
    return pw


# Returns list of users whose passwd entry has been changed
def get_modified_users(pw_old=None, pw_new=None):
    if pw_old == None:
        pw_old = load_passwd_cache()
    if pw_new == None:
        pw_new = secureio.get_pwd_dict()
    users = []
    for user in pw_new:
        try:
            if pw_new[user] != pw_old[user]:
                users.append(user)
        except KeyError:
            users.append(user)
            continue
    save_passwd_cache(pw_new)
    return users


def create_empty_dir():
    sigterm_check()
    if not os.path.lexists(EMPTY_DIR):
        try:
            mod_makedirs(EMPTY_DIR, 0o755)
        except (IOError, OSError):
            secureio.logging('Error: failed to create ' + EMPTY_DIR, SILENT, 1)
            sys.exit(1)
    else:
        try:
            os.chmod(EMPTY_DIR, 0o755)
        except (IOError, OSError):
            secureio.logging('Error: failed to set permissions to ' + EMPTY_DIR, SILENT, 1)
            sys.exit(1)


def mount_empty_dir(path):
    dest = SKELETON+path
    # parent dir exists ?
    if os.path.isdir(dest):
        # mount empty dir over parent dir
        for cmd in ([MOUNT, "-n", "-o", "nosuid", "--bind", EMPTY_DIR, dest], [MOUNT, "-n", "-o", "remount,ro,nosuid,bind", EMPTY_DIR, dest]):
            ret = subprocess.call(cmd)
            if ret != 0:
                secureio.print_error("failed to mount", EMPTY_DIR, '->', dest)
                sys.exit(1)


def etcfs_is_disabled():
    return os.path.exists(DISABLE_ETCFS)


def get_etc_version(path):
    fpath = path + cagefslib.ETC_VERSION
    if os.path.isfile(fpath):
        try:
            f = open(fpath, "r")
            ver = f.readline()
            f.close()
        except (IOError, OSError):
            return 0

        ver = ver.rstrip()

        try:
            # return version
            return int(ver)
        except ValueError:
            # return 0 as version
            return 0
    else:
        # return 0 as version
        return 0


def set_etc_version(path, ver):
    fpath = path + cagefslib.ETC_VERSION
    umask_saved = os.umask(0o77)
    try:
        f = open(fpath, "w")
        f.write("%d\n" % ver)
        f.close()
    except (IOError, OSError):
        secureio.logging('Error: failed to write ' + fpath, SILENT, 1)
        sys.exit(1)
    os.umask(umask_saved)


def copy_etc_version(src, dst):
    srcpath = src + cagefslib.ETC_VERSION
    dstpath = dst + cagefslib.ETC_VERSION
    try:
        # copy file and metadata
        shutil.copy2(srcpath, dstpath)
    except (OSError, IOError, shutil.Error):
        pass


LOGFILE = "/var/log/cagefs-update.log"

SILENT = 0
VERBOSE = 0

def remove_log_file():
    if os.path.isdir(LOGFILE):
        secureio.print_error(LOGFILE, "is a directory")
        sys.exit(1)
    elif os.path.isfile(LOGFILE):
        os.remove(LOGFILE)


MPDIRS = ['/var/lib/mysql', '/var/lib/dav', '/var/www/cgi-bin', '/opt', '/var/www/php-bin', '/dev/shm', '/var/www/html',
'/var/run/pgsql', '/var/passenger', '/dev/pts', '/usr/local/apache/domlogs', '/proc', PROXYEXEC_SOCKET_DIR,
'/var/spool/at', '/var/run/dbus', '/usr/local/cpanel/var', '/var/run/nscd', SELECTOR_CONF_DIR_TEMPLATE.format('nodejs'),
SELECTOR_CONF_DIR_TEMPLATE.format('python')]

MYSQL_SOCK_DIR='/var/lib/mysql'
MYSQL_SOCK='/var/lib/mysql/mysql.sock'
LITESPEED='/usr/local/lsws'

READ_ONLY_MOUNTS = [
        '/lib',
        '/usr/lib',
        '/lib64',
        '/usr/lib64',
        '/usr/include',
        '/usr/share/locale',
        '/usr/share/terminfo',
        '/usr/share/zoneinfo',
        '/usr/share/vim',
        '/usr/local/lib/perl5',
        '/usr/local/lib/php',
        '/usr/local/cpanel/etc',
        '/usr/local/cpanel/Cpanel',
        '/usr/local/cpanel/3rdparty/perl',
        '/usr/local/cpanel/3rdparty/lib',
        '/usr/local/cpanel/3rdparty/lib64',
        '/usr/local/cpanel/3rdparty/share',
        '/usr/local/cpanel/3rdparty/php',
        '/usr/local/cpanel/install',
        '/usr/local/cpanel/lib',
        '/usr/local/cpanel/htdocs',
        '/usr/local/cpanel/shared',
        '/usr/local/cpanel/whostmgr',
        '/usr/local/cpanel/share',
        '/usr/local/cpanel/php',
        '/usr/local/cpanel/libexec',
        '/usr/local/cpanel/lang',
        '/usr/local/cpanel/cgi-priv',
        '/usr/local/cpanel/cpaddons',
        '/usr/local/cpanel/Whostmgr',
        '/usr/local/cpanel/img-sys',
        '/usr/local/cpanel/modules-install',
        '/usr/local/cpanel/locale',
        '/usr/local/cpanel/scripts',
        '/usr/local/cpanel/sbin',
        '/usr/local/cpanel/base',
        '/usr/local/cpanel/hooks',
        '/usr/java',
        '/usr/saase',
        '/usr/local/easy',
        '/var/cpanel/ea4',
        '/usr/share/man',
]

SPLITTED_MOUNTS = ['/var/cpanel/userdata']
SPLITTED_UID_MOUNTS = ['/var/clwpos/uids', '/usr/share/alt-php-xray-tasks']


def is_dev_shm_isolated():
    """
    Return True when /dev/shm isolation is enabled
    see CAG-954 for details
    """
    return os.path.isfile(DEV_SHM_OPTIONS)


def is_outside_mp_path(path):
    path = cagefslib.addslash(path)
    for tmpdir in MPDIRS:
        if tmpdir[0] == '/' and path.startswith(tmpdir+'/'):
            return False
    return True


def save_cagefs_mp_backup():
    try:
        shutil.copyfile(ETC_MPFILE, PREV_MPFILE)
        os.chmod(PREV_MPFILE, 0o600)
    except:
        secureio.print_error('copying', ETC_MPFILE, 'to', PREV_MPFILE)


def create_mp(force, exit_on_error=False):
    if not force and os.path.isfile(ETC_MPFILE):
        print(ETC_MPFILE, "exists")
        return

    umask_saved = os.umask(0o22)
    f = open(ETC_MPFILE, 'w')

    f.write('# Lines, which start with "/", specify mounts, that are common for all users:\n')
    for tmpdir in MPDIRS:
        if is_plesk() and tmpdir == "/var/www/cgi-bin":
            continue
        if tmpdir == '/dev/shm' and is_dev_shm_isolated():
            # CAG-954: do not add /dev/shm mount to cagefs.mp when /dev/shm isolation is enabled
            continue
        if tmpdir[0] == '/' and os.path.isdir(tmpdir):
            f.write(tmpdir)
            f.write('\n')

    # /var/run/postgres should be used on CL5, CL6; /var/run/postgresql should be used on CL7
    # for details plz see CAG-593, CAG-528
    from cagefsreconfigure import POSTGRES_CL7_FOLDER, POSTGRES_CONF, DEFAULT_POSTGRES_FOLDER
    if os.path.isdir(POSTGRES_CONF):
        if os.path.isdir(DEFAULT_POSTGRES_FOLDER):      # CL5, CL6
            f.write('%s\n' % DEFAULT_POSTGRES_FOLDER)
        elif os.path.isdir(POSTGRES_CL7_FOLDER):        # should never happen
            f.write('%s\n' % POSTGRES_CL7_FOLDER)
    else:
        if os.path.isdir(POSTGRES_CL7_FOLDER):          # CL7
            f.write('%s\n' % POSTGRES_CL7_FOLDER)
        elif os.path.isdir(DEFAULT_POSTGRES_FOLDER):    # should never happen
            f.write('%s\n' % DEFAULT_POSTGRES_FOLDER)

    # detect if MYSQL socket is outside of cagefs
    if os.path.realpath(MYSQL_SOCK) != MYSQL_SOCK:
        rpath = os.path.realpath(MYSQL_SOCK)
        rdir = os.path.dirname(rpath)
        if rdir == '/tmp':
            print('Warning: MySQL socket is located in /tmp directory: path', MYSQL_SOCK, 'points to', rpath)
            print('This is not compatible with CageFS.')
            print('Please move socket outside of /tmp directory by changing socket= directive in /etc/my.cnf file and restart MySQL.')
            print('Then execute cagefsctl --create-mp')
            print('Default socket location -', MYSQL_SOCK)
            if exit_on_error:
                f.close()
                os.unlink(ETC_MPFILE)
                sys.exit(1)
        elif rdir != '/' and os.path.isdir(rdir) and is_outside_mp_path(rdir):
            f.write(rdir)
            f.write('\n')

    if os.path.isdir(LITESPEED):
        f.write(LITESPEED)
        f.write('\n')

    f.write('# You can add personal (individual) mounts for users, like below.\n')
    f.write('# Please, start line with "@" symbol, and then specify path and permissions (comma separated).\n')
    f.write('# These directories will be virtualized for each user.\n')
    f.write('@/var/spool/cron,700\n')
    f.write('@/var/run/screen,777\n')
    f.write('@'+cagefslib.VAR_RUN_CAGEFS+',700\n')
    f.write('@/var/cache/php-eaccelerator,777\n')
    f.write('@/var/php/apm/db,777\n')

    f.write('# Please add exclamation sign at the beginning of the line if you want to mount path read-only, like below.\n')
    for tmpdir in READ_ONLY_MOUNTS:
        if tmpdir[0] == '/' and os.path.isdir(tmpdir):
            f.write('!'+tmpdir+'\n')

    f.write('# Please add "%" sign at the beginning of the line if you want to "split" mount by username, like below.\n')
    for tmpdir in SPLITTED_MOUNTS:
        if tmpdir[0] == '/' and os.path.isdir(tmpdir):
            f.write('%'+tmpdir+'\n')

    f.write('# Please add "*" sign at the beginning of the line if you want to "split" mount by UID, like below.\n')
    for tmpdir in SPLITTED_UID_MOUNTS:
        if tmpdir[0] == '/' and os.path.isdir(tmpdir):
            f.write('*'+tmpdir+'\n')

    if is_cpanel():
        # Setup the spamassasin directories to CageFs on cPanel
        f.write('\n')
        #SPAMASSASSIN_DIRS_FOR_CAGEFS = ['/usr/local/cpanel/3rdparty/bin', '/var/lib/spamassassin']
        from cagefsreconfigure import BOX_TRAPPER_DIR
        for line in SPAMASSASSIN_DIRS_FOR_CAGEFS:
            if os.path.isdir(line):
                f.write('!'+line+'\n')
        if os.path.isdir(BOX_TRAPPER_DIR):
            f.write('!'+BOX_TRAPPER_DIR+'\n')
        f.write('\n')

    f.close()
    os.umask(umask_saved)
    os.chmod(ETC_MPFILE, 0o600)
    add_mounts_for_php_selector()
    add_mounts_for_ea_php_sessions()
    if is_plesk():
        from cagefsreconfigure import add_php_session_dir_plesk
        add_php_session_dir_plesk()

    # copy cagefs.mp to cagefs.mp.prev (in order to detect changes of cagefs.mp file in the future)
    if not os.path.isfile(PREV_MPFILE):
        save_cagefs_mp_backup()


def remove_mount_points(mounts, old_mounts, base_path = SKELETON):
    # Remove unused mount points
    for mount in old_mounts:
        if mount not in mounts:
            mount = mount.rstrip()
            try:
                os.removedirs(base_path + mount)
            except (OSError, IOError):
                pass


def remove_unused_mount_points():
    # Previous mpfile exists ?
    if os.path.isfile(PREV_MPFILE):
        # Read previous mount points
        mp_config_old = MountpointConfig(path=PREV_MPFILE,
                                         skip_errors=True,
                                         skip_cpanel_check=True)

        # Reading of old mpfile is successful ?
        if mp_config_old.common_mounts:
            # Read current mp-file
            mp_config = MountpointConfig()

            # Remove common mount points which are not used (from cagefs-skeleton)
            remove_mount_points(mp_config.common_mounts, mp_config_old.common_mounts)

            # Remove personal mount points which are not used (from cagefs-skeleton)
            remove_mount_points(mp_config.personal_mounts, mp_config_old.personal_mounts)

            # Remove splitted by name mount points which are not used (from cagefs-skeleton)
            remove_mount_points(mp_config.splitted_by_username_mounts,
                                mp_config_old.splitted_by_username_mounts)

            # Remove splitted by UID mount points which are not used (from cagefs-skeleton)
            remove_mount_points(mp_config.splitted_by_uid_mounts,
                                mp_config_old.splitted_by_uid_mounts)

            # Remove personal mount points (which are not used) from home dirs
            pw = secureio.clpwd.get_user_dict()
            for user in pw:
                line = pw[user]
                homepath = cagefslib.stripslash(line.pw_dir)
                cagefspath = homepath + '/.cagefs'
                secureio.set_user_perm(line.pw_uid, line.pw_gid)
                remove_mount_points(mp_config.personal_mounts,
                                    mp_config_old.personal_mounts,
                                    cagefspath)
                secureio.set_root_perm()

    save_cagefs_mp_backup()


def dirinjail(testdir, jail):
    if (testdir[-1]!= '/'):
        testdir = testdir+'/'
    return (jail == testdir[:len(jail)])


def user_exists(user):
    return user in secureio.clpwd.get_user_dict()


save_postfix = '.save'


def get_user_mode():
    if os.path.isdir(disabled_dir):
        if os.path.isdir(enabled_dir):
            return 'Error'
        else:
            return 'Enable All'
    elif os.path.isdir(enabled_dir):
        return 'Disable All'
    else:
        return 'Not Initialized'


def check_mode_error(mode = None, raise_exception = False):
    if mode == None:
        mode = get_user_mode()

    if mode == 'Error':
        if raise_exception:
            raise CageFSException
        secureio.print_error('both directories', enabled_dir, 'and', disabled_dir, 'exist.\n',
                                'Please, run one of the following commands:\n',
                                sys.argv[0], '--enable-all\n',
                                'to enable all users, except specified in', disabled_dir,'\n',
                                'or\n',
                                sys.argv[0], '--disable-all\n',
                                'to disable all users, except specified in', enabled_dir)
        sys.exit(1)
    elif mode == 'Not Initialized':
        if raise_exception:
            raise CageFSException
        if os.path.isdir(SKELETON+'/bin'):
            secureio.print_error('mode has not been selected yet.\n',
                                    'Please, run one of the following commands:\n',
                                    sys.argv[0], '--enable-all\n',
                                    'to enable all users, except specified in', disabled_dir,
                                    '\nor\n',
                                    sys.argv[0], '--disable-all\n',
                                    'to disable all users, except specified in', enabled_dir)
        else:
            secureio.print_error('CageFS is not initialized. Use "'+sys.argv[0]+' --init" to initialize CageFS')
        sys.exit(1)


def cagefs_is_enabled():
    return os.path.isdir(disabled_dir) or os.path.isdir(enabled_dir)


def save_dir_exists():
    return os.path.isdir(disabled_dir+save_postfix) or os.path.isdir(enabled_dir+save_postfix)


def save_dir(_dir):
    if os.path.isdir(_dir):
        if os.path.isdir(_dir+save_postfix):
            # This should never happen
            secureio.logging('Error : directory %s already exists' % (_dir+save_postfix))
        else:
            try:
                os.rename(_dir, _dir+save_postfix)
            except (OSError, IOError):
                secureio.print_error('failed to rename', _dir, 'to', _dir+save_postfix)


def restore_dir(_dir):
    if os.path.isdir(_dir+save_postfix):
        if os.path.isdir(_dir):
            # This should never happen
            secureio.logging('Error : directory %s already exists' % _dir)
        else:
            try:
                os.rename(_dir+save_postfix, _dir)
            except (OSError, IOError):
                secureio.print_error('failed to rename', _dir+save_postfix, 'to', _dir)


def disable_cagefs():
    if not cagefs_is_enabled():
        print('CageFS is disabled')
        return
    check_mode_error()
    save_dir(disabled_dir)
    save_dir(enabled_dir)
    if not cagefs_is_enabled():
        print('CageFS has been disabled')


def enable_cagefs():
    if cagefs_is_enabled():
        print('CageFS is enabled')
        return
    restore_dir(disabled_dir)
    restore_dir(enabled_dir)
    check_mode_error()
    if cagefs_is_enabled():
        print('CageFS has been enabled')


def check_save_dir(raise_exception=False):
    if save_dir_exists():
        if cagefs_is_enabled():
            # This should never happen
            if raise_exception:
                raise CageFSException
            secureio.print_error('CageFS is enabled, but "saved" lists of users exist\n',
                                    'Please, remove '+INIPREFIX+'*'+save_postfix)
            sys.exit(1)
        else:
            if raise_exception:
                raise CageFSException
            # This message is parsed in CageFS plugins for control panels
            print('CageFS is disabled.')
            print('Please, run "cagefsctl --enable-cagefs" to enable CageFS.')
            sys.exit(1)


def print_user_mode():
    check_save_dir()
    mode = get_user_mode()
    print('Mode:', mode)


def set_user_mode(enable_all = True):
    check_save_dir()

    # clear permissions (delete both enabled_dir and disabled_dir)
    try:
        shutil.rmtree(enabled_dir, False)
    except (OSError, IOError, shutil.Error):
        pass
    try:
        shutil.rmtree(disabled_dir, False)
    except (OSError, IOError, shutil.Error):
        pass

    if enable_all:
        # enable all users except specified in disabled_dir
        try:
            mod_makedirs(disabled_dir, 0o751)
        except OSError:
            pass
    else:
        # disable all users except specified in enabled_dir
        try:
            mod_makedirs(enabled_dir, 0o751)
        except OSError:
            pass

    # Exclude system users
    check_exclude()

    print_user_mode()


exclude_user_list_cache = {}

def get_exclude_user_list(exclude_path = EXCLUDE_PATH):
    global exclude_user_list_cache
    if exclude_path in exclude_user_list_cache:
        return exclude_user_list_cache[exclude_path]
    user_list = []
    if os.path.isdir(exclude_path):
        for exclude_file_path in os.listdir(exclude_path):
            path = os.path.join(exclude_path, exclude_file_path)
            if os.path.isfile(path) and exclude_file_path != '.htaccess':
                try:
                    f = open(path, "r")
                    for line in f.readlines():
                        line = line.rstrip()
                        if line == '':
                            continue
                        user_list.append(line)
                    f.close()
                except IOError:
                    secureio.print_error("reading", exclude_file_path)
    exclude_user_list_cache[exclude_path] = user_list
    return user_list


def filter_users(user_list):
    return list(set(user_list) - set(get_exclude_user_list()))


def get_user_prefix(username) -> str:
    base = 100
    try:
        uid = secureio.clpwd.get_uid(username)
    except ClPwd.NoSuchUserException:
        secureio.print_error('user %s not found' % username)
        sys.exit(1)
    b = uid % base
    prefix = "%02d" % b
    return prefix


def toggle_file(_dir, username, enable, prefix=None):
    if prefix == None:
        prefix = get_user_prefix(username)
    fname = '/'+prefix+'/'+username
    if enable:
        try:
            os.remove(_dir + fname)
        except (IOError, OSError):
            pass

        remove_htaccess(_dir + '/'+prefix)

        try:
            os.rmdir(_dir + '/'+prefix)
        except (IOError, OSError):
            pass


    else:
        try:
            mod_makedirs(_dir+'/'+prefix, 0o751)
        except (IOError, OSError):
            pass


        try:
            open(_dir + fname, 'w').close()
            os.chmod(_dir + fname, 0o644)
        except (IOError, OSError):
            pass


def toggle_user(username, enable):
    check_save_dir()
    mode = get_user_mode()
    check_mode_error(mode)
    try:
        pw = secureio.clpwd.get_pw_by_name(username)
    except ClPwd.NoSuchUserException:
        secureio.print_error('user', username, 'does not exist')
        return
    if pw.pw_uid < MIN_UID:
        secureio.print_error('user', username, 'should have UID >=', MIN_UID)
        return
    username_list = secureio.clpwd.get_names(pw.pw_uid)
    if mode == 'Enable All':
        for tmp_username in username_list:
            toggle_file(disabled_dir, tmp_username, enable)
    elif mode == 'Disable All':
        for tmp_username in username_list:
            toggle_file(enabled_dir, tmp_username, not enable)


def print_users(users, users_per_line = 5, message = 'users'):
    users_count = len(users)
    if users_count != 0:
        users.sort()
        print(users_count, message)
        name = -1
        for name in range(users_count // users_per_line):
            print('\t'.join(users[name*users_per_line:(name+1)*users_per_line]))
        print('\t'.join(users[(name+1)*users_per_line:]))


# Returns list of users from configuration directory
def get_list_of_users_from_config_dir(_dir):
    users = []
    pw = secureio.clpwd.get_user_dict()
    for subdir in os.listdir(_dir):
        if os.path.isdir(os.path.join(_dir, subdir)):
            for _file in os.listdir(os.path.join(_dir, subdir)):
                if (_file in pw) and (subdir == get_user_prefix(_file)):
                    users.append(_file)
    return users


# _dir == configuration directory (users.enabled for DISABLE_ALL mode, and users.disabled for ENABLE_ALL mode)
def get_list_of_users_from_passwd(_dir):
    # get all users from /etc/passwd
    users = set(secureio.clpwd.get_user_dict())
    # exclude users specified in config dir
    exc_users = set(get_list_of_users_from_config_dir(_dir))
    return list(users - exc_users)


# enabled == True  : return list of enabled users
# enabled == False : return list of disabled users
def get_list_of_users(enabled, raise_exception=False):
    check_save_dir(raise_exception)
    mode = get_user_mode()
    check_mode_error(mode, raise_exception)
    if mode == 'Enable All':
        if enabled:
            return get_list_of_users_from_passwd(disabled_dir)
        else:
            return get_list_of_users_from_config_dir(disabled_dir)
    elif mode == 'Disable All':
        if enabled:
            return get_list_of_users_from_config_dir(enabled_dir)
        else:
            return get_list_of_users_from_passwd(enabled_dir)


def get_enabled_users():
    if cagefs_is_enabled():
        return get_list_of_users(True)
    return []


# enabled == True  : list enabled users
# enabled == False : list disabled users
def list_users(enabled):
    check_save_dir()
    mode = get_user_mode()
    check_mode_error(mode)
    if mode == 'Enable All':
        if enabled:
            print_users(get_list_of_users_from_passwd(disabled_dir), 1, 'enabled user(s)')
        else:
            print_users(filter_users(get_list_of_users_from_config_dir(disabled_dir)), 1, 'disabled user(s)')
    elif mode == 'Disable All':
        if enabled:
            print_users(get_list_of_users_from_config_dir(enabled_dir), 1, 'enabled user(s)')
        else:
            print_users(filter_users(get_list_of_users_from_passwd(enabled_dir)), 1, 'disabled user(s)')


def check_exclude(ex_list = None):
    # check if CageFS is disabled
    if save_dir_exists():
        if cagefs_is_enabled():
            # This should never happen
            secureio.print_error('CageFS is enabled, but "saved" lists of users exist\n',
                                    'Please, remove '+INIPREFIX+'*'+save_postfix)
            sys.exit(1)
        else:
            # CageFS is disabled
            return

    mode = get_user_mode()

    # Read "new" exclude list
    if ex_list == None:
        ex_list = get_exclude_user_list()

    # get all users from /etc/passwd
    pw = secureio.clpwd.get_user_dict()

    # Disable users in "new" exclude list
    for username in ex_list:
        if username in pw:
            if mode == 'Enable All':
                toggle_file(disabled_dir, username, False)
            elif mode == 'Disable All':
                toggle_file(enabled_dir, username, True)

    # Read "old" saved copy of exclude list
    old_ex_list = get_exclude_user_list(EXCLUDE_SAVE_PATH)

    # Enable users from "old" list that do not exist in "new" exclude list
    for username in old_ex_list:
        if username not in ex_list and username in pw:
            if mode == 'Enable All':
                toggle_file(disabled_dir, username, True)
            elif mode == 'Disable All':
                toggle_file(enabled_dir, username, False)

    # Save "new" exclude list (with concurrency in mind)
    if os.path.isdir(EXCLUDE_PATH):
        if not os.path.isdir(EXCLUDE_SAVE_PATH):
            try:
                mod_makedirs(EXCLUDE_SAVE_PATH, 0o750)
            except OSError:
                pass
        tmp_dir = None
        try:
            for f in os.listdir(EXCLUDE_SAVE_PATH):
                path = os.path.join(EXCLUDE_SAVE_PATH, f)
                orig_path = os.path.join(EXCLUDE_PATH, f)
                if os.path.isfile(path) and not os.path.isfile(orig_path):
                    try:
                        os.unlink(path)
                    except OSError:
                        pass
            tmp_dir = tempfile.mkdtemp(dir=EXCLUDE_SAVE_PATH)
            for f in os.listdir(EXCLUDE_PATH):
                tmp_path = os.path.join(tmp_dir, f)
                shutil.copy(os.path.join(EXCLUDE_PATH, f), tmp_path)
                os.rename(tmp_path, os.path.join(EXCLUDE_SAVE_PATH, f))
        except (OSError, IOError, shutil.Error):
            secureio.print_error("copying", EXCLUDE_PATH, "to", EXCLUDE_SAVE_PATH)
        finally:
            if tmp_dir:
                shutil.rmtree(tmp_dir, True)


def clean_var_cagefs():
    bdir = '/var/cagefs'
    pw_db = secureio.clpwd.get_user_dict()
    if os.path.isdir(bdir):
        for prefix in os.listdir(bdir):
            if os.path.isdir(os.path.join(bdir, prefix)):
                for username in os.listdir(os.path.join(bdir, prefix)):
                    path = os.path.join(bdir, prefix, username)
                    if os.path.islink(path) or ((not path.endswith('.lock')) and os.path.isfile(path)):
                        try:
                            os.remove(path)
                        except (OSError, IOError):
                            pass
                    elif os.path.isfile(path):
                        username = username[:-len('.lock')]
                        if (username not in pw_db) or (prefix != get_user_prefix(username)):
                            try:
                                os.remove(path)
                            except (OSError, IOError):
                                pass
                    elif os.path.isdir(path):
                        if (username not in pw_db) or (prefix != get_user_prefix(username)):
                            shutil.rmtree(path, True)
                try:
                    os.rmdir(os.path.join(bdir, prefix))
                except (IOError, OSError):
                    pass


def clean_config_dir(_dir):
    ex_list = get_exclude_user_list()
    pw_db = secureio.clpwd.get_user_dict()
    for subdir in os.listdir(_dir):
        if os.path.isdir(os.path.join(_dir, subdir)):
            for _file in os.listdir(os.path.join(_dir, subdir)):
                if (_file not in ex_list) and ( (_file not in pw_db) or (subdir != get_user_prefix(_file)) ):
                    toggle_file(_dir, _file, True, subdir)


# Remove files that correspond to non-existing users or users with UID < MIN_UID
def clean_config_dirs():
    if os.path.isdir(disabled_dir):
        clean_config_dir(disabled_dir)
    if os.path.isdir(enabled_dir):
        clean_config_dir(enabled_dir)


# Returns random string
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))


def migrate_config_dir(_dir):
    temp_dir = _dir + '.' + id_generator()

    try:
        os.rename(_dir, temp_dir)
    except (OSError, IOError):
        secureio.print_error('failed to rename', _dir, 'to', temp_dir)
        sys.exit(1)

    try:
        mod_makedirs(_dir, 0o751)
    except (OSError, IOError):
        secureio.print_error('failed to create', _dir)
        sys.exit(1)

    pw_db = secureio.clpwd.get_user_dict()

    for subdir in os.listdir(temp_dir):
        if os.path.isdir(os.path.join(temp_dir, subdir)):
            for _file in os.listdir(os.path.join(temp_dir, subdir)):
                if _file in pw_db:
                    # create prefix directory and _file for user
                    toggle_file(_dir, _file, False)

    # remove temp dir
    shutil.rmtree(temp_dir, True)


# Returns True if new prefixes are used
def new_prefixes_are_used():
    pw_db = secureio.clpwd.get_user_dict()
    for _dir in [enabled_dir, disabled_dir, enabled_dir+save_postfix, disabled_dir+save_postfix]:
        if os.path.isdir(_dir):
            for subdir in os.listdir(_dir):
                if os.path.isdir(os.path.join(_dir, subdir)):
                    for _file in os.listdir(os.path.join(_dir, subdir)):
                        if (_file in pw_db) and (subdir != get_user_prefix(_file)):
                            return False
    return True


def migrate_to_new_prefixes():
    if not new_prefixes_are_used():
        if os.path.isdir(disabled_dir):
            migrate_config_dir(disabled_dir)
        if os.path.isdir(enabled_dir):
            migrate_config_dir(enabled_dir)
        if os.path.isdir(disabled_dir+save_postfix):
            migrate_config_dir(disabled_dir+save_postfix)
        if os.path.isdir(enabled_dir+save_postfix):
            migrate_config_dir(enabled_dir+save_postfix)


BASEDIRS_FILE = '/etc/cagefs/cagefs.base.home.dirs'
basedirs = None


def mount_base_dir_enabled():
    global basedirs
    if os.path.isfile(BASEDIRS_FILE):
        if basedirs == None:
            basedirs = cagefslib.read_file(BASEDIRS_FILE)
        if basedirs[0].rstrip() == "mount_basedir=1":
            return True
    return False


def get_base_dir(homepath):
    for reg_exp in basedirs:
        reg_exp = reg_exp.rstrip()
        if reg_exp == "mount_basedir=1" or reg_exp == "mount_basedir=0":
            continue
        m = re.search(reg_exp, homepath)
        if m != None:
            return m.group()
    return ''


def get_mounted_users_old(fix_permissions=False):
    """
    Returns list of users which are currently mounted in CageFS.
    Used when /proc/sys/fs/may_detach_mounts set to 0 (disabled) or does not exist
    :param fix_permissions: when True == fix permissions of directories (mount points) for users' home directories inside /var/cagefs
    :type fix_permissions: bool
    """
    base_dir_flag = mount_base_dir_enabled()
    pw_db = secureio.clpwd.get_user_dict()
    res = set()
    # scan directories of users in /var/cagefs
    if os.path.isdir(BASEDIR):
        for prefix in os.listdir(BASEDIR):
            if os.path.isdir(os.path.join(BASEDIR, prefix)):
                for user in os.listdir(os.path.join(BASEDIR, prefix)):
                    try:
                        pw = pw_db[user]
                    except KeyError:
                        # user does not exist
                        continue

                    if base_dir_flag:
                        base_dir = get_base_dir(pw.pw_dir)
                        if base_dir == '':
                            continue
                        mount_point_path = os.path.join(BASEDIR, prefix, user) + base_dir
                    else:
                        mount_point_path = os.path.join(BASEDIR, prefix, user) + pw.pw_dir

                    if os.path.isdir(mount_point_path):
                        if fix_permissions:
                            try:
                                os.chmod(mount_point_path, 0o755)
                            except OSError as e:
                                print('Error: failed to set permissions to directory', mount_point_path, ':', str(e))
                        else:
                            remove_htaccess(mount_point_path)
                            try:
                                os.rmdir(mount_point_path)
                            except OSError:
                                # mount point is busy - user is mounted
                                for user2 in cagefslib.get_all_users_with_uid(pw.pw_uid):
                                    res.add(user2)
                                continue
                            # recreate mount point
                            try:
                                umask_saved = os.umask(0)
                                os.mkdir(mount_point_path, 0o755)
                                os.umask(umask_saved)
                            except OSError:
                                pass

    return list(res)


def get_mounted_users_new():
    """
    Returns list of users which are currently mounted in CageFS.
    Used when /proc/sys/fs/may_detach_mounts set to 1 (enabled)

    """
    users = get_enabled_users()
    pw_db = secureio.clpwd.get_user_dict()
    mounted_users = []
    for user in users:
        res = subprocess.run(['/bin/lve_suwrapper', '-meck', str(pw_db[user].pw_uid),
                              '/usr/bin/stat', '/var/.cagefs'], capture_output=True, cwd='/')
        if res.returncode == 0:
            mounted_users.append(user)
    return mounted_users


def get_mounted_users(fix_permissions=False):
    """
    Returns list of users which are currently mounted in CageFS.
    """
    if is_may_detach_mounts_enabled():
        return get_mounted_users_new()
    return get_mounted_users_old(fix_permissions)


def get_logged_in_users():
    try:
        pl = subprocess.Popen(['/bin/ps','aux'], stdout=subprocess.PIPE, text=True).communicate()[0]
    except OSError:
        secureio.print_error('failed to run', 'ps', "aux")
        sys.exit(1)
    pattern = re.compile(r'sshd:[a-z_][a-z0-9_-]*[$]?@pts',re.IGNORECASE)
    lst = []
    for i in  pl.split('\n'):
        line = i.split()
        #line format
        #USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
        #0          1    2     3      4     5   6        7     8      9    10+
        if len(line) > 0:
            command = ''.join(line[10:])
            # looking where command start with sshd:USERNAME@pts
            if pattern.match(command):
                try:
                #check if name so long, and ps show UID
                    uid = int(line[0])
                    lst.extend(secureio.clpwd.get_names(uid))
                except ValueError:
                #nope.. ps show username
                    lst.append(line[0])
                    pass
                except ClPwd.NoSuchUserException:
                    secureio.print_error('Can`t get user name for UID ',uid )
    sshd_set = set(lst)
    mounted_set = set(get_mounted_users())
    result_set = sshd_set & mounted_set
    return list(result_set)


def get_lve_list() -> List[int]:
    """
    Return list of id's for existing LVEs
    """
    lve_list = []
    proc_lve = ProcLve()
    for lve_id in proc_lve.lve_id_list():
        lve_list.append(lve_id)
    if proc_lve.resellers_supported():
        for lvp_id in proc_lve.lvp_id_list():
            for lve_id in proc_lve.lve_id_list(lvp_id=lvp_id):
                lve_list.append(lve_id)
    return lve_list


# Returns True if error has occured
def umount_list(_list):
    _list.sort()
    _list.reverse()
    error = False
    for line in _list:
        if len(line) > 0 and line[0] == '/':
            line = line.rstrip()
            try:
                # run the "umount" command and suppress it's output
                p = subprocess.Popen([UMOUNT, "-l", line],\
                                                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                p.communicate()
                # check return code of the child
                if p.returncode != 0:
                    error = True
            except OSError:
                secureio.print_error('failed to run', UMOUNT, "-l", line)
                error = True

    return error


# Returns True if error has occured
def umount_dir(path):
    try:
        ret = subprocess.call([UMOUNT, "-l", SKELETON+path])
        if ret != 0:
            secureio.print_error("failed to unmount", SKELETON+path)
            return True
    except OSError:
        secureio.print_error('failed to run', UMOUNT, "-l", SKELETON+path)
        return True

    return False


def apply_all():
    """
    Run lvectl apply all
    Returns True if error has occured
    """
    ATTEMPTS = 3
    for _ in range(ATTEMPTS):
        error = False
        try:
            # run the command and suppress it's output
            p = subprocess.Popen([LVECTL, "apply", "all", "--force"],\
                                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            p.communicate()
            # check return code of the child
            if p.returncode != 0:
                error = True
            else:
                break
        except OSError:
            secureio.print_error('failed to run', LVECTL, "apply all")
            error = True

    if error:
        secureio.print_error(LVECTL, "apply all failed")

    return error


def destroy_all():
    """
    Destroy all LVEs
    Returns True if error has occured
    """
    ATTEMPTS = 3
    for _ in range(ATTEMPTS):
        error = False
        try:
            # run the command and suppress it's output
            p = subprocess.Popen([LVECTL, "destroy", "all", "--force"],\
                                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            p.communicate()
            # check return code of the child
            if p.returncode != 0:
                error = True
            else:
                break
        except OSError:
            secureio.print_error('failed to run', LVECTL, "destroy all")
            error = True

    if error:
        secureio.print_error(LVECTL, "destroy all failed")

    return error


def destroy_lve(uids):
    """
    Run lvectl destroy for specified uids
    :param uids: list of integers (UIDs)
    :type uids: iterable
    Returns True if error has occured
    """
    error = False

    # create input string for subprocess (lvectl)
    s = ''
    for uid in uids:
        s = s + str(uid) + '\n'

    try:
        # run the command and suppress it's output
        p = subprocess.Popen([LVECTL, "destroy-many"],
                             stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             text=True)
        # send input string to suprocess
        p.communicate(s)
    except OSError:
        secureio.print_error('failed to run', LVECTL, "destroy-many")
        error = True

    return error


def apply_lve(uids):
    """
    Run lvectl apply for specified uids
    :param uids: list of integers (UIDs)
    :type uids: iterable
    Returns True if error has occured
    """
    error = False

    # create input string for subprocess (lvectl)
    s = ''
    for uid in uids:
        s = s + str(uid) + '\n'

    try:
        # run the command and suppress it's output
        p = subprocess.Popen([LVECTL, "apply-many"],
                             stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             text=True)
        # send input string to suprocess
        p.communicate(s)
    except OSError:
        secureio.print_error('failed to run', LVECTL, "apply-many")
        error = True

    return error


def get_uids(users):
    pw_db = secureio.clpwd.get_user_dict()
    uids = []
    for user in users:
        try:
            uids.append(pw_db[user].pw_uid)
        except KeyError:
            continue
    return uids


def remove_duplicates(_list):
    res = []
    for i in _list:
        if i not in res:
            res.append(i)
    return res


def remount(users):
    """
    Remount list of users. Skeleton should be mounted/unmounted before call of this function
    Returns True if error has occured
    :param users: list of usernames
    :type users: iterable
    """
    error = False

    if is_running_without_lve():
        if delete_namespaces(users):
            error = True
        if create_namespaces(users, do_mount_skel=False):
            error = True
        return error

    # Get UIDs of users
    uids = get_uids(users)

    uids = remove_duplicates(uids)

    if destroy_lve(uids):
        error = True

    time.sleep(1)

    if apply_lve(uids):
        error = True

    return error


# Remount all users. Skeleton should be mounted/unmounted before call of this function
# Returns True if error has occured
def remount_all(enabled_users_only = False):
    error = False

    if enabled_users_only:
        # Get list of enabled users
        enabled_users = get_list_of_users(True)

        if remount(enabled_users):
            error = True

        return error

    if is_running_without_lve():
        if delete_namespaces():
            error = True
        if create_namespaces(do_mount_skel=False):
            error = True
        return error

    if destroy_all():
        error = True

    time.sleep(1)

    if apply_all():
        error = True

    return error


def files_exist(_list):
    for _file in _list:
        if not os.path.exists(SKELETON+_file):
            return False
    return True


# Returns True if there is any mounted directory in cagefs-skeleton
def skeleton_is_mounted(skeleton=None):
    if skeleton is None:
        skeleton = SKELETON
    mounts = open("/proc/mounts", "r")
    while True:
        line = mounts.readline()
        if line == '':
            break
        if line.find(skeleton+'/') != -1:
            mounts.close()
            return True
    mounts.close()
    return False


def cagefs_fuse_is_mounted():
    if etcfs_is_disabled():
        return files_exist(['/var/log/messages'])
    else:
        return files_exist(['/etc/passwd', '/var/log/messages'])


# Runs "service cagefs-fuse" with specified command ("start", "restart" or "stop")
# Returns True if error has occured
def cagefs_fuse(command):
    ATTEMPTS = 3
    for _ in range(ATTEMPTS):
        error = False
        try:
            # run the command and suppress it's output
            p = subprocess.Popen(["/sbin/service", "cagefs-fuse", command],\
                                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            p.communicate()
            # check return code of the child
            if p.returncode != 0:
                error = True
        except OSError:
            secureio.print_error('failed to run "service cagefs-fuse '+command+'"')
            error = True

        if command == 'start':
            if cagefs_fuse_is_mounted():
                error = False
                break
            else:
                command = 'restart'
                error = True
        elif command == 'restart':
            if cagefs_fuse_is_mounted():
                error = False
                break
            else:
                error = True
        elif command == 'stop':
            if not cagefs_fuse_is_mounted():
                error = False
                break
            else:
                error = True
        else:
            break

    if error:
        secureio.print_error("executing", '"service cagefs-fuse', command+'"')

    return error

def proxyexecd_is_socket():
    return os.path.lexists(os.path.join(PROXYEXEC_SOCKET_DIR, 'socket'))


def cagefs_proxyexecd(command):
    error = False
    try:
        # run the command and suppress it's output
        p = subprocess.Popen(["/sbin/service", "proxyexecd", command],\
                                                                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        p.communicate()
        if p.returncode != 0:
            error = True
        if command == 'start' or command == 'restart':
            p = subprocess.Popen(["/sbin/service", "proxyexecd", "status"],\
                                                                    stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
            p.communicate()
            if p.returncode != 0:
                error = True
    except OSError:
        secureio.print_error('failed to run "service proxyexecd '+command+'"')
        error = True

    if error:
        secureio.print_error("executing", '"service proxyexecd', command+'"')

    return error


def create_mount_points(_list, mode = 0o755):
    umask_saved = os.umask(0)
    for path in _list:
        if os.path.islink(SKELETON+path):
            try:
                os.unlink(SKELETON+path)
            except (IOError, OSError):
                pass
        if not os.path.isdir(SKELETON+path):
            try:
                mod_makedirs(SKELETON+path, mode)
            except (IOError, OSError):
                pass
    os.umask(umask_saved)


def check_mp_file():
    if not os.path.isfile(ETC_MPFILE):
        if not SILENT:
            secureio.print_error('file', ETC_MPFILE, 'not found\n','Please, run\n',sys.argv[0], '--create-mp')
        sys.exit(1)
    mp_file = cagefslib.read_file(ETC_MPFILE)
    if add_new_line(mp_file):
        cagefslib.write_file(ETC_MPFILE, mp_file)


def remove_service_lockfile():
    try:
        os.remove(SERVICE_CAGEFS_LOCK)
    except (IOError, OSError):
        pass


def create_service_lockfile():
    try:
        open(SERVICE_CAGEFS_LOCK, 'w').close()
    except (IOError, OSError):
        pass


# Returns True if error has occured
def umount_skeleton(save_mounts = True, all_cagefs_mounts = False, current_namespace_only=False, all_namespaces=False):
    def unmount():
        error = True
        for _ in range(10):
            mounts = cagefslib.get_mounted_dirs(all_cagefs_mounts)
            if not mounts:
                error = False
                break
            umount_list(mounts)
        umount_list([SKELETON])
        return error

    if not current_namespace_only:
        remove_service_lockfile()
    error = unmount()
    if save_mounts:
        lvectl_start()

    # CAG-749: unmount CageFS mounts in all mount namespaces (resolve conflict with systemd)
    if os.path.isfile('/usr/bin/systemctl'):
        if current_namespace_only:
            if error:
                subprocess.run('/bin/umount -l /usr &>/dev/null', shell=True, executable='/bin/bash')
                error = unmount() or error
        elif all_namespaces:
            destroy_all()
            time.sleep(1)
            for pid in Execute('/bin/ps --no-headers -xao pid').split():
                if pid:
                    subprocess.run("/usr/bin/nsenter -m -t " + pid +
                                   " /bin/bash -c 'if /bin/grep -q cagefs /proc/mounts; then" +
                                   " /usr/sbin/cagefsctl --unmount-cur-ns; fi' &>/dev/null",
                                   shell=True, executable='/bin/bash')

    return error


def unlock(lockfile, lockname = LOCKNAME):
    try:
        fcntl.lockf(lockfile, fcntl.LOCK_UN)
    except (IOError, OSError):
        secureio.print_error('failed to unlock', lockname)

    try:
        lockfile.close()
    except (IOError, OSError):
        pass

    try:
        os.unlink(lockname)
    except (IOError, OSError):
        pass


def Execute(command):
    proc = subprocess.Popen(command,
                            shell=True,
                            executable='/bin/bash',
                            stdout=subprocess.PIPE,
                            text=True,
                            bufsize=-1)
    return proc.communicate()[0]


def get_parents(process: psutil.Process):
    """
    Helper to get all parents list
    """
    parents = []
    process = process.parent()
    while process is not None:
        parents.append(process)
        process = process.parent()
    return parents


def save_processes(file_like_io):
    """
    Saves info about parent processes to lock file
    """
    try:
        current_process = psutil.Process(os.getpid())
        cmd_line = current_process.cmdline()
        parents = get_parents(current_process)
    except Exception:
        parents = []
        cmd_line = []
    file_like_io.write(f'Command line: {" ".join(cmd_line)}\n')
    for process in parents:
        file_like_io.write(f'pid: "{process.pid}", name: "{process.name()}", command line "{process.cmdline()}"\n')
    file_like_io.flush()


def print_lock_data(lockname):
    """
    Prints to stdout info from lockfile
    """
    if os.path.exists(lockname):
        with open(lockname, 'r') as f:
            lock_content = f.readlines()
        secureio.print_error('Currently running cagefsctl process info:\n{}'.format('\n'.join(lock_content)))


# Acquire lock
def acquire_lock(lockname=LOCKNAME, wait=False, quiet=False):
    try:
        if not os.path.exists(lockname):
            open(lockname, 'w').close()
        lockfile = open(lockname, 'r+')
        if wait:
            print('Acquiring lock... Please wait... ')
        if not wait:
            fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
        fcntl.lockf(lockfile, fcntl.LOCK_EX)
        if os.path.exists(DEBUG_CAGEFS_MARKER):
            lockfile.truncate(0)
            save_processes(lockfile)
        if wait:
            print('Lock acquired')
        return lockfile
    except (IOError, OSError):
        if not quiet:
            if (not wait) and len(Execute('ps aux | grep cagefsctl').split('\n')) > 1:
                secureio.print_error('cagefsctl is already running. please try again later.')
                if os.path.exists(DEBUG_CAGEFS_MARKER):
                    secureio.print_error('current cagefsctl process information')
                    save_processes(sys.stdout)
                    print_lock_data(lockname)
            else:
                secureio.print_error('failed to acquire lock file', lockname)
        sys.exit(1)


def mount_dir(line, read_only = False, ignore_errors = False):
    if len(line) > 0 and line[0] == '/':
        line = line.rstrip()
        if not os.path.isdir(line):
            secureio.print_error(ETC_MPFILE, "file contains incorrect path -", line, "is NOT a directory or does NOT exist")
            if ignore_errors:
                return
            sys.exit(1)
        create_mount_points([line])
        ret = subprocess.call([MOUNT, "-n", "-o", "nosuid", "--rbind", line, SKELETON+line])
        if read_only:
            if ret == 0:
                ret = subprocess.call([MOUNT, "-n", "-o", "remount,ro,nosuid,bind", line, SKELETON+line])
        else:
            if ret == 0:
                if line == '/dev/shm':
                    # CAG-812: mount /dev/shm with noexec option in CageFS
                    ret = subprocess.call([MOUNT, "-n", "-o", "remount,nosuid,noexec,nodev,bind", line, SKELETON+line])
                else:
                    ret = subprocess.call([MOUNT, "-n", "-o", "remount,nosuid,bind", line, SKELETON+line])
        if ret != 0:
            secureio.print_error("failed to mount", line)
            sys.exit(1)


def print_cpanel_home_warning():
    secureio.logging('Please ensure that the following option in cPanel/WHM is set to blank value (not default "home"):', SILENT, 1)
    secureio.logging('WHM -> Server Configuration -> Basic cPanel/WHM Setup -> Basic Config -> Additional home directories', SILENT, 1)
    secureio.logging('When this option is set to "home", cPanel can create home directories in incorrect places.', SILENT, 1)


class MountpointType(Enum):
    COMMON = '/'
    PERSONAL = '@'
    SPLITTED_BY_USERNAME = '%'
    SPLITTED_BY_UID = '*'
    READ_ONLY = '!'


class MountpointConfig:
    # mpconfig_cache = {
    #   'path_to_mp_file1': {
    #       MountpointType.COMMON.name: [ ... list of mount points ... ],
    #       MountpointType.PERSONAL.name: [ ... ],
    #       MountpointType.SPLITTED_BY_USERNAME.name: [ ... ],
    #       ...
    #   }
    #   'path_to_mp_file2': {
    #       ...
    #   }
    # }
    mpconfig_cache: Dict[str, Dict[str, List[str]]] = {}

    def __init__(self,
                 path: str = ETC_MPFILE,
                 skip_errors: bool = False,
                 skip_cpanel_check: bool = False,
                 ignore_cache: bool = False):
        self.path = path
        self.skip_errors = skip_errors
        self.skip_cpanel_check = skip_cpanel_check
        self.ignore_cache = ignore_cache
        self.data = self._load()

    def _load(self) -> Dict[str, List[str]]:
        """
        Load a list of mount points from the config file.
        Use cached value if exists unless special option specified.
        """
        if self.ignore_cache or self.path not in self.mpconfig_cache:
            self.mpconfig_cache[self.path] = self._read_config()

        return copy.deepcopy(self.mpconfig_cache[self.path])

    def _read_config(self) -> Dict[str, List[str]]:
        """
        Read the config file and construct
        a complete list of mount points.
        """
        mounts = defaultdict(list)
        try:
            with open(self.path) as f:
                for line in f:
                    self._process_mount_line(line, mounts)
        except OSError:
            if self.skip_errors:
                return mounts
            secureio.print_error('failed to read', self.path)
            sys.exit(1)

        if not self.skip_cpanel_check and is_cpanel():
            self._process_cpanel_mounts(mounts)

        self._process_proxyexec_socket_mounts(mounts)
        return mounts

    def _process_mount_line(self, line: str, mounts: Dict[str, List[str]]) -> None:
        """
        Process the line specifying a mount point.
        Determine whether the line belongs to
        any of predefined types and process it accordingly.
        """
        if line.startswith('#'):  # skip comments
            return

        line = line.rstrip()

        # Process each line based on its mount point type
        for mount_type in MountpointType:
            if line.startswith(mount_type.value):
                self._process_line(line, mount_type, mounts)
                break

    def _process_line(self,
                      line: str,
                      mount_type: MountpointType,
                      mounts: Dict[str, List[str]]) -> None:
        # For all mounts other than common cut the first symbol,
        # for personal mounts also cut the part after the comma
        start = 0 if mount_type == MountpointType.COMMON else 1
        end = line.rfind(',') if mount_type == MountpointType.PERSONAL else -1
        path = line[start:end] if end != -1 else line[start:]
        path = path.rstrip()

        if self._is_invalid_mount_point(path):
            if self.skip_errors:
                return
            secureio.print_error('Invalid mount point', line, 'in file', self.path)
            sys.exit(1)

        # For some reason, we add common mounts with '\n' at the end
        if mount_type == MountpointType.COMMON:
            mounts[mount_type.name].append(path + '\n')
        else:
            mounts[mount_type.name].append(path)
            # Read only mounts are also being added to common mounts
            if mount_type == MountpointType.READ_ONLY:
                mounts[MountpointType.COMMON.name].append(path + '\n')

    def _is_invalid_mount_point(self, path: str) -> bool:
        """
        Check if a given path is an invalid mount path.
        """
        return path == '/' \
            or not path.startswith('/') \
            or '/../' in path \
            or path.endswith('/..')

    def _process_cpanel_mounts(self, mounts: Dict[str, List[str]]) -> None:
        """
        Check invalid paths for cPanel.
        """
        common_mounts = mounts[MountpointType.COMMON.name]
        for line in common_mounts:
            line = line.rstrip()
            if 'home' in line and invalid_homes_exist():
                secureio.logging(f'Warning: file {self.path} contains line "{line}"', SILENT, 1)
                print_cpanel_home_warning()
                break

    def _process_proxyexec_socket_mounts(self, mounts: Dict[str, List[str]]) -> None:
        """
        Add correct path to the proxyexec socket file.
        """
        common_mounts = mounts[MountpointType.COMMON.name]
        proxyexec_socket_dir_old_line = f'{PROXYEXEC_SOCKET_DIR_OLD}\n'
        proxyexec_socket_dir_line = f'{PROXYEXEC_SOCKET_DIR}\n'

        if proxyexec_socket_dir_old_line in common_mounts:
            common_mounts.remove(proxyexec_socket_dir_old_line)
        if proxyexec_socket_dir_line not in common_mounts:
            common_mounts.append(proxyexec_socket_dir_line)

    @property
    def all_mounts(self) -> Dict[str, List[str]]:
        return self.data

    @property
    def common_mounts(self) -> List[str]:
        return self.data[MountpointType.COMMON.name]

    @property
    def personal_mounts(self):
        return self.data[MountpointType.PERSONAL.name]

    @property
    def splitted_by_username_mounts(self):
        return self.data[MountpointType.SPLITTED_BY_USERNAME.name]

    @property
    def splitted_by_uid_mounts(self):
        return self.data[MountpointType.SPLITTED_BY_UID.name]

    @property
    def read_only_mounts(self):
        return self.data[MountpointType.READ_ONLY.name]


# Save mounts in default VE
def lvectl_start():
    Execute(LVECTL+' start > /dev/null 2>&1')


def mount_should_be_readonly(path, read_only_mounts):
    """
    Return True when path is included in one of the read-only paths
    :param path: mount path to check
    :type path: string
    :param read_only_mounts: list of read-only mounts from cagefs.mp file
    :type read_only_mounts: list
    """
    if path.startswith('/opt/cpanel/ea-php'):
        return True
    path = cagefslib.addslash(path)
    for mount in read_only_mounts:
        mount = cagefslib.addslash(mount)
        if path.startswith(mount):
            return True
    return False


def unsafe_mounts_exist():
    """
    Search CageFS for mounts that do not have 'nosuid' option
    Also search for read-write mounts that should be read-only
    Return True when found, False otherwise
    For details see CAG-526, CAG-634
    """
    no_suid_dirs = cagefslib.get_mounted_dirs(without_nosuid = True)
    no_suid_dirs = list(filter(lambda x: '/proc/sys/fs/binfmt_misc' not in x, no_suid_dirs))
    if no_suid_dirs:
        return True
    mp_config = MountpointConfig()
    rw_mounts = cagefslib.get_mounted_dirs(rw_mounts_only = True)
    for mount in rw_mounts:
        path = cagefslib.strip_path(mount)
        if mount_should_be_readonly(path, mp_config.read_only_mounts):
            return True
    return False


def remount_unsafe_mounts(read_only_mounts):
    """
    Remount all CageFS "unsafe" mounts, so that they become "safe".
    Make all mounts "nosuid", and make some mounts "read-only" (when needed)
    For details see CAG-526, CAG-634
    """
    def remount_dir(old, new, read_only=False):
        if read_only:
            ret = subprocess.call([MOUNT, "-n", "-o", "remount,ro,nosuid,bind", '/usr/share/cagefs/not-existing-directory'+old, new])
        else:
            ret = subprocess.call([MOUNT, "-n", "-o", "remount,nosuid,bind", '/usr/share/cagefs/not-existing-directory'+old, new])
        if ret != 0:
            secureio.print_error("failed to mount", old)

    remount_dir(SKELETON, SKELETON)
    wo_nosuid = set(cagefslib.get_mounted_dirs(without_nosuid = True))
    for path_new in wo_nosuid:
        path_old = cagefslib.strip_path(path_new)
        remount_dir(path_old, path_new, read_only=mount_should_be_readonly(path_old, read_only_mounts))
    rw_mounts = set(cagefslib.get_mounted_dirs(rw_mounts_only = True)) - wo_nosuid
    for path_new in rw_mounts:
        path_old = cagefslib.strip_path(path_new)
        if mount_should_be_readonly(path_old, read_only_mounts):
            remount_dir(path_old, path_new, read_only=True)


# Personal (private) mount points for user
MOUNT_POINTS = [
        '/etc',
        '/var/log',
        '/var/run/screen',
        cagefslib.VAR_RUN_CAGEFS,
        '/var/spool/cron',
        '/var/cache/php-eaccelerator',
        '/var/.cagefs'
]


def read_symlink(path):
    """
    Return value of symlink or None when error occurs
    :param path: path to symlink
    :type path: string
    """
    try:
        return os.readlink(path)
    except OSError:
        pass
    return None


def mount_file(path, do_mount=False, read_only=False):
    """
    Mount one separate file to CageFS using hardlink & mount
    :param path: path to file
    :type path: string
    :param do_mount: when True mount directory with hardlink to CageFS
    :type do_mount: bool
    :param read_only: when True mount read-only, read-write otherwise
    :type read_only: bool
    """
    if not os.path.isfile(path) and not is_socket_file(path):
        return
    path = os.path.realpath(path)
    skel_path = SKELETON + path
    filename = os.path.basename(path)
    dir_path = path + '.cagefs'
    hardlink_path = os.path.join(dir_path, filename)
    cagefslib.make_dir(dir_path, 0o755, allow_symlink=False)
    if not os.path.lexists(hardlink_path) or not os.path.samefile(path, hardlink_path):
        cagefslib.unlink(hardlink_path)
        try:
            os.link(path, hardlink_path)
        except OSError as e:
            if not os.path.isfile(hardlink_path) or not os.path.samefile(path, hardlink_path):
                secureio.logging('Error: failed to create hardlink ' + hardlink_path + ' to ' + path + ' : ' + str(e), SILENT, 1)
                return
    cagefslib.make_dir(SKELETON + dir_path, 0o755, allow_symlink=False, update_perm=False)
    spath = read_symlink(skel_path)
    if spath != hardlink_path:
        cagefslib.unlink(skel_path)
        try:
            os.symlink(hardlink_path, skel_path)
        except OSError as e:
            spath = read_symlink(skel_path)
            if spath != hardlink_path:
                secureio.logging('Error: failed to create symlink ' + skel_path + ' to ' + hardlink_path + ' : ' + str(e), SILENT, 1)
                return
    if do_mount:
        mount_dir(dir_path, read_only)


def _mount_systemd_journal_socket(do_mount: bool) -> None:
    """
    Mount socket of systemd-journal into CageFS
    :param do_mount: when True mount directory with hardlink to CageFS
    """
    # Check that /dev/log is really symlink and
    # real file is socket of systemd-journal
    if not is_new_syslog_socket_used():
        return

    mount_file(SYSTEMD_JOURNAL_SOCKET, do_mount=do_mount)

    # Directory `SKELETON/dev dir` may be absent
    # at the moment of CageFS initialization
    skeleton_dev_dir = os.path.join(
        SKELETON,
        'dev',
    )
    if not os.path.exists(skeleton_dev_dir):
        cagefslib.make_dir(
            path=skeleton_dev_dir,
            perm=0o755,
            allow_symlink=False,
            update_perm=False,
        )
    # Create symlink SKELETON/dev/log -> socket of systemd-journal
    # like as in real fs
    create_symlink(
        SYSTEMD_JOURNAL_SOCKET,
        os.path.join(
            skeleton_dev_dir,
            'log',
        ),
    )


def mount_skeleton(remount_users = False):
    """
    Function remounts skeleton and all users
    !!WARNING!!: part of this logic is duplicated in jail.c from kmoc-lve project
    :param remount_users: when True, destroy&create LVE&namespaces for all users
    :type remount_users: bool
    """
    # Ensure that mp-file exists
    check_mp_file()

    Execute('/bin/mount --make-rprivate / >/dev/null 2>&1')

    # Create mount points in skeleton
    create_mount_points(MOUNT_POINTS)

    # --remount_all option is used or cagefs-fuse is not running ?
    # if remount_users or (not cagefs_fuse_is_mounted()):
        # Restart cagefs-fuse service
    #       if cagefs_fuse('restart'):
    #               sys.exit(1)

    if remount_users or (not proxyexecd_is_socket()):
        # Restart proxyexecd service
        if cagefs_proxyexecd('restart'):
            sys.exit(1)

    # Create mount point for tmp directory
    create_mount_points(['/tmp'])

    umount_skeleton(save_mounts = False)

    ret = subprocess.call([MOUNT, "-n", "-o", "nosuid", "--rbind", SKELETON, SKELETON])
    if ret != 0:
        secureio.print_error("failed to mount", SKELETON)

    # Read mp-file
    mp_config = MountpointConfig()
    read_only_mounts = mp_config.read_only_mounts
    personal_mounts = mp_config.personal_mounts
    cagefslib.mounts = mp_config.common_mounts

    create_mount_points(personal_mounts)

    umask_saved = os.umask(0)

    # Mount directories specified in mp-file
    # we sort mounts because of CAG-709 (cagefs mounts should not break systemd services with directives like ProtectSystem=full)
    for line in sorted(cagefslib.mounts):
        line = line.rstrip()
        if line != '/proc' and line != '/tmp' and not line.startswith('/tmp/'):
            mount_dir(line, read_only = (line in read_only_mounts), ignore_errors = True)

    # Mount empty dir over user-defined dirs + default /opt/suphp/sbin see CAG-999
    try:
        emptied_dirs_path = "/etc/cagefs/empty.dirs"
        for filename in os.listdir(emptied_dirs_path):
            emptied_config = os.path.join(emptied_dirs_path, filename)
            if os.path.isfile(emptied_config):
                with open(emptied_config, "r") as emptied_dirs_file:
                    for emptied_dir in emptied_dirs_file:
                        mount_empty_dir(emptied_dir.rstrip())
    except IOError as e:
        secureio.print_error("Error while reading file.", e)

    setup_cpanel_multiphp(do_mount=True)

    # LU-640: mount license file to CageFS (needed by cloudlinux-selector to check license)
    mount_file(LICENSE_TIMESTAMP_FILE, do_mount=True, read_only=True)

    _mount_systemd_journal_socket(do_mount=True)

    # Mount /proc directory last
    mount_dir('/proc')

    remount_unsafe_mounts(read_only_mounts)

    # Ensure that mount points exist after mounting of skeleton
    create_mount_points(MOUNT_POINTS)
    create_mount_points(personal_mounts)

    os.umask(umask_saved)

    # Save mounts in default VE
    lvectl_start()

    if remount_users:
        # Exclude system users BEFORE remounting skeleton
        check_exclude()
        # Remount all users
        remount_all(enabled_users_only = False)
        remove_unused_mount_points()
        remove_remount_flag()

    create_service_lockfile()


def verify_paths(paths):
    for path in paths:
        path2 = os.path.realpath(path)
        path2 = path2 + '/'
        if path2.startswith(SKELETON+'/'):
            secureio.print_error("path", path, "is incorrect")
            path2 = cagefslib.stripslash(path2)
            if path2 != path:
                secureio.print_error("(it refers to", path2, ")")
            sys.exit(1)


class cagefs_init(object):

    def __init__(self):
        self.didfiles = []
        self.didsections = []
        self.diddevices = []
        self.didusers = []
        self.didgroups = []


    def update_paths(self, config, chroot, paths, try_glob = 0):
        if paths:
            verify_paths(paths)
            self.didfiles = cagefslib.copy_binaries_and_libs(chroot, paths, config['force'], config['verbose'], check_libs=1,\
                    try_hardlink=config['hardlink'], retain_owner=1, try_glob_matching=try_glob, handledfiles=self.didfiles, update=config['update'])


    def update_alt_php_libs(self, config, chroot):
        paths = cagefslib.get_alt_php_libs()
        self.update_paths(config, chroot, paths)


    def handle_cfg_section(self,config,chroot,cfg,section):
        if(chroot[-1] == '/'):
            chroot = chroot[:-1]

        # first create the chroot jail itself if it does not yet exist
        if (not os.path.exists(chroot)):
            print('Creating jail '+chroot)
            mod_makedirs(chroot, 0o755)
            # if the parent is setuid or setgid that is not covered by the umask set above, so we remove that
            os.chmod(chroot, 0o755)

        sections = cagefslib.config_get_option_as_list(cfg,section,'includesections')
        for tmp in sections:
            sigterm_check()
            if (tmp not in self.didsections):
                self.handle_cfg_section(config,chroot,cfg,tmp)
                self.didsections.append(tmp)

        #libraries, executables, regularfiles and directories are now all handled as 'paths'
        paths = cagefslib.config_get_option_as_list(cfg,section,'paths')
        # CAG-936: remove invalid path /usr/local/awstats/ that is added by /usr/local/directadmin/scripts/awstats_process.sh script
        if section == 'directadmin':
            try:
                paths.remove('/usr/local/awstats/')
            except ValueError:
                pass
        paths = paths + cagefslib.config_get_option_as_list(cfg,section,'libraries')
        paths = paths + cagefslib.config_get_option_as_list(cfg,section,'executables')
        paths = paths + cagefslib.config_get_option_as_list(cfg,section,'regularfiles')
        paths = paths + cagefslib.config_get_option_as_list(cfg,section,'directories')
        self.update_paths(config, chroot, paths, try_glob = 1)

        paths_w_owner = cagefslib.config_get_option_as_list(cfg,section,'paths_w_owner')
        self.update_paths(config, chroot, paths_w_owner, try_glob = 1)

        emptydirs = cagefslib.config_get_option_as_list(cfg,section,'emptydirs')
        for edir in emptydirs:
            cagefslib.create_parent_path(chroot,edir, config['verbose'], copy_permissions=1, allow_suid=0, copy_ownership=1)
        users = []
        groups = []
        tmplist = cagefslib.config_get_option_as_list(cfg,section,'users')
        for tmp in tmplist:
            if (tmp not in self.didusers):
                users.append(tmp)
        tmplist = cagefslib.config_get_option_as_list(cfg,section,'groups')
        for tmp in tmplist:
            if (tmp not in self.didusers):
                groups.append(tmp)

        cagefslib.init_passwd_and_group(FUSE_DIR, users, groups, config['verbose'])
        cagefslib.init_safe_users_and_groups(FUSE_DIR, users, groups, config['verbose'])
        cagefslib.init_passwd_and_group(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', users, groups, config['verbose'])
        cagefslib.init_shadow(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', users, config['verbose'])

        self.didusers = self.didusers + users
        self.didgroups = self.didusers + groups
        devices = cagefslib.config_get_option_as_list(cfg,section,'devices')
        for tmp in devices:
            if (tmp not in self.diddevices):
                cagefslib.create_parent_path(chroot,os.path.dirname(tmp), config['verbose'], copy_permissions=1, allow_suid=0, copy_ownership=1)
                cagefslib.copy_device(chroot,tmp,config['verbose'])
                self.diddevices.append(tmp)


    def update_etc_paths(self, paths):
        if paths:
            verify_paths(paths)
            cagefslib.copy_to_etc(paths)

    def update_etc_from_section(self, config, cfg, section):
        sections = cagefslib.config_get_option_as_list(cfg,section,'includesections')
        for tmp in sections:
            if (tmp not in self.didsections):
                self.update_etc_from_section(config,cfg,tmp)
                self.didsections.append(tmp)

        #libraries, executables, regularfiles and directories are now all handled as 'paths'
        paths = cagefslib.config_get_option_as_list(cfg,section,'paths')
        paths = paths + cagefslib.config_get_option_as_list(cfg,section,'libraries')
        paths = paths + cagefslib.config_get_option_as_list(cfg,section,'executables')
        paths = paths + cagefslib.config_get_option_as_list(cfg,section,'regularfiles')
        paths = paths + cagefslib.config_get_option_as_list(cfg,section,'directories')

        self.update_etc_paths(paths)

        paths_w_owner = cagefslib.config_get_option_as_list(cfg,section,'paths_w_owner')
        self.update_etc_paths(paths_w_owner)

        users = []
        groups = []
        tmplist = cagefslib.config_get_option_as_list(cfg,section,'users')
        for tmp in tmplist:
            if (tmp not in self.didusers):
                users.append(tmp)
        tmplist = cagefslib.config_get_option_as_list(cfg,section,'groups')
        for tmp in tmplist:
            if (tmp not in self.didusers):
                groups.append(tmp)

        cagefslib.init_passwd_and_group(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', users, groups, config['verbose'])
        cagefslib.init_shadow(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', users, config['verbose'])

        self.didusers = self.didusers + users
        self.didgroups = self.didusers + groups


# Return True if mount points are busy (are used by cagefs-fuse)
def mount_points_busy(_list):
    for path in _list:
        if os.path.isdir(SKELETON+path) and (not os.path.islink(SKELETON+path)):
            try:
                shutil.rmtree(SKELETON+path, False)
            except (IOError, OSError, shutil.Error):
                return True
            # recreate mount point
            try:
                umask_saved = os.umask(0)
                os.mkdir(SKELETON+path)
                os.umask(umask_saved)
            except (IOError, OSError):
                pass
    return False


def check_skeleton_not_busy():
    # Check that skeleton is unmounted (mount points are not busy)
    if mount_points_busy(['/tmp']):
        secureio.print_error('failed to unmount CageFS - skeleton directory is busy.')
        sys.exit(1)


# Stops cagefs-fuse service, unmounts skeleton and all users
def unmount_all(remount_users = False, check_busy = False, all_cagefs_mounts = False):
    if is_running_without_lve():
        delete_namespaces()
    umount_skeleton(all_cagefs_mounts = all_cagefs_mounts, all_namespaces = True)
    # cagefs_fuse('stop')
    # CAG-440
    # cagefs_proxyexecd('stop')
    time.sleep(1)
    if skeleton_is_mounted():
        secureio.print_error('failed to unmount cagefs-skeleton')
        sys.exit(1)
    if remount_users:
        if not is_running_without_lve() and remount_all():
            sys.exit(1)
        remove_unused_mount_points()
        time.sleep(1)
        if check_busy:
            check_skeleton_not_busy()


# NOTE: path should include path to cagefs-skeleton
def mounts_are_found(path, proc_mounts = None, comparator = cagefslib.mounts_are_found_comparator):
    cagefslib.mounts = MountpointConfig().common_mounts

    if cagefslib.mounts_are_found(path, comparator):
        return True

    if proc_mounts == None:
        proc_mounts = cagefslib.get_mounted_dirs()

    path2 = cagefslib.addslash(os.path.realpath(path))
    for mount in proc_mounts:
        mount = cagefslib.addslash(mount)
        if comparator(path2, mount):
            return True

    return False


# NOTE: path should include path to cagefs-skeleton
def path_includes_mount_point(path, proc_mounts = None):
    return mounts_are_found(path, proc_mounts = proc_mounts, comparator = cagefslib.path_includes_mount_point_comparator)


# NOTE: path should include path to cagefs-skeleton
def path_is_mounted(path, proc_mounts = None):
    return mounts_are_found(path, proc_mounts = proc_mounts, comparator = cagefslib.path_is_mounted_comparator)


def create_remount_flag():
    try:
        open(REMOUNT_FLAG, 'w').close()
    except IOError as e:
        secureio.print_error("failed to create", REMOUNT_FLAG, str(e))


def remove_remount_flag():
    try:
        os.unlink(REMOUNT_FLAG)
    except OSError:
        pass


def home_dirs_search_is_disabled():
    return os.path.isfile('/etc/cagefs/disable.home.dirs.search')


def create_homeN_dirs_in_skeleton():

    """
    Create /usr/share/cagefs-skeleton/home* directories and symlinks when needed,
    so they have the same meaning as in real file system. Make all symlinks relative to /usr/share/cagefs-skeleton.
    Create need.remount flag when needed. Return True if remount is needed.
    """
    if mount_base_dir_enabled():
        return False
    homes = cagefslib.get_homeN_dirs()
    if not homes:
        return False
    proc_mounts = cagefslib.get_mounted_dirs()
    always_home_mounting_mode_enabled = home_dirs_search_is_disabled()
    unmounted = False
    for home in homes:
        path = SKELETON + home
        try:
            if always_home_mounting_mode_enabled:
                if os.path.lexists(path):
                    if os.path.islink(path):
                        if home == '/home':
                            if not unmounted:
                                unmount_all(remount_users = True, check_busy = False)
                                unmounted = True
                            os.unlink(path)
                            os.mkdir(path, 0o755)
                        else:
                            skel_link_to = os.readlink(path)
                            if skel_link_to != 'home':
                                if not unmounted:
                                    unmount_all(remount_users = True, check_busy = False)
                                    unmounted = True
                                os.unlink(path)
                                os.symlink('home', path)
                    elif home == '/home':
                        if not os.path.isdir(path):
                            os.unlink(path)
                            os.mkdir(path, 0o755)
                    elif not mounts_are_found(path, proc_mounts):
                        if not unmounted:
                            unmount_all(remount_users = True, check_busy = False)
                            unmounted = True
                        cagefslib.remove_file_or_dir(path)
                        os.symlink('home', path)
                else:
                    os.symlink('home', path)
            else:
                if os.path.islink(home):
                    link_to = os.path.realpath(home)
                    link_to = link_to[1:]   # remove leading slash - make path relative
                    if os.path.lexists(path):
                        if os.path.islink(path):
                            skel_link_to = stripslash(os.readlink(path))
                            if skel_link_to != link_to:
                                os.unlink(path)
                                os.symlink(link_to, path)
                        elif os.path.isdir(path):
                            if not mounts_are_found(path, proc_mounts):
                                if not unmounted:
                                    unmount_all(remount_users = True, check_busy = False)
                                    unmounted = True
                                shutil.rmtree(path, True)
                                os.symlink(link_to, path)
                        else:   # path is file or socket or device or named pipe
                            os.unlink(path)
                            os.symlink(link_to, path)
                    else:
                        os.symlink(link_to, path)
                elif os.path.isdir(home):
                    if os.path.lexists(path):
                        if os.path.islink(path):
                            if not unmounted:
                                unmount_all(remount_users = True, check_busy = False)
                                unmounted = True
                            os.unlink(path)
                            os.mkdir(path, 0o755)
                        elif os.path.isdir(path):
                            continue
                        else:   # path is file or socket or device or named pipe
                            os.unlink(path)
                            os.mkdir(path, 0o755)
                    else:
                        os.mkdir(path, 0o755)
        except OSError as err:
            secureio.print_error('failed to create', path, ':', str(err))
    if unmounted:
        create_remount_flag()
    return unmounted


def update_homeN_symlinks_in_skeleton():
    """
    Update symlinks /usr/share/cagefs-skeleton/home*, so they point to the same location as in real file system
    """
    if not os.path.isdir(SKELETON) or mount_base_dir_enabled() or home_dirs_search_is_disabled() or not cagefslib.get_homeN_dirs():
        return
    homes = cagefslib.get_homeN_dirs(use_glob=True)
    for home in homes:
        path = SKELETON + home
        try:
            if os.path.islink(home) and os.path.islink(path):
                link_to = os.path.realpath(home)
                link_to = link_to[1:]   # remove leading slash - make path relative
                skel_link_to = stripslash(os.readlink(path))
                if skel_link_to != link_to:
                    os.unlink(path)
                    os.symlink(link_to, path)
        except OSError as err:
            secureio.print_error('failed to process symlink', path, ':', str(err))


def add_parents(list_of_files, set_of_files):
    """ Add parent directories to list_of_files if they do not present in set_of_files """
    is_dict = True
    if not isinstance(set_of_files, dict):
        is_dict = False
        if not isinstance(set_of_files, set):
            set_of_files = set(set_of_files)
    parents = set()
    for filename in list_of_files:
        # remove redundant separators in order to prevent infinite loop
        filename = os.path.normpath(filename)
        if filename.startswith('/'):
            parent = os.path.dirname(filename)
            while parent != '/':
                if (parent not in set_of_files) and (parent not in parents):
                    parents.add(parent)
                    if is_dict:
                        set_of_files[parent] = 1
                    else:
                        set_of_files.add(parent)
                parent = os.path.dirname(parent)
    list_of_files.extend(list(parents))


# Creates (overwrites) file of white list for cagefs-fuse
# (white list of files in system's /etc directory)
def save_etc_white_list():
    sigterm_check()
    white_list = {}
    white_list_copy = list(cagefslib.white_list)

    for ind in range(len(white_list_copy)):
        white_list_copy[ind] = white_list_copy[ind].replace('/etc', '', 1)
        white_list[white_list_copy[ind]] = 1

    add_parents(white_list_copy, white_list)

    white_list_copy.sort()

    umask_saved = os.umask(0o22)
    _file = open(FUSE_WHITE_LIST, 'w')
    for filename in white_list_copy:
        _file.write('%s\n' % filename)
    _file.close()
    os.umask(umask_saved)
    os.chmod(FUSE_WHITE_LIST, 0o600)


list_copy = []
files_list = {}


# Function adds parent directories to (global) list_copy and files_list
def add_parents_to_lists():
    global list_copy, files_list
    list_copy = list(cagefslib.files_list)
    files_list = cagefslib.files_list
    add_parents(list_copy, files_list)


# Function add_parents_to_lists() should be called before call of this function
def save_list_of_files_in_skeleton(files_list = None):
    sigterm_check()
    if files_list is None:
        files_list = list_copy
    files_list.sort()
    umask_saved = os.umask(0o77)
    try:
        f = open(FILES_LIST, 'w')
        for filename in files_list:
            f.write('%s\n' % filename)
        f.close()
    except IOError as e:
        secureio.print_error('Failed to write ' + FILES_LIST + ' : ' + str(e))
    os.umask(umask_saved)
    try:
        os.chmod(FILES_LIST, 0o600)
    except OSError as e:
        secureio.print_error('Failed to change permissions of ' + FILES_LIST + ' : ' + str(e))


# Load list of files in skeleton
def load_list(filename, _list):
    try:
        _file = open(filename, 'r')
    except IOError:
        return
    while True:
        line = _file.readline()
        if line == '':
            break
        if line[0] != '\n':
            line = line.rstrip()
            if line != '' and line[0] == '/':
                _list.append(line)
            else:
                secureio.print_error('path', line, 'is relative')
    _file.close()


# Function compares lists of files in skeleton
# Returns list of files (or dirs) that present in old list and do not present in new list
def compare_lists(old, new, ignore_realpath = False):
    diff = []
    old_len = len(old)
    item = 0
    while item < old_len:
        line = old[item]
        if line not in new:
            if not ignore_realpath:
                path = os.path.realpath(line)
            if ignore_realpath or (path not in new):
                diff.append(line)
                item += 1
                while (item < old_len) and old[item].startswith(line+'/'):
                    diff.append(old[item])
                    item += 1
                continue
        item += 1
    return diff


# Function add_parents_to_lists() should be called before call of this function
def delete_files_from_skeleton(config):
    sigterm_check()
    if config['reinit'] == 1 or config['init'] == 1:
        return
    old_list = []
    load_list(FILES_LIST, old_list)
    files_to_delete = compare_lists(old_list, files_list)
    proc_mounts = cagefslib.get_mounted_dirs()
    for _file in files_to_delete:
        sigterm_check()
        cagefslib.del_libs_from_list(_file)
        file2 = cagefslib.addslash(_file)
        path = SKELETON + _file
        if (not file2.startswith('/dev/')) and (not mounts_are_found(path, proc_mounts)):
            if config['dont-clean'] == 1:
                secureio.logging("Skipping "+ path,SILENT,config['verbose'])
                continue
            if os.path.isdir(path) and (not os.path.islink(path)):
                try:
                    shutil.rmtree(path, False)
                    secureio.logging("Removed directory "+ path,SILENT,1)
                except (OSError, IOError, shutil.Error):
                    secureio.logging('Error while removing directory '+ path,SILENT,1)
            elif os.path.lexists(path):
                try:
                    os.unlink(path)
                    secureio.logging('Removed file '+ path,SILENT,1)
                except (OSError, IOError):
                    secureio.logging('Error while removing file '+ path,SILENT,1)


# Function sends SIGUSR1 signal to cagefs-fuse in order to reload cagefs-fuse config files
def reload_fuse_conf():
    for pid in Execute("/sbin/pidof cagefs-fuse").split():
        os.kill(int(pid), signal.SIGUSR1)


# Compare dirs
# Returns True if dirs are equal, False otherwise
def are_dirs_equal(dir1, dir2):
    if (not os.path.isdir(dir1)) or (not os.path.isdir(dir2)):
        return False

    try:
        # run the "diff" command and suppress it's output
        p = subprocess.Popen([DIFF, "-r", dir1, dir2],\
                                                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        p.communicate()
        # check return code of the child
        if p.returncode == 0:
            # dirs are equal
            return True
    except OSError:
        secureio.logging('failed to run ' + DIFF + " -r " + dir1 + " " + dir2, SILENT, 1)

    # some differences were found
    return False


def create_symlink_for_selector_config_dir(selector_name):
    """
    Create symlink to NodeJS/Python/etc selector config directory
    inside template for user etc directory
    For details see CAG-797, CAG-828
    :param selector_name: name of selector: nodejs, python, etc
    """
    # construct symlink path inside etc template (we ommit leading slash)
    temp_path = os.path.join(cagefslib.ETC_TEMPLATE_NEW_DIR, 'etc/cl.{}'.format(selector_name))
    link_to = SELECTOR_CONF_DIR_TEMPLATE.format(selector_name)
    try:
        create_symlink(link_to, temp_path)
    except OSError as e:
        secureio.logging('Error while creating symlink ' + temp_path + ' to ' + link_to + str(e), SILENT, 1)


def compare_etc_templates(force_update_etc = False):
    # Remove /etc/mail from template of etc directory (/etc/mail is mounted from real system)
    shutil.rmtree(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc/mail', True)

    create_symlink_for_selector_config_dir('nodejs')
    create_symlink_for_selector_config_dir('python')

    # Remove blacklisted files & dirs from skeleton
    remove_blacklisted_files()

    # Old template exists ?
    if os.path.isfile(cagefslib.ETC_TEMPLATE_DIR+'/etc/passwd'):
        # Compare old and new templates of etc directory. Increase version if not equal
        old_etc_version = get_etc_version(cagefslib.ETC_TEMPLATE_DIR+'/etc')
        # set_etc_version(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', old_etc_version)
        copy_etc_version(cagefslib.ETC_TEMPLATE_DIR+'/etc', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc')
        if (not force_update_etc) and cagefslib.are_dirs_equal(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', cagefslib.ETC_TEMPLATE_DIR+'/etc', shallow = False):
            # Do not replace etc template (templates are equal)
            return
        else:
            set_etc_version(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', old_etc_version+1)
    else:
        # Do not compare old and new templates of etc directory (old template does not exist)
        # Set version of new template to 1
        set_etc_version(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', 1)

    # Remove old etc template
    shutil.rmtree(cagefslib.ETC_TEMPLATE_DIR+'/etc', True)

    # Move new etc template to proper location
    try:
        os.rename(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', cagefslib.ETC_TEMPLATE_DIR+'/etc')
    except (OSError, IOError):
        secureio.logging("Error moving "+cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc to '+cagefslib.ETC_TEMPLATE_DIR+'/etc', SILENT, 1)
        sys.exit(1)


def remove_nested_skeleton():
    sigterm_check()
    # Hide (override) global SKELETON variable (latter can be changed if /usr/share/cagefs-skeleton is symlink)
    SKELETON = '/usr/share/cagefs-skeleton'
    if os.path.lexists(SKELETON+SKELETON):
        try:
            if os.path.isdir(SKELETON+SKELETON) and (not os.path.islink(SKELETON+SKELETON)):
                shutil.rmtree(SKELETON+SKELETON, False)
            else:
                os.unlink(SKELETON+SKELETON)
        except (OSError, IOError, shutil.Error) as e:
            secureio.logging("Error: failed to remove "+SKELETON+SKELETON+" : "+str(e), SILENT, 1)
    if os.path.lexists(SKELETON+SKELETON):
        secureio.logging("Error: "+SKELETON+SKELETON+" exists. Please remove manually.", SILENT, 1)
        sys.exit(1)

    bdir = SKELETON+'/var/cagefs'

    if os.path.lexists(bdir):
        try:
            if os.path.isdir(bdir) and (not os.path.islink(bdir)):
                shutil.rmtree(bdir, False)
            else:
                os.unlink(bdir)
        except (OSError, IOError, shutil.Error) as e:
            secureio.logging("Error: failed to remove "+bdir+" : "+str(e), SILENT, 1)
    if os.path.lexists(bdir):
        secureio.logging("Error: "+bdir+" exists. Please remove manually.", SILENT, 1)


def update_etc_only(config, users = None, print_selector_errors = False):
    remove_log_file()
    ci = cagefs_init()

    cfg = read_config()

    cagefslib.read_native_conf()

    load_black_list()

    umask_saved = os.umask(0)

    # Create empty directory for template of etc directory
    shutil.rmtree(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', True)
    if not os.path.isdir(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc'):
        try:
            mod_makedirs(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', 0o755)
        except OSError:
            secureio.print_error('creating', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc')
            sys.exit(1)

    # Build or update template of etc directory
    for section in cfg.sections():
        ci.update_etc_from_section(config, cfg, section)
        remove_nested_skeleton()

    # Update native php files in template of etc directory (if needed)
    ci.update_etc_paths(list(cagefslib.orig_binaries.values()))  # pylint: disable=dict-values-not-iterating
    if print_selector_errors:
        for alias, orig_path in cagefslib.orig_binaries.items():
            orig_path2 = os.path.realpath(orig_path)
            if orig_path2.startswith('/etc/'):
                if (not cagefslib.move_to_alternatives(orig_path2, etc = True)) and cagefslib.is_mandatory(alias):
                    secureio.print_error("CloudLinux Selector setup, path:", orig_path)
                    return True

    cagefslib.remove_unwanted_users_from_groups()

    os.umask(umask_saved)

    create_files_for_symlink_protection()
    create_dirs_for_symlink_protection()

    compare_etc_templates(force_update_etc = config['force-update-etc'])

    if users == None:
        # Create etc for all users > MIN_UID (ignore users.enabled directory)
        update_etc(config, all_users = True)
    else:
        # update etc directory for specified users
        update_etc(config, users = users)

    return False


# /etc/cagefs/proxy.commands format:
# SENDMAIL=/usr/sbin/sendmail
# SENDMAILQ=/var/qmail/bin/sendmail
# MAIMANCPAN=/usr/local/cpanel/3rdparty/mailman/mail/mailman
# CRONTAB_LIST:proxy.crontab.cagefs=root:/usr/bin/crontab
# CRONTAB_SAVE:noproceed=root:/usr/bin/crontab

def load_wrappers(update_wrappers = False):
    wrappers, wrappers_names = build_wrappers_dicts()

    cagefslib.wrappers.update(wrappers)
    cagefslib.wrappers_names.update(wrappers_names)

    if update_wrappers:
        cagefslib.mounts = MountpointConfig().common_mounts
        proc_mounts = cagefslib.get_mounted_dirs()

        # Install wrappers
        for _file in cagefslib.wrappers:
            if os.path.isfile(_file):
                path = SKELETON+_file
                if not path_is_mounted(path, proc_mounts):
                    if os.path.isfile(path):
                        cagefslib.install_wrapper(_file)
                    elif not os.path.exists(path):
                        cagefslib.create_parent_path(SKELETON, os.path.dirname(_file), copy_permissions=1, copy_ownership=1)
                        cagefslib.install_wrapper(_file)

def check_separator(separator, line):
    if separator not in line:
        return None, None # main separator not found
    line_parts = line.split(separator)
    if len(line_parts) != 2:
        return None, None # too many main separators in line
    return line_parts[0], line_parts[1]


def validate_with_regex(regex, part, can_be_none):
    if part is not None:
        regexp_comp = re.compile(regex)
        p1 = regexp_comp.match(part)
        if p1 is None:
            return False # username is not valid
        return True
    if can_be_none:
        return True
    return False


def check_proxy_line(line):
    """
    Return False if line is corrupted
    """
    if line.startswith('#') or line.isspace():  # skip comments and empty lines
        return True
    alias_wrapper, user_command = check_separator('=', line)
    if alias_wrapper is None or user_command is None:
        return False
    if ':' in alias_wrapper:
        alias, wrapper = check_separator(':', alias_wrapper)
    else:
        alias = alias_wrapper
        wrapper = None
    if ':' in user_command:
        user, command = check_separator(':', user_command)
    else:
        command = user_command
        user = None
    if alias is None or command is None:
        return False

    # check that last param is valid asb path
    if not os.path.isabs(command):
        return False
    if command.strip()[-1] == '/':
        return False
    # check that string is MAYBE username
    if not validate_with_regex(r'^[a-z][-a-z0-9]*$', user, can_be_none = True):
        return False

    # check alias
    if not validate_with_regex(r'^[a-zA-Z][-a-zA-Z0-9_]*$', alias, can_be_none = False):
        return False

    #check wrapper
    if not validate_with_regex(r'^[a-zA-Z.][-a-zA-Z0-9_.]*$', wrapper, can_be_none = True):
        return False

    return True


def build_wrappers_dicts(raise_exception = False):
    DEFAULT_PROXY_NAME = "cagefs.proxy.program"

    commands = load_wrappers_commands()

    # Build hashes
    wrappers = {}
    wrappers_names = {}
    for line in commands:
        if not check_proxy_line(line):
            if raise_exception:
                raise Exception('Warning: Found corrupted line:' + str(line) + '. Skip line.')
            else:
                print('Warning: Found corrupted line:' + str(line) + '. Skip line.')
            continue
        if line.startswith('#'):
            continue
        words = line.strip().split('=', 1)
        if len(words) == 2:
            words_left = words[0].strip().split(':', 1)

            if len(words_left) == 2 and words_left[1] == 'noproceed':
                continue

            alias = words_left[0].strip()

            if len(words_left) == 2:
                wrapper_name = words_left[1].strip()
            else:
                wrapper_name = DEFAULT_PROXY_NAME

            if words[1].find(':') == -1:
                command = words[1].strip()
            else:
                words_right = words[1].strip().split(':', 1)
                command = words_right[1].strip()

            if command not in wrappers:
                wrappers[command] = alias
            if command not in wrappers_names:
                wrappers_names[command] = wrapper_name

    return wrappers, wrappers_names


def load_wrappers_commands():
    proxy_commands_dir = os.path.dirname(PROXY_COMMANDS)
    proxy_commands_name = os.path.basename(PROXY_COMMANDS)

    # Load user command files first
    filenames = sorted([
        filename for filename in os.listdir(proxy_commands_dir)
        if filename.endswith(proxy_commands_name) and filename != proxy_commands_name
    ])
    filenames.append(proxy_commands_name)

    commands = []
    for filename in filenames:
        path = os.path.join(proxy_commands_dir, filename)
        if os.path.isfile(path):
            commands.extend(cagefslib.read_file(path))

    return commands


# ETC_MPFILE should be read to cagefslib.mounts before call of this function
def remove_blacklisted_files():
    proc_mounts = cagefslib.get_mounted_dirs()
    for black_list_file in cagefslib.black_list:
        sigterm_check()
        # Do not delete jailshell because it should be replaces with symlink to /bin/bash
        if black_list_file == '/usr/local/cpanel/bin/jailshell':
            continue

        is_in_etc = black_list_file.startswith('/etc/')
        if is_in_etc:
            path = cagefslib.ETC_TEMPLATE_NEW_DIR + black_list_file
        else:
            path = SKELETON + black_list_file

        if os.path.lexists(path):
            if not is_in_etc:
                if path_is_mounted(path, proc_mounts):
                    secureio.logging("Warning: blacklisted path "+black_list_file+" is mounted", SILENT, 1)
                    continue
                if path_includes_mount_point(path, proc_mounts):
                    secureio.logging("Warning: blacklisted path "+black_list_file+" includes mount point", SILENT, 1)
                    continue

            try:
                if os.path.isdir(path) and (not os.path.islink(path)):
                    shutil.rmtree(path, False)
                else:
                    os.unlink(path)
                secureio.logging("Removed "+path, SILENT, VERBOSE)
            except (OSError, IOError, shutil.Error) as e:
                secureio.logging("Error: failed to remove "+path+" : "+str(e), SILENT, 1)
            if os.path.lexists(path):
                secureio.logging("Warning: blacklisted path "+path+" exists. Please remove manually.", SILENT, 1)


def load_black_list(remove = False):
    black_list_dir = os.path.dirname(BLACK_LIST_FILE)
    black_list_name = os.path.basename(BLACK_LIST_FILE)

    cagefslib.black_list = []
    for filename in os.listdir(black_list_dir):
        path = os.path.join(black_list_dir, filename)
        if filename.endswith(black_list_name) and os.path.isfile(path):
            # Read content of black list file, remove path to skeleton and trailing newlines
            black_list = cagefslib.read_file(path)
            for line in black_list:
                line = cagefslib.strip_path(line.rstrip())
                if line.startswith('/'):
                    if line =="/" or line.find("/../") != -1 or line.endswith("/.."):
                        secureio.print_error("Invalid path", line, "in file", path)
                        continue
                    if not line.startswith('/etc/'):
                        line = os.path.realpath(line)
                    if line not in cagefslib.black_list:
                        cagefslib.black_list.append(line)

    cagefslib.mounts = MountpointConfig().common_mounts

    if remove:
        remove_blacklisted_files()


def replace_jailshell():
    bin_list = ['/usr/local/cpanel/bin/jailshell', '/usr/local/psa/bin/chrootsh']
    dest = '/bin/bash'

    for bin_name in bin_list:
        parent_dir = SKELETON + os.path.dirname(bin_name)
        link_name = SKELETON + bin_name

        if os.path.isdir(parent_dir) and (not os.path.islink(link_name)):
            if os.path.lexists(link_name):
                cagefslib.remove_file_or_dir(link_name, check_mounts = True)
            try:
                os.symlink(dest, link_name)
            except (OSError, IOError):
                secureio.print_error('creating symlink', link_name, 'to', dest)
                sys.exit(1)


def copy_options(src, section, dst, new_section):
    for option in src.options(section):
        dst.set(new_section, option, src.get(section, option))


def copy_section(src, dst, section):
    if src.has_section(section):
        if (not dst.has_section(section)) and (section.lower() != 'default'):
            dst.add_section(section)
            copy_options(src, section, dst, section)
        else:
            error = True
            for num in range(100):
                new_section = section + str(num)
                if not dst.has_section(new_section):
                    error = False
                    break
            if error:
                new_section = section + id_generator()
            dst.add_section(new_section)
            copy_options(src, section, dst, new_section)


def read_config(fail_if_sections_are_duplicated = False):
    cfg = configparser.RawConfigParser(strict=False)

    # section -> filepath
    sections = {}

    # Read base config files
    for _file in os.listdir(CONFIG_DIR):
        if _file.endswith('.cfg'):
            path = CONFIG_DIR+_file

            tmp = configparser.RawConfigParser(strict=False)
            tmp.read(path)

            if fail_if_sections_are_duplicated:
                for section in tmp.sections():
                    if section in sections:
                        secureio.logging("Error: duplicated section ["+section+"] in files "+sections[section]+" and "+path, SILENT, 1)
                        sys.exit(1)
                    else:
                        sections[section] = path

            for section in tmp.sections():
                copy_section(tmp, cfg, section)

    # Read config files for packages that are installed by user
    if os.path.isdir(WORK_CONFIG_DIR):
        for _file in os.listdir(WORK_CONFIG_DIR):
            if _file.endswith('.work'):
                path = os.path.join(WORK_CONFIG_DIR, _file)

                tmp = configparser.RawConfigParser(strict=False)
                tmp.read(path)

                if fail_if_sections_are_duplicated:
                    for section in tmp.sections():
                        if section in sections:
                            secureio.logging("Error: duplicated section ["+section+"] in files "+sections[section]+" and "+path, SILENT, 1)
                            sys.exit(1)
                        else:
                            sections[section] = path

                for section in tmp.sections():
                    copy_section(tmp, cfg, section)

    return cfg


def create_symlinks_in_skeleton():
    """
    Create symlinks in CageFS skeleton
    """
    symlinks = {
        SKELETON+'/var/tmp' : '../tmp',
        SKELETON+'/var/run' : '../run'
    }
    cagefslib.write_symlinks(symlinks)


def update_cagefs(config):
    if (config['update'] == 1) and (config['force-update'] == 0):
        if not cagefslib.update_of_cagefs_skeleton_is_needed():
            if not SILENT:
                print("cagefs-skeleton has been updated recently, if you want to force the update, please run:")
                print('"cagefsctl --force-update"')
            return

    update_rpm_packages()

    add_default_rpm_packages_to_cagefs()

    remove_log_file()
    ci = cagefs_init()

    # create cagefs.mp
    if config['init'] == 1:
        create_mp(False, exit_on_error=True)

    cagefslib.mounts = MountpointConfig().common_mounts

    cfg = read_config()

    cagefslib.read_native_conf()

    create_files_for_symlink_protection()
    create_dirs_for_symlink_protection()

    if config['reinit'] == 0 and config['init'] == 0:
        # Load list of libraries in skeleton
        cagefslib.load_libs(LIBS_LIST)

    # Debugging, checks...
    if cagefslib.debug_option and cagefslib.libs_list and os.path.isfile(LIBDIR+'/libs.dat'):
        print('Loading libs.dat', end=' ', flush=True)
        rtf = open(LIBDIR+'/libs.dat', 'r')
        td = eval(rtf.read())
        rtf.close()
        print("Done")

        if td:

            print("Pickle len", len(cagefslib.libs_list))
            print("Eval len", len(td))

            print('Comparing', end=' ', flush=True)
            try:
                for key in td:
                    for item in td[key]:
                        if item not in cagefslib.libs_list[key]:
                            secureio.print_error('line', cagefslib.lineno(), ' : NOT EQUAL', key)
                            break
            except KeyError as e:
                secureio.print_error('line', cagefslib.lineno(), " : Key Error", e)

            try:
                for key in cagefslib.libs_list:
                    for item in cagefslib.libs_list[key]:
                        if item not in td[key]:
                            secureio.print_error('line', cagefslib.lineno(), ' : NOT EQUAL', key)
                            break
            except KeyError as e:
                secureio.print_error('line', cagefslib.lineno(), " : Key Error", e)

            print("Done")

    load_wrappers(True)

    load_black_list()

    umask_saved = os.umask(0)

    # Create empty directory for template of etc directory
    sigterm_check()
    shutil.rmtree(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', True)
    if not os.path.isdir(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc'):
        try:
            mod_makedirs(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', 0o755)
        except OSError:
            secureio.print_error('creating', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc')
            sys.exit(1)

    # Build or update skeleton
    for section in cfg.sections():
        sigterm_check()
        ci.handle_cfg_section(config, SKELETON, cfg, section)
        remove_nested_skeleton()

    # Update native php files in cagefs-skeleton and in template of etc directory
    ci.update_paths(config, SKELETON, list(cagefslib.orig_binaries.values()))  # pylint: disable=dict-values-not-iterating

    ci.update_alt_php_libs(config, SKELETON)

    cagefslib.remove_unwanted_users_from_groups()

    # Create mount points in skeleton
    create_mount_points(MOUNT_POINTS)

    os.umask(umask_saved)

    # CAG-993: create /root directory inside CageFS, because
    # home directory for root user is needed when running cagefsctl --enter
    cagefslib.make_dir(SKELETON+'/root', 0o550, allow_symlink=False, update_perm=True)
    cagefslib.set_owner(SKELETON+'/root', 0, 0)

    cagefslib.save_etc_safe_list(['/passwd', '/group'])

    # Add parent directories to lists
    add_parents_to_lists()

    # Load old list of files, compare to current list,
    # delete files from skeleton, delete appropriate libs from cagefslib.libs_list
    delete_files_from_skeleton(config)

    replace_jailshell()

    create_symlinks_in_skeleton()

    cagefs_da_lib.create_symlink_to_php_ini_for_DA(SKELETON)

    # Save new list of files
    save_list_of_files_in_skeleton()

    # Save list of libraries in skeleton
    cagefslib.save_libs(LIBS_LIST)

    if cagefslib.debug_option:
        tf2 = open(LIBDIR+'/libs.txt', 'w')
        for key in sorted(cagefslib.libs_list):
            tf2.write("%s : %s\n" % (key, " ".join(cagefslib.libs_list[key])))
        tf2.close()

        print('Saving libs.dat', end=' ', flush=True)
        tf = open(LIBDIR+'/libs.dat', 'w')
        tf.write(repr(cagefslib.libs_list))
        tf.close()
        print("Done")

    # Create (overwrite) white list for cagefs-fuse
    save_etc_white_list()

    compare_etc_templates()

    if etcfs_is_disabled():
        # CageFS is enabled ?
        if (not save_dir_exists()):
            update_etc(config)
        else:
            # Update etc for all users > MIN_UID
            update_etc(config, all_users = True)
    elif config['update'] == 1:
        reload_fuse_conf()

    if (config['reinit'] == 1) and ('cagefs_was_enabled' in config):
        enable_cagefs()

    create_empty_dir()

    # CAG-706: setup emulation for /var/run/utmp
    cagefslib.create_utmp_in_skeleton()

    setup_cpanel_multiphp(do_mount=False)

    # LU-640: update license file inside CageFS (needed by cloudlinux-selector to check license)
    mount_file(LICENSE_TIMESTAMP_FILE, do_mount=False)

    cagefslib.add_syslog_socket()
    # CAG-1062: [CL8.2] Mount socket of systemd-journal into CageFS
    _mount_systemd_journal_socket(do_mount=False)

    unmounted = create_homeN_dirs_in_skeleton()

    update_homeN_symlinks_in_skeleton()

    from cagefsreconfigure import add_mounts_for_passenger
    add_mounts_for_passenger()

    # CageFS is enabled and init (or reinit) command is running ?
    if (config['reinit'] == 1 or config['init'] == 1) and (not save_dir_exists()):
        # Remount skeleton and all users
        mount_skeleton(True)
        unmounted = False
        if os.path.isfile('/usr/bin/systemctl'):
            Execute('/usr/bin/systemctl start cagefs')

    if unmounted:
        mount_skeleton(True)

    # Update statuses of users (needed for PHP Selector)
    if cagefs_is_enabled():
        secureio.logging("Updating statuses of users ...", SILENT, 1)
        update_users_status()

    cagefslib.save_last_update_time()


do_not_ask_option = False


def confirm(message):
    print(message, end=' ', flush=True)

    if do_not_ask_option:
        print('yes')
        return

    while True:
        line = sys.stdin.readline()
        if line == "yes\n":
            break
        elif line == "no\n":
            print("Aborting")
            sys.exit(1)
        print("Please, reply with yes or no")


def unmount_skeleton_in_all_namespaces():
    print("Unmounting skeleton    ", end=' ', flush=True)
    if umount_skeleton(all_cagefs_mounts=True, all_namespaces=True):
        secureio.print_error('unmounting skeleton')
        sys.exit(1)
    time.sleep(1)
    print("[DONE]")


def remove_all():
    confirm("WARNING: If you continue, CageFS will be disabled, and all "
            "related files and directories will be removed. Do you want to continue (yes/no)? ")

    print("Disabling CageFS    ", end=' ', flush=True)
    shutil.rmtree(INIPREFIX+'users.disabled', True)
    shutil.rmtree(INIPREFIX+'users.enabled', True)
    update_users_status(disable_all = True)
    print("[DONE]")

    if is_running_without_lve():
        print("Unmounting users   ", end=' ', flush=True)
        if delete_namespaces():
            secureio.print_error('unmounting users')
            sys.exit(1)
        time.sleep(1)
        print("[DONE]")
        unmount_skeleton_in_all_namespaces()
    else:
        unmount_skeleton_in_all_namespaces()

        # CAG-440 - do not stop proxyexecd service because it is needed for cagefs_enter
        # print "Stopping proxyexecd service    ",
        # cagefs_proxyexecd('stop')
        # time.sleep(1)
        # print "[DONE]"

        print("Unmounting users   ", end=' ', flush=True)
        if remount_all():
            secureio.print_error('unmounting users')
            sys.exit(1)
        time.sleep(1)
        print("[DONE]")

    check_skeleton_not_busy()

    if skeleton_is_mounted():
        secureio.print_error('failed to unmount cagefs-skeleton')
        sys.exit(1)

    if repair_homes.invalid_homes_exist():
        print('Users with invalid pathes to home directories exist! DO NOT REMOVE /var/cagefs !')
    else:
        print("Removing "+BASEDIR, end=' ', flush=True)
        shutil.rmtree(BASEDIR, True)
        print("   [DONE]")

    print("Removing "+SKELETON, end=' ', flush=True)
    shutil.rmtree(SKELETON, True)
    print("   [DONE]")

    old_skel = SKELETON + '.old'
    if os.path.isdir(old_skel) and not skeleton_is_mounted(old_skel):
        print("Removing "+old_skel, end=' ', flush=True)
        shutil.rmtree(old_skel, True)
        print("   [DONE]")


def usage():
    print('')
    print('Use following syntax to manage CageFS:')
    print(sys.argv[0]+" [OPTIONS]")
    print('Options:')
    print(" -i | --init                 : initialize CageFS (create CageFS if it does not exist)")
    print(" -r | --reinit               : reinitialize CageFS (make backup and recreate CageFS)")
    print(" -u | --update               : update files in CageFS (add new and modified files to CageFS,")
    print("                               remove unneeded files)")
    print(" -f | --force                : recreate CageFS (do not make backup, overwrite existing files)")
    print(' -d | --dont-clean           : do not delete any files from skeleton (use with --update option)')
    print(" -k | --hardlink             : use hardlinks if possible")
    print('      --create-mp            : Recreates /etc/cagefs/cagefs.mp file with default set of mount points.')
    print('                               WARNING: Any previous changes made to file by admin or by any software will be lost')
    print('      --mount-skel           : mount CageFS skeleton directory')
    print("      --unmount-skel         : unmount CageFS skeleton directory")
    print('      --remove-all           : disable CageFS, remove templates and /var/cagefs directory')
    print('      --sanity-check         : perform basic self-diagnistics of common cagefs-related issues(mostly useful for support)')
    print('      --addrpm               : add rpm-packages into CageFS (run "cagefsctl --update" in order to apply changes)')
    print('                             : only package name should be specified (without package version and release)')
    print('                             : example: cagefsctl --addrpm ImageMagick')
    print('      --delrpm               : remove rpm-packages from CageFS (run "cagefsctl --update" in order to apply changes)')
    print('      --list-rpm             : list rpm-packages that are installed in CageFS')
    print(" -e | --enter                : enter into user's CageFS as root")
    print('      --update-list          : update specified files only (paths are read from stdin)')
    print('      --update-etc           : update etc directory of all or specified users')
    print('      --set-update-period    : set min period of update of CageFS in days (default = 1 day)')
    print('      --force-update         : force update of CageFS (ignore period of update)')
    print('      --force-update-etc     : force update of /etc directories for users in CageFS')
    print('      --reconfigure-cagefs   : configure CageFS integration with other software (control panels,')
    print('                               database servers, etc)')
    print('')
    print('Use following syntax to manage users:')
    print(sys.argv[0]+' [OPTIONS] username [more usernames]')
    print('Options:')
    print(' -m | --remount           : remount specified user(s)')
    print(' -M | --remount-all       : remount CageFS skeleton directory and all users')
    print('                            (use this each time you have changed cagefs.mp file)')
    print(' -w | --unmount           : unmount specified user(s)')
    print('    | --unmount-dir       : unmount specified dir in all mount namespaces')
    print(' -W | --unmount-all       : unmount CageFS skeleton directory and all users')
    print(' -l | --list              : list users that entered in CageFS')
    print('      --list-logged-in    : list users that entered in CageFS via SSH')
    print('      --enable            : enable CageFS for the user')
    print('      --disable           : disable CageFS for the user')
    print('      --enable-all        : enable all users, except specified in', disabled_dir)
    print('      --disable-all       : disable all users, except specified in', enabled_dir)
    print('      --display-user-mode : display current mode ("Enable All" or "Disable All")')
    print('      --toggle-mode       : toggle mode saving current lists of users')
    print('                            (lists of enabled and disabled users remain unchanged)')
    print('      --list-enabled      : list enabled users')
    print('      --list-disabled     : list disabled users')
    print('      --user-status       : print status of specified user (enabled or disabled)')
    print("      --getprefix         : display prefix for user")
    print('')
    print('PHP Selector related options:')
    print('      --setup-cl-selector         : setup PHP Selector or register new alt-php versions')
    print('      --remove-cl-selector        : unregister alt-php versions, switch users to default php version when needed')
    print('      --rebuild-alt-php-ini       : rebuild alt_php.ini file for specified users (or all users if none specified)')
    print('      --validate-alt-php-ini      : same as --rebuild-alt-php-ini but also validates alt_php.ini options ')
    print('      --cl-selector-reset-versions: reset php version for specifed users to default (or all users if none specified)')
    print('      --cl-selector-reset-modules : reset php modules (extensions) for specific users to defaults (or all users if none specified)')
    print('      --create-virt-mp            : create virtual mount points for the user')
    print('      --create-virt-mp-all        : create virtual mount points for all users')
    print('      --remount-virtmp            : create virtual mount points and remount user')
    print('      --apply-global-php-ini      : use with 0, 1 or 2 arguments from the list: error_log, date.timezone')
    print('                                    without arguments applies all global php options including two above')
    print('')
    print('Common options:')
    print('      --enable-cagefs                : enable CageFS')
    print('      --disable-cagefs               : disable CageFS')
    print('      --cagefs-status                : print CageFS status (enabled or disabled)')
    print('      --check-cagefs-initialized     : properly checks whether CageFS is initialized and print result')
    print('      --set-min-uid                  : Set min UID')
    print('      --get-min-uid                  : Display current MIN_UID setting')
    print('      --print-suids                  : Print list of SUID and SGID programs in skeleton')
    print('      --do-not-ask                   : assume "yes" in all queries (should be the first option in command)')
    print('      --clean-var-cagefs             : clean /var/cagefs directory (remove data of non-existent users)')
    print('      --set-tmpwatch                 : set tmpwatch command and parameters (save to '+cagefslib.CAGEFS_INI+' file)')
    print('      --tmpwatch                     : execute tmpwatch (remove outdated files in tmp directories in CageFS for all users)')
    print("      --toggle-plugin                : disable/enable CageFS plugin")
    if is_running_without_lve():
        print("      --create-namespace USER        : create namespace for the USER (only for containers)")
        print("      --create-namespaces            : create namespaces for all users (only for containers)")
        print("      --delete-namespace USER        : delete namespace or the USER (only for containers)")
        print("      --delete-namespaces            : delete namespaces for all users (only for containers)")
    print(' -v | --verbose                      : verbose output')
    print('      --wait-lock                    : wait for end of execution of other cagefsctl processes (when needed) before execution of the command')
    print(' -h | --help                         : this message')
    print('')


def check_skeleton():
    if not check_cagefs_skeleton():
        secureio.print_error('directory', SKELETON, 'does NOT exist or is empty.')
        secureio.print_error('Use "'+sys.argv[0]+' --init" to create CageFS')
        sys.exit(1)
    os.chmod(SKELETON, 0o755)


def remove_parent_dirs(paths):
    result = []
    for path in paths:
        spath = cagefslib.addslash(path)
        parent = False
        for path2 in paths:
            if path2.startswith(spath):
                parent = True
                break
        if not parent:
            result.append(path)
    return result


def addrpm(pkg_name, silent = False):
    WORK_DIR = WORK_CONFIG_DIR
    from simple_rpm import get_package_files

    package_files = get_package_files(pkg_name)
    if package_files is None:
        if not silent:
            print("Package %s not installed" % pkg_name)
        return

    if not os.path.lexists(WORK_DIR):
        try:
            mod_makedirs(WORK_DIR, 0o700)
        except (OSError, IOError):
            secureio.print_error("failed to create", WORK_DIR)
            sys.exit(1)
    elif not os.path.isdir(WORK_DIR):
        secureio.print_error("path", WORK_DIR, "should be directory")
        sys.exit(1)

    umask_saved = os.umask(0o77)

    WORK_FILE = os.path.join(WORK_DIR, pkg_name+".work")
    aFile = open ( WORK_FILE, 'w' )
    aFile.write("["+pkg_name+"]\n")
    aFile.write("paths=")
    i = 0
    package_files = remove_duplicates(package_files)
    package_files = remove_parent_dirs(package_files)
    for b in package_files:
        if not b.startswith("/usr/share/man/") and not b.startswith("/usr/share/locale/") and \
            not b.startswith("/usr/share/doc/") and not b.startswith("/usr/share/info/") and \
            not b.startswith("/usr/lib/.build-id/") and not b.startswith("/usr/share/licenses/"):
            if (i != 0):
                aFile.write(", "+b)
            else:
                aFile.write(b)
            i = i + 1
    aFile.write("\n")
    aFile.close()

    os.umask(umask_saved)


def delrpm(pkg_name, silent = False):
    WORK_DIR = WORK_CONFIG_DIR
    WORK_FILE = os.path.join(WORK_DIR, pkg_name+".work")
    if not os.path.lexists(WORK_FILE):
        if not silent:
            print("Rpm %s is not installed in CageFS" % pkg_name)
    elif os.path.islink(WORK_FILE) or (not os.path.isfile(WORK_FILE)):
        secureio.print_error(WORK_FILE, "should be regular file")
    else:
        try:
            os.remove(WORK_FILE)
        except (OSError, IOError):
            secureio.print_error("failed to remove", WORK_FILE)
        # if this is standard packet, save it
        if pkg_name in STD_PACKAGES:
            try:
                # Add package to file '/usr/share/cagefs/exclude.packages'
                # read old list
                packagesToExclude = []
                if os.path.exists ( STD_PACKAGES_FILE ):
                    packagesToExclude = cagefslib.read_file ( STD_PACKAGES_FILE )
                # Append to exclude list, if it is not there
                if (pkg_name+'\n') not in packagesToExclude:
                    packagesToExclude.append ( pkg_name+'\n' )
                    cagefslib.write_file ( STD_PACKAGES_FILE, packagesToExclude, False )
            except (OSError, IOError):
                secureio.print_error("failed to write package list")


def list_rpm(silent = False):
    WORK_DIR = WORK_CONFIG_DIR
    rpms = []
    if os.path.isdir(WORK_DIR):
        for work in os.listdir(WORK_DIR):
            file_name = work[:work.rfind(".")]
            rpms.append(file_name)
    rpms.sort()
    if not silent:
        for package in rpms:
            print(package)
    return rpms


def add_rpm_packages_to_cagefs(args, overwrite = False):
    for rpm in args:
        sigterm_check()
        if overwrite or (not os.path.isfile(os.path.join(WORK_CONFIG_DIR, rpm + '.work'))):
            addrpm(rpm, silent = True)


def remove_rpm_packages_from_cagefs(args):
    for rpm in args:
        delrpm(rpm, silent = True)


def add_default_rpm_packages_to_cagefs():
    # standard packages - STD_PACKAGES
    # packages to exclude (file '/usr/share/cagefs/exclude.packages')
    packagesToExclude = []
    if os.path.exists ( STD_PACKAGES_FILE ):
        packagesToExclude = cagefslib.read_file ( STD_PACKAGES_FILE )
    packagesToAdd = []
    for packName in STD_PACKAGES:
        if (packName+'\n') not in packagesToExclude:
            packagesToAdd.append ( packName )
    # Add to cageFs
    add_rpm_packages_to_cagefs ( packagesToAdd )


def update_rpm_packages():
    sigterm_check()
    rpms = list_rpm(silent = True)
    add_rpm_packages_to_cagefs(rpms, overwrite = True)


# Write MIN_UID to file
def set_min_uid(value):
    global MIN_UID
    buf_val = int(value)
    if buf_val < 100:
        secureio.print_error("MIN UID should be >= 100")
        sys.exit(1)
    MIN_UID = buf_val
    try:
        binfile = open(MIN_UID_FILENAME, 'wb')
        data = struct.pack('i', MIN_UID)
        binfile.write(data)
        binfile.close()
    except:
        secureio.print_error("writting MIN UID to file", MIN_UID_FILENAME)
        sys.exit(1)


# Read MIN_UID from file
def get_min_uid():
    global MIN_UID

    try:
        uid = read_min_uid()
    except ValueError as e:
        secureio.print_error(str(e), MIN_UID_FILENAME)
        sys.exit(1)

    if uid is not None:
        MIN_UID = uid


def read_min_uid():
    """
    Gets minuid from file and returns
    unpacked value if no errors happened
    otherwise None
    """
    if not os.path.isfile(MIN_UID_FILENAME):
        return None

    try:
        binfile = open(MIN_UID_FILENAME, 'rb')
        intsize = struct.calcsize('i')
        data = binfile.read(intsize)
        binfile.close()
    except:
        raise ValueError('failed to read MIN UID from file')

    if len(data) != intsize:
        raise ValueError('reading MIN UID from file')

    num = struct.unpack('i', data)
    if len(num) > 0 and num[0] >= 100:
        return num[0]
    return None


# toggle mode saving current lists of users
# (lists of enabled and disabled users remain unchanged)
def toggle_mode():
    check_save_dir()
    mode = get_user_mode()
    check_mode_error(mode)

    if mode == 'Enable All':
        # Get list of enabled users
        enabled_users = get_list_of_users(True)

        # Set mode "Disable All"
        set_user_mode(False)

        for user in enabled_users:
            # Enable user
            toggle_user(user, True)

    elif mode == 'Disable All':
        # Get list of disabled users
        disabled_users = filter_users(get_list_of_users(False))

        # Set mode "Enable All"
        set_user_mode(True)

        for user in disabled_users:
            # Disable user
            toggle_user(user, False)


def addgrouptojail(bdir, group_id, user, config):
    bdir = cagefslib.stripslash(bdir)
    try:
        gr = grp.getgrgid(group_id)
    except:
        secureio.logging('Warning: getgrgid() failed for group id '+str(group_id)+" skipping...", SILENT, 1)
        return 0
    if (not cagefslib.test_group_exist(gr.gr_name, bdir+'/etc/group')):
        _file = bdir+'/etc/group'
        secureio.logging('adding group '+gr[0]+' to '+_file, SILENT, config['verbose'])
        try:
            tmp = gr[0]+':x:'+str(gr[2])+':'
            if (type(user)==str and len(user)>0):
                tmp = tmp + user
            tmp = tmp + '\n'
            fd = open(_file, 'a')
            fd.write(tmp)
            fd.close()
        except IOError:
            secureio.logging('ERROR: failed to write group '+gr[0]+' to '+_file, SILENT, 1)
            return 0
    return 1


def addusertogroupinjail(bdir, group_id, user, config):
    ret = addgrouptojail(bdir, group_id, user, config)
    if (ret == 0):
        return 0
    try:
        _file = bdir+'/etc/group'
        fd = open(_file, 'r+')
        line = fd.readline()
        while (len(line)>0):
            splitted = line.split(':')
            if (len(splitted)==4 and int(splitted[2]) == group_id):
                users = splitted[3][:-1].split(',')
                if (user in users):
                    fd.close()
                    return 1
                else :
                    secureio.logging('Adding user '+user+' to group '+splitted[0], SILENT, config['verbose'])
                    pos = fd.tell()
                    buf = fd.read()
                    fd.seek(pos-len(line))
                    if (len(users)==1 and users[0] == ''):
                        users = [user]
                    else:
                        users.append(user)
                    tmp = splitted[0]+':x:'+splitted[2]+':'
                    tmp2 = ','.join(users)
                    tmp += tmp2+'\n'+buf
                    fd.write(tmp)
                    fd.close()
                    return 1
            line = fd.readline()
    except IOError:
        secureio.logging('ERROR: failed to add user '+user+' to group '+str(group_id)+' in '+_file, SILENT, 1)
        return 0
    return 0


#Create .htaccess
def create_htaccess(path):
    file_name = path + "/.htaccess"
    try:
        _file = open(file_name, 'w')
        _file.write('#CageFS autogenerated file\n')
        _file.write('deny from all\n')
        _file.close()
        os.chmod(file_name, 0o644)
    except (IOError, OSError):
        pass


def remove_htaccess(path):
    file_name = path + "/.htaccess"
    try:
        os.remove(file_name)
    except (IOError, OSError):
        pass


domlogs_found = None

def copyetc(user, config, ignore_errors = False, recreate = False, passwd_only = False, custom_etc_files = None):
    global domlogs_found, SPECIAL_PATHS
    pw_line = secureio.clpwd.get_pw_by_name(user)
    prefix = get_user_prefix(user)
    etcskel = cagefslib.ETC_TEMPLATE_DIR + '/etc'
    bdir = BASEDIR + '/' + prefix + '/' + user + '/'
    etcuser = bdir + 'etc'

    if passwd_only:
        for cf in ['/passwd', '/shadow']:
            if cagefslib.copy_file(etcskel+cf, etcuser+cf, create_parent_dir = False) == 1:
                secureio.logging("Error copying "+etcskel+cf+' to '+etcuser+cf, SILENT, 1)
                if not ignore_errors:
                    sys.exit(2)
    else:
        if custom_etc_files == None:
            custom_etc_files = cagefslib.get_additional_etc_files_for_user(user, etcuser)
        if (config['init'] == 1) or (config['reinit'] == 1) or recreate:
            try:
                shutil.rmtree(etcuser, True)
                if cagefslib.copytree(etcskel, etcuser, True, skip_dst_files = custom_etc_files) == 1:
                    raise Exception('copytree() failed')
                if create_etc_alternatives(users = [user]):
                    raise Exception('Failed to setup cl-selector for user '+user)
            except Exception as e:
                secureio.logging("Error while copying "+etcskel+' to '+etcuser+': '+str(e), SILENT, 1)
                if not ignore_errors:
                    sys.exit(2)
        else:
            if (cagefslib.copytree(etcskel, etcuser, True, update = True, skip_dst_files = custom_etc_files) == 1) or \
                            create_etc_alternatives(users = [user]):
                secureio.logging("Error copying "+etcskel+' to '+etcuser, SILENT, 1)
                if not ignore_errors:
                    sys.exit(2)
        if domlogs_found is None:
            domlogs_found = os.path.isdir('/usr/local/apache/domlogs') and os.path.isdir('/etc/apache2/logs/domlogs')
            SPECIAL_PATHS.append('/apache2/')
        if domlogs_found:
            try:
                logs_dir_path = etcuser + '/apache2/logs'
                domlogs_path = etcuser + '/apache2/logs/domlogs'
                if not os.path.lexists(domlogs_path):
                    if not os.path.lexists(logs_dir_path):
                        mod_makedirs(logs_dir_path, 0o755)
                    relative_symlink('/usr/local/apache/domlogs', domlogs_path)
            except OSError as e:
                secureio.logging("Error while creating "+domlogs_path+' : '+str(e), SILENT, 1)
                if not ignore_errors:
                    sys.exit(2)

    # get all entries for users with uid
    pw_db = secureio.clpwd.get_pw_by_uid(pw_line.pw_uid)

    # get /etc/group database
    groups = grp.getgrall()

    # Process all users with uid
    for pw in pw_db:
        # add user to passwd file
        try:
            fd = open(etcuser + '/passwd', 'a')
            fd.write(pw.pw_name+':x:'+str(pw[2])+':'+str(pw[3])+':'+pw[4]+':'+pw[5]+':'+pw[6]+'\n')
            fd.close()
        except:
            secureio.logging('Error while adding user '+pw.pw_name+' to passwd file', SILENT, 1)
            if not ignore_errors:
                sys.exit(2)

        # lookup the primary group and make sure it also exists in the jail
        if not addgrouptojail(bdir, pw[3], None, config):
            if not ignore_errors:
                sys.exit(2)

        # look up all other groups
        for gr in groups:
            if (pw.pw_name in gr.gr_mem):
                ret = addusertogroupinjail(bdir, gr.gr_gid, pw.pw_name, config)
                if not ret:
                    if not ignore_errors:
                        sys.exit(2)

        cagefslib.add_user_to_shadow(etcuser, pw.pw_name, config['verbose'])


def remove_file_or_directory(path):
    try:
        sbuf = os.lstat(path)
    except (OSError, IOError):
        return
    if stat.S_ISDIR(sbuf.st_mode):
        try:
            shutil.rmtree(path, False)
            secureio.logging("Removed directory "+ path,SILENT,1)
        except (OSError, IOError, shutil.Error):
            secureio.logging('Error while removing directory '+ path,SILENT,1)
    else:
        try:
            os.unlink(path)
            secureio.logging('Removed file '+ path,SILENT,1)
        except (OSError, IOError):
            secureio.logging('Error while removing file '+ path,SILENT,1)


# etc_user_version -> files_to_delete
files_to_delete_cache = {}
SPECIAL_PATHS = ['/mail/', '/'+cagefslib.CL_ALT_NAME+'/', '/'+cagefslib.CL_PHP_DIR_NAME+'/',
                '/'+cagefslib.CL_ALT_NAME+'/']


def check_special_paths(path):
    for spec_path in SPECIAL_PATHS:
        if path.startswith(spec_path):
            return False
    return True


def clean_etc(user, userdir, etc_skel, config, etc_user_version, custom_etc_files = None):
    global files_to_delete_cache

    if cagefslib.custom_etc_present() or (etc_user_version not in files_to_delete_cache):
        # Get list of files in etc in userdir
        etc_user = {}
        cagefslib.add_tree_to_list(userdir+'/etc', etc_user, cut_path = userdir+'/etc')
        etc_user_list = list(etc_user)
        etc_user_list.sort()
        files_to_delete = compare_lists(etc_user_list, etc_skel)
        files_to_delete_cache[etc_user_version] = files_to_delete
    else:
        files_to_delete = files_to_delete_cache[etc_user_version]

    if custom_etc_files is None:
        custom_etc_files = cagefslib.get_additional_etc_files_for_user(user, userdir+'/etc')

    for _file in files_to_delete:
        file2 = cagefslib.addslash(_file)
        path = userdir + '/etc' + _file
        if path not in custom_etc_files and check_special_paths(file2):
            if config['dont-clean'] == 1:
                secureio.logging("Skipping "+ path, SILENT, config['verbose'])
                continue
            remove_file_or_directory(path)


def get_all_users_from_passwd():
    # get all users from /etc/passwd
    return list(secureio.clpwd.get_user_dict())


def update_etc(config, users=None, ignore_errors=True, all_users=False):
    secureio.logging("Updating users ..." , SILENT, 1)
    users = get_cagefs_users(users, all_users)

    # Get list of users whose passwd entry has been changed
    modified_users = get_modified_users()

    # Get version of etc skeleton
    etc_skel_version = get_etc_version(cagefslib.ETC_TEMPLATE_DIR+'/etc')

    cagefslib.make_dir(BASEDIR, 0o751, allow_symlink = True)
    if is_running_without_lve():
        cagefslib.make_dir(BASEDIR_UID, 0o751, allow_symlink=True)

    etc_skel = None

    for user in users:
        prefix = get_user_prefix(user)
        prefixdir = BASEDIR + '/' + prefix
        dirname = '/' + prefix + '/' + user
        userdir = BASEDIR + dirname
        user_etc_path = userdir + '/etc'

        if cagefslib.make_dir(prefixdir, 0o751):
            # Error
            continue
        if cagefslib.make_dir(userdir, 0o751):
            # Error
            continue

        custom_etc_files = cagefslib.get_additional_etc_files_for_user(user, user_etc_path)
        custom_etc_files2 = cagefslib.cut_path(custom_etc_files, user_etc_path)

        # Get version of etc of user
        etc_user_version = get_etc_version(user_etc_path)

        if config['force-update-etc'] or (etc_skel_version > etc_user_version):
            secureio.logging("Updating user " + user + " ...", SILENT, 1)

            if (not os.path.isfile(userdir+'/etc/passwd')) or (config['init'] == 1) or (config['reinit'] == 1):
                copyetc(user, config, ignore_errors, custom_etc_files = custom_etc_files)
            else:
                copyetc(user, config, ignore_errors, custom_etc_files = custom_etc_files)
                if etc_skel == None:
                    # Get list of files in etc template
                    etc_skel = {}
                    cagefslib.add_tree_to_list(cagefslib.ETC_TEMPLATE_DIR+'/etc', etc_skel, cut_path = cagefslib.ETC_TEMPLATE_DIR+'/etc')
                clean_etc(user, userdir, etc_skel, config, etc_user_version, custom_etc_files = custom_etc_files)
        else:
            message_printed = False
            if user in modified_users:
                secureio.logging("Updating user " + user + " ...", SILENT, 1)
                message_printed = True
                copyetc(user, config, ignore_errors, passwd_only = True)

            custom_files_to_delete = cagefslib.get_custom_etc_files_to_delete(user, custom_etc_files2)
            if custom_files_to_delete:
                if not message_printed:
                    secureio.logging("Updating user " + user + " ...", SILENT, 1)
                if etc_skel == None:
                    # Get list of files in etc template
                    etc_skel = {}
                    cagefslib.add_tree_to_list(cagefslib.ETC_TEMPLATE_DIR+'/etc', etc_skel, cut_path = cagefslib.ETC_TEMPLATE_DIR+'/etc')
                copyetc_needed = False
                for path in custom_files_to_delete:
                    if path not in etc_skel:
                        fullpath = user_etc_path + path
                        if config['dont-clean'] == 1:
                            secureio.logging("Skipping "+ fullpath, SILENT, config['verbose'])
                            continue
                        remove_file_or_directory(fullpath)
                    else:
                        copyetc_needed = True
                if copyetc_needed:
                    copyetc(user, config, ignore_errors, custom_etc_files = custom_etc_files)

        cagefslib.update_custom_etc_files_for_user(user, user_etc_path)
        cagefslib.save_custom_etc_log(user, custom_etc_files2)
        cagefslib.create_utmp_for_user(user, exit_on_error=False)


def enable_cagefs_for_users_with_duplicate_uids(enabled_users = None):
    if enabled_users is None:
        enabled_users = filter_users(get_list_of_users(True))
    for user in enabled_users:
        toggle_user(user, True)


def update_users_status(disable_all=False, users=None, status=None, fix_owner=False, old_enabled_users=None):
    if disable_all:
        users = get_all_users_from_passwd()
        cagefslib.update_status(users, False)
    elif users is not None and status is not None:
        if status:
            users = filter_users(users)
        cagefslib.update_status(users, status)
    else:
        enabled_users = filter_users(get_list_of_users(True))
        cagefslib.update_status(enabled_users, True, fix_owner=fix_owner)
        if fix_owner:
            # process users with duplicate uids also
            enable_cagefs_for_users_with_duplicate_uids(enabled_users)
        disabled_users = get_list_of_users(False)
        cagefslib.update_status(disabled_users, False, fix_owner=fix_owner)
    # for reinit case we do not want to reload php process
    # because reinit clear skeleton and we do not need this extra action
    if old_enabled_users is not None:
        # it is enough to know only list of enabled users before status change
        reload_php_for_users_with_changed_status(old_enabled_users)


def reload_php_for_users_with_changed_status(old_enabled_users):
    """
    Filter users and reload php process only if status was REALLY changed
    :param old_enabled_users: enabled users before any status change
    :return:
    """
    new_enabled_users = get_enabled_users()
    # old enabled users: u1, u2
    # we want to enable: u2, u3
    # php process will be reloaded only for u1, u3
    # the same with disable status
    users_to_kill_process = list(set(old_enabled_users).symmetric_difference(new_enabled_users))
    reload_php_for_users(users_to_kill_process)


def get_cagefs_users(users = None, all_users = False, raise_exception = False):
    if users is None:
        if all_users:
            users = get_all_users_from_passwd()
        else:
            # Get list of enabled users
            users = get_list_of_users(True, raise_exception)
    users = filter_users(users)
    return users


def create_utmp_for_all_users():
    """
    Create user's personal /home/user/.cagefs/var/run/cagefs/utmp
    file for all users
    For details see CAG-706
    """
    for user in get_cagefs_users(all_users=True):
        cagefslib.create_utmp_for_user(user, exit_on_error=False)


def reload_php_for_users(users = None, all_users = False):
    users = get_cagefs_users(users, all_users)
    for user in users:
        reload_processes('php', user)


def switch_symlink(dest_path, link_name, write_log = True, force = True):
    if force or not os.path.islink(link_name):
        try:
            os.unlink(link_name)
        except OSError as e:
            if e.errno == errno.ENOENT:  # No such file error
                logger.info(f'Symlink {link_name} does not exist')
            else:
                logger.error(f'Error: Unable to remove symlink {link_name}', exc_info=e)
        try:
            clcaptain.symlink(dest_path, link_name)
        except (OSError, ExternalProgramFailed) as e:
            msg = f'Error: failed to create symlink {link_name} to {dest_path} : {str(e).replace("Errno", "Err code")}'
            logger.error(msg, exc_info=e)
            if write_log:
                secureio.logging(msg, SILENT, 1)
            else:
                print(msg, file=sys.stderr)
            return True
    return False


use_selector = None

def selector_modules_must_be_used():
    """
    Return True if modules selected via PHP Selector (alt_php.ini) must be used always (never use modules selected in cPanel MultiPHP Manager)
    See CAG-511 for details
    """
    global use_selector
    if use_selector is None:
        syml_rules = clconfpars.load_once('/etc/cl.selector/symlinks.rules', ignore_errors = True)
        try:
            val = syml_rules['php.d.location']
        except KeyError:
            use_selector = False
            return False
        use_selector = val.lower() == 'selector'
    return use_selector


def multiphp_system_default_is_ea_php():
    """
    Return True when default system php version selected via MultiPHP Manager in cPanel WHM is ea-php (not alt-php)
    For details see CAG-774
    """
    if is_ea4_enabled():
        conf = read_cpanel_ea4_php_conf()
        if conf:
            try:
                return conf['default'].startswith('ea-php')
            except KeyError:
                pass
    return True


def switch_symlink_for_alt_php_ini(php_vers, homedir, write_log = True, force = True):
    """
    Switch symlink so it will point to directory with modules for alt-php
    For details see CAG-447
    Returns True if error has occured
    Should be called as user (not root)!
    :param php_vers: alt-php version selected for an user (for example 'native' or '5.6')
    :type php_vers: string
    :param force: recreate symlinks even when they exist
    :type force: bool
    """
    def switch_symlink_for_dir(php_dir):
        # create path to link, like /home/$USER/.cagefs/opt/alt/php55/link/conf
        link_path = os.path.join(homedir, '.cagefs/opt/alt', php_dir, 'link/conf')
        dir_path = os.path.dirname(link_path)
        if not os.path.lexists(dir_path):
            try:
                # os.makedirs(dir_path, 0700)
                clcaptain.mkdir(dir_path, 0o700, recursive=True)
            except (OSError, ExternalProgramFailed):
                pass
        selected_php_dir = 'php'+php_vers.replace('.','')
        if not selector_modules_must_be_used() and (selected_php_dir != php_dir or not multiphp_system_default_is_ea_php()):
            # path to default alt-php modules
            link_to = '/opt/alt/%s/etc/php.d' % php_dir
        else:
            # path to user's custom modules selected via CloudLinux PHP Selector - like /etc/cl.php.d/alt-php55
            link_to = os.path.join(cagefslib.ETC_CL_PHP_PATH, 'alt-'+php_dir)
        return switch_symlink(link_to, link_path, write_log, force)

    error = False
    # get dirnames of all alt-php dirs as list
    alt_php_dirs = cagefslib.get_alt_dirs()
    # switch symlinks for ALL alt-php versions
    for php_dir in alt_php_dirs:
        if switch_symlink_for_dir(php_dir):
            error = True
    return error


EA4_PHP_CONF = '/etc/cpanel/ea4/php.conf'
EA4_PHP_CONF_CACHE = None

def read_cpanel_ea4_php_conf():
    """
    Read /etc/cpanel/ea4/php.conf, return something like {'default': 'ea-php54', 'ea-php56': 'suphp', 'ea-php54': 'cgi', 'ea-php55': 'suphp'}
    Return None if error has occured
    """
    global EA4_PHP_CONF_CACHE
    if EA4_PHP_CONF_CACHE is None:
        try:
            f = open(EA4_PHP_CONF, 'r')
            # conf = {'default': 'ea-php54', 'ea-php56': 'suphp', 'ea-php54': 'cgi', 'ea-php55': 'suphp'}
            EA4_PHP_CONF_CACHE = yaml.load(f, yaml.SafeLoader)
            f.close()
        except (yaml.YAMLError, IOError):
            EA4_PHP_CONF_CACHE = {}
            try:
                f.close()
            except:
                pass
            return None
    return EA4_PHP_CONF_CACHE


SYMLINKS = {'%s/etc/cl.selector/ea-php' : ('php','/opt/cpanel/%s/root/usr/bin/php-cgi.cagefs'),
                        '%s/etc/cl.selector/ea-php-cli' : ('php-cli','/opt/cpanel/%s/root/usr/bin/php.cagefs'),
                        '%s/etc/cl.selector/ea-php.ini' : ('php.ini','/opt/cpanel/%s/root/etc/php.ini.cagefs'),
                        '%s/etc/cl.selector/ea-lsphp' : ('lsphp','/opt/cpanel/%s/root/usr/bin/lsphp.cagefs')}

def create_files_for_symlink_protection():
    """
    Configure symlink protection for symlinks created for integration with cPanel MultiPHP
    Return True if error has occured
    """
    if is_running_without_lve():
        return False
    if not is_ea4_enabled():
        return False
    try:
        linksafe_gid = grp.getgrnam('linksafe').gr_gid
    except KeyError:
        # linksafe group not found - symlink protection is not configured properly
        return False
    conf = read_cpanel_ea4_php_conf()
    if not conf:
        return False
    error = False
    umask_old = os.umask(0o22)
    for alias in conf:
        if alias.startswith('ea-php'):
            for path in SYMLINKS.values():
                try:
                    file_path = path[1] % alias
                    f = open(file_path, 'w')
                    f.write('CageFS integration for cPanel MultiPHP\n')
                    f.close()
                    os.chown(file_path, 0, linksafe_gid)
                except OSError as e:
                    secureio.print_error('failed to create file', file_path, ':', str(e))
                    error = True
    os.umask(umask_old)
    return error


def switch_symlink_for_cpanel_multi_php(pw, selected_php_vers, write_log = True, force = True):
    """
    Switch symlinks that are used for integration with cPanel MultiPHP:
    when selected_php_vers == alt-php version, then create symlinks like /etc/cl.selector/ea-php -> php;
    when selected_php_vers == native version, then create symlinks like /etc/cl.selector/ea-php -> /opt/cpanel/ea-phpXX/root/usr/bin/php.cagefs;
    For details please see CAG-445
    Return True if error has occured
    :param pw: password file entry for an user
    :type pw: as defined in standard pwd module
    :param selected_php_vers: alt-php version selected for an user (for example 'native' or '5.6')
    :type selected_php_vers: string
    :param write_log: write error messages to log or not
    :type write_log: bool
    :param force: recreate symlinks even when they exist
    :type force: bool
    """
    def get_default_native_version_selected():
        """
        Return string like ea-phpXX when symlinks have been created already and native version is selected
        Return None otherwise
        """
        try:
            link_to = os.readlink('%s/etc/cl.selector/ea-php.ini' % user_cagefs_path)
        except OSError:
            return None
        if link_to.startswith('/opt/cpanel/ea-php'):
            return link_to.split('/')[3]
        return None

    if not is_ea4_enabled():
        return False
    conf = read_cpanel_ea4_php_conf()
    if not conf:
        return False
    try:
        # get default system php version selected via MultiPHP Manager in cPanel WHM
        default_php = conf['default']
    except KeyError:
        return True
    # LVEMAN-1170: do not configure PHP Selector when system default version is alt-php
    if not default_php.startswith('ea-php'):
        return False
    username = pw.pw_name
    user_cagefs_path = BASEDIR + '/' + get_user_prefix(username) + '/' + username
    if not force:
        old_eaphp_default = get_default_native_version_selected()
        if old_eaphp_default is not None:
            selected_php_vers = 'native'
            if old_eaphp_default != default_php:
                # we should recreate symlinks when native version is selected actually
                # and when default ea-php version is changed via cPanel MultiPHP
                force = True
    error = False
    for sympath, link_to in SYMLINKS.items():
        link_path = sympath % user_cagefs_path
        if selected_php_vers == 'native':
            error = switch_symlink(link_to[1] % default_php, link_path, write_log, force) or error
        else:
            error = switch_symlink(link_to[0], link_path, write_log, force) or error
    return error


def configure_alt_php(pw, php_vers, write_log = True, drop_perm = True, force = True, configure_multiphp = True):
    """
    Create .cagefs directory in home directory of an user (if that dir does not exist),
    and create symlinks to modules for alt-php
    For details see CAG-447
    Also switch symlinks that are used for integration with cPanel MultiPHP
    For details please see CAG-445
    drop_perm should be True when called as root, otherwise drop_perm should be False
    Returns True if error has occured
    :param pw: password file entry for an user
    :type pw: as defined in standard pwd module
    :param php_vers: alt-php version selected for an user (for example 'native' or '5.6')
    :type php_vers: string
    :param write_log: write error messages to log or not
    :type write_log: bool
    :param force: recreate symlinks even when they exist
    :type force: bool
    """
    # create /home/user/.cagefs directory if it does not exist, set permissions/owner otherwise
    real_homepath = os.path.realpath(pw.pw_dir)
    path = os.path.join(pw.pw_dir, '.cagefs')
    if drop_perm:
        if cagefslib.make_userdir(path, 0o771, pw.pw_uid, pw.pw_gid, real_homepath):
            return True
    elif not os.path.lexists(path):
        try:
            clcaptain.mkdir(path, 0o771)
        except (OSError, ExternalProgramFailed) as e:
            msg = f'Error: failed to create directory {path} : {str(e).replace("Errno", "Err code")}'
            logger.error(msg, exc_info=e)
            print(msg, file=sys.stderr)
            return True
    if drop_perm:
        # drop privileges (switch to user)
        secureio.set_user_perm(pw.pw_uid, pw.pw_gid)
    error = switch_symlink_for_alt_php_ini(php_vers, pw.pw_dir, write_log, force)
    if configure_multiphp:
        error = switch_symlink_for_cpanel_multi_php(pw, php_vers, write_log, force) or error
    if drop_perm:
        # restore root privileges
        secureio.set_root_perm()
    return error


def php_version_is_removed(php_vers):
    if php_vers == 'native':
        return False
    return php_vers not in cagefslib.get_alt_versions()


def php_version_is_disabled(php_vers, cl_alt_def_php_state):
    return (cl_alt_def_php_state != None) and (php_vers in cl_alt_def_php_state) and (not cl_alt_def_php_state[php_vers])


# Returns True if error has occurred
def create_etc_alternatives(users=None, all_users=False, repair_symlinks=False,
                            reset_modules_to_default=False, rebuild_alt_php_ini=False):
    users = get_cagefs_users(users, all_users)

    error = False

    umask_saved = os.umask(0)

    for user in users:
        pw = secureio.clpwd.get_pw_by_name(user)
        prefix = get_user_prefix(user)
        dirname = '/'+prefix+'/'+user
        userdir = BASEDIR + dirname
        # userdir = /var/cagefs/<prefix>/<user>
        if os.path.isdir(userdir):
            LINK_DIR = cagefslib.ETC_CL_ALT_PATH
            link_dir = userdir + LINK_DIR

            # cannot drop permissions because root only can write to /var/cagefs/prefix/user/etc
            if cagefslib.make_dir(link_dir, 0o755):
                continue
            cagefslib.set_owner(link_dir, pw.pw_uid, pw.pw_gid)

            # cannot drop permissions because root only can write to /var/cagefs/prefix/user/etc
            user_php_dir = userdir + cagefslib.ETC_CL_PHP_PATH
            # user_php_dir = /var/cagefs/prefix/user/etc/cl.php.d
            if cagefslib.make_dir(user_php_dir, 0o755):
                continue
            cagefslib.set_owner(user_php_dir, pw.pw_uid, pw.pw_gid)

            cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other = cagefslib.read_cl_alt_defaults()  # @UnusedVariable
            def_vers, php_modules, php_state_ignored, other_ignored = cagefslib.read_cl_alt_backup_as_user(pw.pw_dir, pw.pw_uid, pw.pw_gid)  # @UnusedVariable
            def_vers_old = def_vers

            # Backup is absent or backup exists and php version (from backup) is disabled or removed?
            if (def_vers == None) or ((def_vers != None) and
                            (php_version_is_removed(def_vers) or php_version_is_disabled(def_vers, cl_alt_def_php_state))):
                # global defaults are absent or global defaults exist and default php version is disabled or removed?
                if (cl_alt_def_vers == None) or ((cl_alt_def_vers != None) and
                        (php_version_is_removed(cl_alt_def_vers) or php_version_is_disabled(cl_alt_def_vers, cl_alt_def_php_state))):
                    def_vers = 'native'
                else:
                    def_vers = cl_alt_def_vers
            changed = False

            for alias in cagefslib.orig_binaries:
                # orig_path = cagefslib.orig_binaries[alias]
                filename = alias

                if def_vers == 'native':
                    LINK_TO = cagefslib.get_usr_selector_path(alias)
                else:
                    alt_path = cagefslib.get_alt_conf(def_vers, alias)
                    if alt_path != None:
                        LINK_TO = alt_path
                    else:
                        LINK_TO = cagefslib.get_usr_selector_path(alias)

                LINK_NAME = cagefslib.ETC_CL_ALT_PATH+'/'+filename

                link_name = userdir + LINK_NAME

                if not os.path.islink(link_name):
                    cagefslib.remove_file_or_dir(link_name)

                    try:
                        os.symlink(LINK_TO, link_name)
                        if alias == 'php.ini':
                            changed = True
                    except OSError as e:
                        msg = f'Error: failed to create symlink {link_name} : {str(e).replace("Errno", "Err code")}'
                        logger.error(msg, exc_info=e)
                        secureio.logging(msg, SILENT, 1)
                        error = True

                elif repair_symlinks:
                    try:
                        link_to = os.readlink(link_name)
                    except OSError as e:
                        msg = f'Error: failed to read symlink {link_name} : {str(e).replace("Errno", "Err code")}'
                        logger.error(msg, exc_info=e)
                        secureio.logging(msg, SILENT, 1)
                        error = True
                        continue

                    if link_to.startswith('/opt/alt/php'):
                        link_to_dirname, link_to_filename = os.path.split(link_to)
                        _dirname, _filename = os.path.split(LINK_TO)

                        repaired = None
                        if link_to_dirname == _dirname and link_to_filename != _filename:
                            repaired = LINK_TO

                        if repaired != None:
                            try:
                                os.unlink(link_name)
                                os.symlink(repaired, link_name)
                            except (OSError, IOError) as e:
                                msg = f'Error: failed to create symlink {link_name} : {str(e).replace("Errno", "Err code")}'
                                logger.error(msg, exc_info=e)
                                secureio.logging(msg, SILENT, 1)
                                error = True

            cagefslib.select_default_php_modules(user_php_dir,
                                                 pw.pw_dir,
                                                 pw.pw_uid,
                                                 pw.pw_gid,
                                                 def_vers,
                                                 cl_alt_def_modules,
                                                 php_modules,
                                                 changed,
                                                 def_vers_old,
                                                 reset_modules_to_default,
                                                 rebuild_alt_php_ini,
                                                 user)

            if cagefs_da_lib.create_php_ini_for_DA(userdir, user, def_vers, pw.pw_uid, pw.pw_gid):
                error = True

            # Switch symlink so it will point to directory with modules for alt-php
            # For details see CAG-447
            if configure_alt_php(pw, def_vers, force=changed):
                error = True

    os.umask(umask_saved)

    return error


def reset_modules_to_default(users = None):
    if not users:
        users = None
    create_etc_alternatives(users = users, all_users = True, reset_modules_to_default = True)
    reload_php_for_users(users = users)


def rebuild_alt_php_ini(users = None):
    if not users:
        users = None
    create_etc_alternatives(users = users, all_users = True, rebuild_alt_php_ini = True)
    reload_php_for_users(users = users)


def check_php_ini_options(users=None):
    if not users:
        users = None
    cagefslib.validate_alt_php_ini = True
    create_etc_alternatives(users=users, all_users=True, rebuild_alt_php_ini=True)
    reload_php_for_users(users=users)


def add_new_line(lines):
    # file is not empty and last line in the file does not end with new line symbol ?
    if lines and lines[0] != '' and lines[-1][-1] != '\n':
        lines[-1] += '\n'
        return True
    return False


def write_cagefs_mp(new_lines):
    # write cagefs.mp file if needed
    if new_lines:
        lines = cagefslib.read_file(ETC_MPFILE)
        add_new_line(lines)
        lines.extend(new_lines)
        cagefslib.write_file(ETC_MPFILE, lines)
        os.chmod(ETC_MPFILE, 0o600)
        create_remount_flag()


def add_mounts_for_ea_php_sessions():
    """
    Add mount points like "@/var/cpanel/php/sessions/ea-php56,700" to /etc/cagefs/cagefs.mp file
    """
    if os.path.isdir('/var/cpanel/php/sessions') and os.path.isfile(ETC_MPFILE):
        mp_config = MountpointConfig(
            skip_errors=True,
            skip_cpanel_check=True,
            ignore_cache=True,
        )
        personal_mounts = mp_config.personal_mounts
        new_lines = []
        # get dirnames of all alt-php dirs as list
        php_dirs = glob.glob('/var/cpanel/php/sessions/ea*')
        for php_dir in php_dirs:
            if php_dir not in personal_mounts and os.path.isdir(php_dir):
                mount_str = '@%s,700\n' % php_dir
                new_lines.append(mount_str)
        write_cagefs_mp(new_lines)


def add_mounts_for_php_selector():
    """
    Add mount points for php selector and alt-php to /etc/cagefs/cagefs.mp file
    """
    mp_config = MountpointConfig(
        skip_errors=True,
        skip_cpanel_check=True,
        ignore_cache = True,
    )
    cagefslib.mounts = mp_config.common_mounts
    personal_mounts = mp_config.personal_mounts
    new_lines = []
    # Add '/opt/alt' to cagefs.mp if needed
    if ('/opt/alt\n' not in cagefslib.mounts) and ('/opt\n' not in cagefslib.mounts):
        cagefslib.mounts.append('/opt/alt\n')
        new_lines.append('/opt/alt\n')
    # get dirnames of all alt-php dirs as list
    alt_php_dirs = cagefslib.get_alt_dirs()
    for php_dir in alt_php_dirs:
        # something like /opt/alt/php55/link
        mount_path = '/opt/alt/%s/link' % php_dir
        mount_str = '@/opt/alt/%s/link,700\n' % php_dir
        if mount_path not in personal_mounts:
            new_lines.append(mount_str)
        mount_path = '/opt/alt/%s/var/lib/php/session' % php_dir
        mount_str = '@/opt/alt/%s/var/lib/php/session,700\n' % php_dir
        if mount_path not in personal_mounts:
            new_lines.append(mount_str)
        # add php-newrelic logs path, CAG-1118
        mount_path = '/var/log/alt-%s-newrelic' % php_dir
        mount_str = '@/var/log/alt-%s-newrelic,700\n' % php_dir
        if mount_path not in personal_mounts:
            new_lines.append(mount_str)
    write_cagefs_mp(new_lines)


def remove_mounts_for_php_selector():
    """
    Remove mount points for uninstalled alt-php versions from /etc/cagefs/cagefs.mp file
    """
    php_alt_dirs = cagefslib.get_alt_dirs()
    needed_mounts = set(['/opt/alt/%s/link' % php_dir for php_dir in php_alt_dirs])
    needed_mounts.update(['/opt/alt/%s/var/lib/php/session' % php_dir for php_dir in php_alt_dirs])
    needed_mounts.update(['/var/log/alt-%s-newrelic' % php_dir for php_dir in php_alt_dirs])
    lines = cagefslib.read_file(ETC_MPFILE)
    new_lines = []
    pattern = re.compile(r'@(/opt/alt/php\d\d/link|/opt/alt/php\d\d/var/lib/php/session|/var/log/alt-php\d\d-newrelic),')
    changed = False
    for line in lines:
        m = pattern.match(line)
        if m:
            if m.group(1) in needed_mounts:
                new_lines.append(line)
            else:
                changed = True
        else:
            new_lines.append(line)
    if changed:
        cagefslib.write_file(ETC_MPFILE, new_lines)
        os.chmod(ETC_MPFILE, 0o600)
        create_remount_flag()


def add_mount_for_php_apm():
    """
    Adds mount point for default location of PHP APM DB
    :return:
    """
    if not os.path.isfile(ETC_MPFILE):
        # do nothing, if cagefs.mp is absent
        return

    path_to_add = '/var/php/apm/db'
    personal_mounts = []
    mp_config = MountpointConfig(
        skip_errors=True,
        skip_cpanel_check=True,
    )
    mounts = mp_config.common_mounts
    personal_mounts = mp_config.personal_mounts
    if path_to_add not in personal_mounts and path_to_add not in mounts:
        # Path not found, add it
        lines = cagefslib.read_file(ETC_MPFILE)
        add_new_line(lines)
        lines.append('@%s,777\n' % path_to_add)
        cagefslib.write_file(ETC_MPFILE, lines)
        os.chmod(ETC_MPFILE, 0o600)
        create_remount_flag()


def create_dirs_for_symlink_protection():
    """
    Cretate /etc/cl.php.d/alt-phpNN directories for all alt-php versions (in real filesystem) with group owner 'linksafe'
    for details see CAG-532, CAG-454
    Return True if error has occured
    """
    if is_running_without_lve():
        return False
    try:
        linksafe_gid = grp.getgrnam('linksafe').gr_gid
    except KeyError:
        # linksafe group not found - symlink protection is not configured properly
        return False
    alt_php_dirs = cagefslib.get_alt_dirs()
    error = False
    for php_dir in alt_php_dirs:
        etc_php_dir = '/etc/cl.php.d/alt-%s' % php_dir
        alt_php_ini = '/etc/cl.php.d/alt-%s/alt_php.ini' % php_dir
        try:
            if not os.path.exists(etc_php_dir):
                mod_makedirs(etc_php_dir, 0o755)
            os.chown(etc_php_dir, 0, linksafe_gid)
            open(alt_php_ini, 'w').close()
            os.chown(alt_php_ini, 0, linksafe_gid)
        except OSError as e:
            secureio.print_error('failed to configure linksafe', ':', str(e))
            error = True
    return error


def clean_dir_recursive(cagefs_dir, orig_dir):
    """
    Delete from cagefs_dir files/dirs that do not exist in orig_dir
    :param cagefs_dir: path to dir in CageFS (dir to delete files from)
    :type cagefs_dir: string
    :param orig_dir: path to original dir
    :type orig_dir: string
    """
    for file_name in os.listdir(cagefs_dir):
        if file_name.endswith('.cagefs'):
            continue
        orig_path = os.path.join(orig_dir, file_name)
        cagefs_path = os.path.join(cagefs_dir, file_name)
        if not os.path.lexists(orig_path):
            remove_file_or_directory(cagefs_path)
        elif os.path.isdir(cagefs_path) and not os.path.islink(cagefs_path):   # cagefs_path is a directory ?
            clean_dir_recursive(cagefs_path, orig_path)


def setup_cpanel_multiphp(do_mount=False, config=None):
    """
    Setup CageFS for integration with cPanel MultiPHP
    For details please see CAG-445
    :param do_mount: when True, do mounting; when False, do copying files to CageFS
    :type do_mount: bool
    """
    if not is_ea4_enabled():
        # EasyApache4 is not setup
        return
    SYMLINK_NAMES = {'php-cgi':'/etc/cl.selector/ea-php',
                                     'php':'/etc/cl.selector/ea-php-cli',
                                     'php.ini':'/etc/cl.selector/ea-php.ini',
                                     'lsphp':'/etc/cl.selector/ea-lsphp'}
    CAGEFS_PHP_BASEDIR = '/usr/share/cagefs/.cpanel.multiphp'
    DIRS_TO_MOUNT = ('/opt/cpanel/%s/root/usr/bin', '/opt/cpanel/%s/root/etc')
    conf = read_cpanel_ea4_php_conf()
    if not conf:
        return
    try:
        # get default system php version selected via MultiPHP Manager in cPanel WHM
        default_php = conf['default']
    except KeyError:
        return
    for alias in conf:
        if alias.startswith('ea-php'):
            for path in DIRS_TO_MOUNT:
                optdir = path % alias
                cagefs_optdir = '%s%s' % (SKELETON, optdir)
                cagefs_dir = '%s%s' % (CAGEFS_PHP_BASEDIR, optdir)
                if os.path.isdir(optdir):
                    if not os.path.isdir(cagefs_dir):
                        create_remount_flag()
                        if cagefslib.make_dir(cagefs_dir, 0o755):
                            # failed to create directory. so, we remove need.remount flag because we should not do remount in such case
                            remove_remount_flag()
                            sys.exit(1)
                    if do_mount:
                        ret = subprocess.call([MOUNT, "-n", "-o", "nosuid", "--rbind", cagefs_dir, cagefs_optdir])
                        if ret == 0:
                            ret = subprocess.call([MOUNT, "-n", "-o", "remount,ro,nosuid,bind", cagefs_dir, cagefs_optdir])
                        if ret != 0:
                            secureio.print_error("failed to mount", cagefs_dir)
                            sys.exit(1)
                    else:
                        # copy php files to cagefs
                        for file_name in os.listdir(optdir):
                            if file_name.endswith('.cagefs'):
                                continue
                            orig_path = os.path.join(optdir, file_name)
                            dest_file = os.path.join(cagefs_dir, file_name)
                            if os.path.isdir(orig_path):
                                if cagefslib.copytree(orig_path, dest_file, update=True):
                                    secureio.logging('Error copying '+orig_path+' to '+dest_file, SILENT, 1)
                                    sys.exit(1)
                                continue
                            if alias == default_php and file_name in SYMLINK_NAMES:
                                if switch_symlink(SYMLINK_NAMES[file_name], dest_file):
                                    cagefslib.kill_php(file_name)
                                    if switch_symlink(SYMLINK_NAMES[file_name], dest_file):
                                        sys.exit(1)
                                dest_file += '.cagefs'
                            if cagefslib.copy_file(orig_path, dest_file, create_parent_dir=False, update=True):
                                # Error has occured - kill php processes and try copy again
                                cagefslib.kill_php(file_name)
                                if cagefslib.copy_file(orig_path, dest_file, create_parent_dir=False):
                                    secureio.logging('Error copying '+orig_path+' to '+dest_file, SILENT, 1)
                                    sys.exit(1)
    if not do_mount:
        # delete old (unneeded) files
        clean_dir_recursive(CAGEFS_PHP_BASEDIR+'/opt/cpanel', '/opt/cpanel')
    # Compare php.conf files and update php.conf file in CageFS when needed
    if (config is not None) and (not do_mount):
        php_conf = cagefslib.ETC_TEMPLATE_DIR + EA4_PHP_CONF
        if not os.path.isfile(php_conf) or cagefslib.is_update_needed(EA4_PHP_CONF, php_conf, use_cache=False):
            update_etc_only(config)


def setup_cl_alt(config, options=None):
    check_skeleton()

    error = create_files_for_symlink_protection()
    error = create_dirs_for_symlink_protection() or error

    cagefs_da_lib.create_symlink_to_php_ini_for_DA(SKELETON)
    cagefs_da_lib.configure_selector_for_directadmin()

    # copy lsphp binary if needed
    if not os.path.isfile('/usr/local/bin/lsphp'):
        if os.path.isfile('/usr/local/lsws/fcgi-bin/lsphp5'):
            lsphp_path = os.path.realpath('/usr/local/lsws/fcgi-bin/lsphp5')
            cagefslib.copy_file(lsphp_path, '/usr/local/bin/lsphp')

    # alt-php versions are installed ?
    alt = cagefslib.get_alt_versions()
    if alt:
        from cagefsreconfigure import litespeed_configure_selector, replace_alt_settings
        litespeed_configure_selector()
        replace_alt_settings(options)

    # Create /opt/alt directory if needed
    if not os.path.lexists('/opt/alt'):
        try:
            mod_makedirs('/opt/alt', 0o755)
            create_remount_flag()
        except (OSError, IOError):
            secureio.print_error('failed to create directory /opt/alt')
            sys.exit(1)

    add_mounts_for_php_selector()
    add_mounts_for_ea_php_sessions()

    # Read paths to php binaries from config file
    cagefslib.read_native_conf()

    # Create directories of CL Selector in etc and convert modules from symlinks to single ini file (if needed)
    create_etc_alternatives(all_users = True, repair_symlinks = True)

    # Update native php files in etc directory
    if cagefslib.is_etc_in_native_conf():
        error = update_etc_only(config, print_selector_errors = True) or error

    # Update native php files in cagefs-skeleton
    for alias, orig_path in cagefslib.orig_binaries.items():
        orig_path2 = os.path.realpath(orig_path)
        if (not orig_path.startswith('/etc/') and os.path.islink(orig_path) and
                                not cagefslib.path_is_mounted(orig_path) and cagefslib.copy_file(orig_path, SKELETON + orig_path) == 1):
            error = True
        if is_ea4_enabled() and alias in ('php', 'php-cli', 'lsphp', 'php.ini'):
            # Update php file inside cagefs without replacing it with symlink
            if (not orig_path2.startswith('/etc/') and os.path.exists(orig_path2) and
                            not cagefslib.path_is_mounted(orig_path2) and cagefslib.copy_file(orig_path2, SKELETON + orig_path2) == 1):
                error = True
            # Create stubs for php files to make selectorctl, cl-selector work as before
            if cagefslib.create_php_stub(alias):
                error = True
            continue
        if not orig_path2.startswith('/etc/'):
            if cagefslib.path_is_mounted(orig_path2):
                secureio.print_error(orig_path2, 'is mounted to CageFS. CloudLinux Selector will not be available.')
                error = True
            elif not cagefslib.move_to_alternatives(orig_path2, etc = False) and cagefslib.is_mandatory(alias):
                secureio.print_error("CloudLinux Selector setup error for path:", orig_path)
                error = True

    # alt-php versions are installed ?
    if alt and (not error):
        import cagefs_ispmanager_lib
        cagefs_ispmanager_lib.configure_selector_for_ispmanager()

    setup_cpanel_multiphp(do_mount=False, config=config)

    if not config['skip-php-reload']:
        reload_php_for_users()

    if not error:
        print("CloudLinux Selector setup: successful")


# Checks symlinks in /etc/cl.selector for users and replaces broken symlinks (i.e. symlinks
# to non-existent alternatives) with symlinks to native (original) binaries.
# when force == True, replaces ALL symlinks with symlinks to native (original) binaries
# (i.e. reset "alternatives" settings for users to "native")
# Returns True if error has occured
def remove_etc_alternatives(users = None, all_users = False, force = False):
    users = get_cagefs_users(users, all_users)

    cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other = cagefslib.read_cl_alt_defaults()
    alt_versions = cagefslib.get_alt_versions()

    alt_paths = cagefslib.get_alt_paths(cl_alt_def_php_state)

    if cl_alt_def_vers == None:
        # global defaults are not set (link files to native binary)
        dest_vers = 'native'
    elif cl_alt_def_vers == 'native':
        # global default version is native (link files to native binary)
        dest_vers = 'native'
    elif (cl_alt_def_vers not in alt_versions) or ((cl_alt_def_vers in cl_alt_def_php_state) and (not cl_alt_def_php_state[cl_alt_def_vers])):
        # default version has been removed or disabled
        # link files to native binary
        # set global default version to native
        dest_vers = 'native'
        # write global defaults (owner - root)
        cagefslib.write_cl_alt_to_backup(None, dest_vers, cl_alt_def_modules, 0, 0, cl_alt_def_php_state, cl_alt_def_other)
    else:
        # link files to global default binary (global defaults are set already)
        dest_vers = cl_alt_def_vers

    native_php_is_disabled = False
    if (cl_alt_def_php_state != None) and ('native' in cl_alt_def_php_state) and (not cl_alt_def_php_state['native']):
        native_php_is_disabled = True

    error = False

    for user in users:
        pw = secureio.clpwd.get_pw_by_name(user)
        prefix = get_user_prefix(user)
        dirname = '/'+prefix+'/'+user
        userdir = BASEDIR + dirname

        LINK_DIR = cagefslib.ETC_CL_ALT_PATH
        link_dir = userdir + LINK_DIR

        if os.path.isdir(link_dir):

            changed = False

            # for filename in os.listdir(link_dir):
            for filename in cagefslib.get_alt_aliases(dest_vers):
                native_path = cagefslib.get_usr_selector_path(filename)

                if dest_vers == 'native':
                    dest_path = native_path
                else:
                    dest_path = cagefslib.get_alt_conf(dest_vers, filename)
                    if dest_path == None:
                        continue

                link_name = link_dir+'/'+filename

                if not os.path.islink(link_name):
                    if filename == 'php.ini':
                        changed = True
                    cagefslib.remove_file_or_dir(link_name)
                    try:
                        os.symlink(dest_path, link_name)
                    except (OSError, IOError):
                        secureio.logging('Error while creating symlink ' + link_name, SILENT, 1)
                        error = True
                else:
                    try:
                        link_to = os.readlink(link_name)
                    except (OSError, IOError):
                        secureio.logging('Error: failed to read symlink ' + link_name, SILENT, 1)
                        error = True
                        continue

                    # Switch symlink if native php is disabled
                    if (link_to == native_path) and (dest_vers != 'native') and (native_php_is_disabled or force):
                        if filename == 'php.ini':
                            changed = True
                        if switch_symlink(dest_path, link_name):
                            error = True
                    # Path to alternative (link_to) must exist in real system. So path to skeleton is not added to os.path.lexists(link_to)
                    # Skeleton may be unmounted, so path SKELETON+link_to may not exist
                    # Switch symlink if alt-php version has been removed (or disabled)
                    elif (link_to not in (native_path, dest_path)) and ( force or (link_to not in alt_paths) or (not os.path.lexists(link_to)) ):
                        if filename == 'php.ini':
                            changed = True
                        if switch_symlink(dest_path, link_name):
                            error = True

            # Switch symlink so it will point to directory with modules for alt-php
            # For details see CAG-447
            if configure_alt_php(pw, dest_vers, force=changed):
                error = True

            if changed:
                reload_processes('php', user)

            # read user's backup
            def_vers, php_modules, php_state_ignored, other_ignored = cagefslib.read_cl_alt_backup_as_user(pw.pw_dir, pw.pw_uid, pw.pw_gid)  # @UnusedVariable
            if (def_vers == None or def_vers != dest_vers) and changed:
                # Save backup
                cagefslib.write_cl_alt_to_backup(pw.pw_dir, dest_vers, php_modules, pw.pw_uid, pw.pw_gid)

    return error


def kernel_is_supported():
    if is_running_without_lve():
        return True
    try:
        f = open('/proc/lve/list', 'r')
        line = f.readline()
        f.close()
        return bool(line)
    except IOError:
        return False


def check_kernel():
    if not kernel_is_supported():
        remove_log_file()
        secureio.logging('Error: current running kernel is NOT supported')
        sys.exit(1)


config_copy = {}


def do_profiling():
    update_cagefs(config_copy)


def run_toggle_plugin_utility():
    if not os.path.isfile(PLUGIN_STATE):
        return False
    try:
        ret = subprocess.call([PLUGIN_STATE, "--toggle-plugin"])
        if ret != 0:
            secureio.logging('Error: '+PLUGIN_STATE, SILENT, 1)
            return True
    except OSError:
        secureio.logging('Error: failed to run '+PLUGIN_STATE, SILENT, 1)
        return True

    return False


def toggle_plugin():
    run_toggle_plugin_utility()


def print_user_status(user):
    if cagefs_is_enabled():
        if user in get_list_of_users(True):
            print('Enabled')
            sys.exit(0)
    print('Disabled')
    sys.exit(1)


def print_cagefs_status():
    if cagefs_is_enabled():
        print('Enabled')
        sys.exit(0)
    print('Disabled')
    sys.exit(1)


def read_paths_from_file(filename):
    """ Read paths separated by commas from a file """
    cfg = configparser.ConfigParser(interpolation=None, strict=False)
    try:
        cfg.read(filename)
    except configparser.Error:
        return []
    return cagefslib.config_get_option_as_list(cfg, filename, 'paths')


def write_paths_to_file(filename, paths, comment = None):
    """ Write paths from list to a file """
    paths = remove_duplicates(paths)
    paths = remove_parent_dirs(paths)
    umask_saved = os.umask(0o77)
    try:
        f = open(filename, 'w')
        f.write("[%s]\n" % filename)
        if comment:
            f.write("comment=%s\n" % comment)
        f.write("paths=")
        for index in range(len(paths)):
            if index > 0:
                f.write(", " + paths[index])
            else:
                f.write(paths[index])
        f.write("\n")
        f.close()
    except IOError as e:
        secureio.print_error('Failed to write ' + filename + ' : ' + str(e))
    os.umask(umask_saved)


def update_list(config):
    """ Read paths from stdin and updates appropriate files in cagefs-skeleton """
    check_skeleton()

    if not os.path.isdir(cagefslib.ETC_TEMPLATE_DIR+'/etc'):
        secureio.print_error('skeleton of etc directory is not found')
        sys.exit(1)

    cagefslib.mounts = MountpointConfig().common_mounts

    files = sys.stdin.readlines()

    etc_found = False
    for filename in files:
        if filename.startswith('/etc/'):
            etc_found = True
            break

    if etc_found:
        # Create empty directory for template of etc directory
        shutil.rmtree(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', True)
        if not os.path.isdir(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc'):
            try:
                mod_makedirs(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', 0o755)
            except OSError:
                secureio.print_error('creating', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc')
                sys.exit(1)

        if cagefslib.copytree(cagefslib.ETC_TEMPLATE_DIR+'/etc', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', True) == 1:
            secureio.print_error('Error while creating skeleton of etc directory')
            sys.exit(1)

    etc_modified = False
    copied_files = []

    for filename in files:
        filename = filename.strip()
        if filename.startswith('/') and (os.path.isfile(filename) or os.path.islink(filename)):
            if filename.startswith('/etc/'):
                if not cagefslib.copy_file(filename, cagefslib.ETC_TEMPLATE_NEW_DIR+filename, create_parent_dir = True):
                    print(filename)
                    etc_modified = True
                    copied_files.append(filename)
            elif (not cagefslib.path_is_mounted(filename)):
                if not cagefslib.copy_file(filename, SKELETON+filename, create_parent_dir = True):
                    print(filename)
                    copied_files.append(filename)

    if etc_modified:
        compare_etc_templates()

        # CageFS is enabled ?
        if (not save_dir_exists()) and etcfs_is_disabled():
            update_etc(config)

    # Create cfg-file that contains copied paths (needed for cagefsctl --update to update those paths)
    UPDATE_LIST_CFG_FILE = CONFIG_DIR + 'cagefsctl-update-list.cfg'
    cfg = read_paths_from_file(UPDATE_LIST_CFG_FILE)
    cfg.extend(copied_files)
    write_paths_to_file(UPDATE_LIST_CFG_FILE, cfg, 'Files added by "cagefsctl --update-list" command')

    # Update list of files stored in cagefs-skeleton in order to handle deletion of files in cagefs-skeleton properly
    list_of_files = []
    load_list(FILES_LIST, list_of_files)
    add_parents(copied_files, copied_files)
    list_of_files.extend(copied_files)
    save_list_of_files_in_skeleton(list_of_files)


def demote(uid, gid):
    def func():
        os.setgid(gid)
        os.setuid(uid)
    return func


def tmpwatch():
    pw = secureio.clpwd.get_user_dict()
    tmpwatch_command = cagefslib.get_tmpwatch_params()
    tmpwatch_dirs = set(cagefslib.get_tmpwatch_dirs())
    tmpwatch_dirs.add('/var/cache/php-eaccelerator')
    if os.path.isdir('/var/lib/php/session'):
        tmpwatch_dirs.add('/var/lib/php/session')
    if not is_cpanel() and not is_plesk():
        # clean php sessions for all alt-php versions (for CP other than cPanel:
        # on cPanel sessions are cleaned by /usr/share/cagefs/clean_user_php_sessions script
        # on Plesk sessions are cleaned by /usr/share/cagefs/clean_user_alt_php_sessions_plesk script
        # executed via /etc/cron.d/cpanel_php_sessions_cron)
        alt_php_dirs = cagefslib.get_alt_dirs()
        for php_dir in alt_php_dirs:
            tmpwatch_dirs.add('/opt/alt/%s/var/lib/php/session' % php_dir)
    dev_null = open('/dev/null', 'w')
    for user in pw:
        cmd = tmpwatch_command.split()
        line = pw[user]
        tmp_path = os.path.join(line.pw_dir, '.cagefs/tmp')
        # we assume that tmp directory always exists when cagefs is enabled
        # whenever there aren't tmp dir, we assume that cagefs isn't enabled
        if os.path.isdir(tmp_path):
            cmd.append(tmp_path)
            for dir_name in tmpwatch_dirs:
                dir_path = os.path.join(line.pw_dir, '.cagefs') + dir_name
                if os.path.isdir(dir_path):
                    cmd.append(dir_path)
            subprocess.call(cmd, stderr=dev_null, stdout=dev_null, preexec_fn=demote(line.pw_uid, line.pw_gid), cwd=line.pw_dir)


def invalid_homes_exist():
    # get all users from /etc/passwd
    pw = secureio.get_pwd_dict()

    for user in pw:
        if pw[user].pw_dir.find('cagefs-skeleton') != -1:
            return True

    return False


def get_users_from_args(args):
    users = []
    for username in args:
        if user_exists(username):
            users.append(username)
        else:
            secureio.print_error('user', username, 'does not exist')
            sys.exit(1)
    return users


def is_user_enabled(username):
    """
    Check that cagefs enabled for user
    """
    try:
        uid = secureio.clpwd.get_uid(username)
    except ClPwd.NoSuchUserException:
        return False
    user_prefix = get_user_prefix(username)
    get_min_uid()
    if uid >= MIN_UID:
        if os.path.exists(enabled_dir) and os.path.exists(enabled_dir + '/' + str(user_prefix) + '/' + username):
            return True
        if os.path.exists(disabled_dir) and not os.path.exists(disabled_dir + '/' + str(user_prefix) + '/' + username):
            return True
    return False


def cpetc_for_user(username, config):
    user_etc_path = BASEDIR + '/' + get_user_prefix(username) + '/' + username + '/etc'
    custom_etc_files = cagefslib.get_additional_etc_files_for_user(username, user_etc_path)
    custom_etc_files2 = cagefslib.cut_path(custom_etc_files, user_etc_path)
    copyetc(username, config, ignore_errors = False, recreate = False, custom_etc_files = custom_etc_files)
    cagefslib.update_custom_etc_files_for_user(username, user_etc_path)
    cagefslib.save_custom_etc_log(username, custom_etc_files2)


# Add the spamassassin directories to CageFs in cPanel
def add_spamassassin_dirs_cpanel():
    # Add spamassassin directories to cagefs.mp file
    try:
        # exit, if cagefs.mp absent
        if not os.path.isfile(ETC_MPFILE):
            return

        # 1. Read cagefs.mp file
        f = open(ETC_MPFILE, 'r')
        cagefs_mp_lines = f.readlines()
        f.close()
        cagefs_mp_lines = [l.strip() for l in cagefs_mp_lines]

        # 2. Modify cagefs.mp contents
        for line in SPAMASSASSIN_DIRS_FOR_CAGEFS:
            line_to_check_and_write = '!'+line
            if line_to_check_and_write not in cagefs_mp_lines and os.path.isdir(line):
                cagefs_mp_lines.append(line_to_check_and_write)
        cagefs_mp_lines = [l+'\n' for l in cagefs_mp_lines]

        # 3. Write cagefs.mp back
        f = open(ETC_MPFILE, 'w')
        f.writelines(cagefs_mp_lines)
        f.close()
    except OSError as e:
        print('Error:', str(e), file=sys.stderr)


def unmount_dir_in_lve(path: str, lve_list: List[int]) -> None:
    """
    Unmount path in all LVE namespaces.
    Enter to LVE and unmount directory without destroying LVE.
    :param: path `str` path for unmount
    :param: lve_list `list` list of id's for existing LVEs
    :return: None
    """
    for lve_id in lve_list:
        p = subprocess.Popen([LVE_UMOUNT, str(lve_id), path],
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             text=True)
        _, strerr = p.communicate()
        if VERBOSE:
            if p.returncode:
                # error occured
                secureio.print_error('LVE', lve_id, strerr.strip())
            else:
                print('Unmount for LVE', lve_id, 'succeeded')


def unmount_dir_for_all_processes(path: str) -> None:
    """
    Unmount directory in all mount namespaces of all processes running in a system
    :param path: absolute path to directory to unmount
    """
    # get list of PIDs of all running processes
    ps_cmd = ['/bin/ps', '--no-headers', '-xao', 'pid']
    p = subprocess.run(ps_cmd, capture_output=True, text=True)
    if p.returncode:
        # error occured
        secureio.print_error('failed to execute:', *ps_cmd,
                             'return code:', p.returncode, 'stderr:', str(p.stderr).strip())
        sys.exit(1)
    pids = p.stdout.split()
    for pid in pids:
        if pid:
            # enter mount namespace and unmount directory ignoring errors
            p = subprocess.run(['/usr/bin/nsenter', '-m', '-t', pid, UMOUNT, '-l', path],
                               capture_output=True, text=True)
            if VERBOSE:
                if p.returncode:
                    # error occured
                    secureio.print_error('PID', pid, 'stderr:', str(p.stderr).strip())
                else:
                    print('Unmount for PID', pid, 'succeeded')


def unmount_dir(dir_list: List[str]) -> None:
    """
    Unmount directories from list in all mount namespaces
    :param dir_list: list of paths to directories for unmounting
    """
    # check if all directories are unmounted in real FS
    mounted_dirs = cagefslib.get_mounted_dirs(all_mounts=True)
    found = False
    for directory in dir_list:
        if os.path.realpath(directory) in mounted_dirs:
            found = True
            secureio.print_error('directory', directory, 'is mounted. ',
                                 'Please unmount the directory before running this command.')
    if found:
        sys.exit(1)
    # update namespace template using "lvectl start", so subsequent enters
    # to CageFS/namespace will be with updated set of mounts
    lvectl_start()
    for directory in dir_list:
        # unmount directory in all existing LVE/namespaces
        unmount_dir_in_lve(directory, get_lve_list())
        unmount_dir_for_all_processes(directory)


def unmount_user(user_name):
    """
    Unmount CageFS for user. Return True if error has occured
    :param user_name: name of user
    :type user_name: str
    """
    try:
        pw = secureio.clpwd.get_pw_by_name(user_name)
    except ClPwd.NoSuchUserException:
        secureio.print_error("User", user_name, "does not exists")
        return True

    if is_running_without_lve():
        return cagefs_without_lve_lib._delete_namespace_user(user_name)

    # acqire lock
    prefix = get_user_prefix(user_name)
    lock_path = os.path.join(BASEDIR, prefix, user_name+'.lock')
    dir_path = os.path.dirname(lock_path)
    cagefslib.make_dir(dir_path, 0o751)
    _ = acquire_lock(lock_path, wait=True, quiet=True)

    # destroy and recreate LVE in order to remove effect of chroot/pivot_root syscalls
    # (otherwise we will be not able to unmount all CageFS mounts)
    if remount([user_name]):
        secureio.print_error("Failed to destroy/apply LVE for user", user_name)
        return True

    # enter LVE/namespace and unmount all CageFS mounts
    # use '-mek' options for lve_suwrapper in order to disable PMEM, NPROC, EP limits
    # and also prevent killing cagefsctl process inside LVE by lvectl destroy
    cmd = ["/bin/lve_suwrapper", '-mek', str(pw.pw_uid), "/usr/sbin/cagefsctl", "--unmount-cur-ns"]
    try:
        subprocess.call(cmd, shell=False)
    except OSError:
        secureio.print_error(*cmd)
        return True
    return False


def check_cagefs_skeleton():
    """
    Checks that cagefs skeleton exists and is not empty
    """
    return os.path.isdir(os.path.join(SKELETON, 'bin'))


def print_cagefs_skeleton_status():
    if not check_cagefs_skeleton():
        print(SKELETON_NOT_INITIALIZED)
        sys.exit(1)
    print(SKELETON_INITIALIZED)


def main():
    import syslog
    syslog.openlog(native_str('cagefsctl'))
    try:
        main_func()
    except SystemExit as e:
        sys.exit(int(str(e)))
    except Exception as e:
        cagefslib.print_exception(level = syslog.LOG_ERR, includetraceback = True)
        sys.exit(1)


def init_min_uid():
    # Read MIN_UID from file
    get_min_uid()
    # Copy MIN_UID to securelve module
    secureio.MIN_UID = MIN_UID
    # create ClPwd instance
    secureio.clpwd = ClPwd(min_uid = MIN_UID)


init_min_uid()


def _get_username_list_from_args(args: List[str]) -> List[str]:
    """
    Retrives users list from cmd line
    :param args: args list
    :return: users list
    """
    users_list = []
    for username in args:
        try:
            pw_db = secureio.clpwd.get_pw_by_uid(int(username))
            for pw in pw_db:
                users_list.append(pw.pw_name)
        except (ValueError, ClPwd.NoSuchUserException):
            if user_exists(username):
                users_list.append(username)
            else:
                secureio.print_error('user or UID', username, 'does not exist')
                sys.exit(1)
    return users_list


def exit_if_lve_supported():
    """
    Print error and exit when LVE is supported
    """
    if not is_running_without_lve():
        print("ERROR: This command is workable only in environments without LVE support")
        sys.exit(1)


def exit_if_lve_not_supported():
    """
    Print error and exit when LVE is not supported
    """
    if is_running_without_lve():
        print("ERROR: This command is not supported in environments without LVE support")
        sys.exit(1)


def create_namespaces(users : Optional[List[str]] = None, do_mount_skel : bool = False) -> bool:
    exit_if_lve_supported()
    check_skeleton()
    remove_nested_skeleton()
    if do_mount_skel and not skeleton_is_mounted():
        mount_skeleton()
    if users is None:
        users = get_enabled_users()
    return cagefs_without_lve_lib.create_namespace_user_list(users)


def delete_namespaces(users : Optional[List[str]] = None) -> bool:
    exit_if_lve_supported()
    if users is None:
        users = get_cagefs_users(all_users=True)
    return cagefs_without_lve_lib.delete_namespace_user_list(users)


def clean_without_lve_environment() -> int:
    exit_if_lve_supported()
    rc = 0
    rc += delete_namespaces()
    cagefs_without_lve_lib.restore_httpd_php_fpm_services()
    cagefs_universal_hook_lib.remove_without_lve_universal_hooks()
    return rc


def main_func():
    global SKELETON, do_not_ask_option, config_copy, VERBOSE

    try:
        options_list = ["update", "dont-clean", "reinit", "help", 'version',
                        "verbose", "force", 'hardlink', 'init', 'remove-all', 'set-tmpwatch=', 'tmpwatch',
                        'list', 'help', 'unmount', 'unmount-dir', 'unmount-all', 'unmount-really-all', 'enable', 'disable',
                        'enable-all', 'disable-all', 'display-user-mode', 'list-enabled', 'wait-lock',
                        'list-disabled', 'create-mp', 'check-mp', 'mount-skel', 'unmount-skel', 'remount-all', 'remount',
                        'addrpm','delrpm', 'enter=', 'enable-cagefs', 'disable-cagefs','do-not-ask',
                        'debug', 'profiling', 'migrate-prefixes', 'getprefix=', 'list-rpm', 'apply-global-php-ini',
                        'set-min-uid=', 'get-min-uid', 'toggle-mode', 'silent', 'cpetc', 'update-etc', 'force-update-etc',
                        'check-kernel-version', 'update-wrappers', 'remove-blacklisted', 'detect-postgres', "toggle-plugin", 'print-suids',
                        'hook-install', 'hook-remove', 'reconfigure-cagefs', 'configure-litespeed',
                        'clean-var-cagefs', 'user-status=', 'cagefs-status', 'update-list', 'rebuild-alt-php-ini',
                        'validate-alt-php-ini', 'setup-cl-selector', 'skip-php-reload', 'check-for-unsafe-mounts',
                        'remove-cl-selector', 'cl-selector-reset-versions', 'setup-cl-alt', 'remove-cl-alt', 'cl-selector-reset-modules',
                        'update-users-status', "update-users-status-fix-owner", 'set-default-user-status', 'remove-unused-mount-points',
                        'create-homeN-dirs-in-skeleton', 'unmount-cur-ns', 'configure-openlitespeed',
                        'enable-cagefs-without-etc-update', 'without-lock', 'set-update-period=', 'force-update', 'add-default-rpm-packages',
                        'create-virt-mp', 'create-virt-mp-all', 'remount-virtmp', 'list-logged-in', 'clean-config-dirs',
                        'create-dirs-for-symlink-protection', 'sanity-check', 'check-cagefs-initialized'
                        ]
        if is_running_without_lve():
            options_list.extend(['create-namespace', 'create-namespaces',
                                 'delete-namespace', 'delete-namespaces',
                                 'clean-without-lve-environment'])
        opts, args = getopt.getopt(sys.argv[1:], "ihvVfurdkwW?lmMe:", options_list)
    except getopt.GetoptError as e:
        usage()
        print("\nError:", str(e))
        sys.exit(1)
    import cagefsreconfigure
    import virtmp_mount

    # Logging all args
    from clcommon import ClAuditLog
    log = ClAuditLog ( INFO_LOG_FILE )
    log.info_log_write ()

    config = {}
    config['verbose'] = 0

    for o, a in opts:
        if o in ("-h", "-?", "--help"):
            usage()
            sys.exit(0)
        elif o in ("-V", "--version"):
            print(cagefs_version)
            sys.exit(0)
        elif o in ("--check-kernel-version",):
            check_kernel()
            sys.exit(0)
        elif o in ("-v", "--verbose"):
            config['verbose'] = 1
            VERBOSE = 1
            cagefslib.VERBOSE_FLAG = 1
        elif o in ("--silent",):
            global SILENT
            SILENT=1
            secureio.SILENT_FLAG = 1

    if (os.geteuid()!=0):
        secureio.print_error('root privileges required. Abort.')
        sys.exit(5)

    # remove possible symlinks from path to cagefs-skeleton
    try:
        SKELETON = os.path.realpath(SKELETON)
        cagefslib.SKELETON = SKELETON
    except:
        sys.stderr.write('Error while determining real path to skeleton directory '+SKELETON+'\n')
        sys.exit(1)

    config['dry-run'] = 0
    config['interactive'] = 0
    config['unmount'] = 0
    config['enable'] = 0
    config['disable'] = 0
    config['force'] = 0
    config['update'] = 0
    config['reinit'] = 0
    config['dont-clean'] = 0
    config['hardlink'] = 0
    config['init'] = 0
    config['remount'] = 0
    config['profiling'] = 0
    config['force-update'] = 0
    config['force-update-etc'] = False
    config['skip-php-reload'] = False
    manage_user_flag = False
    build_jail_flag = False
    lock_is_required = True
    wait_lock = False

    for o, a in opts:
        if o in ('--getprefix',):
            print(get_user_prefix(a))
            sys.exit(0)
        elif o in ("-d", "--dont-clean"):
            config['dont-clean'] = 1
            build_jail_flag = True
        elif o in ("--cpetc",):
            if (len(args) == 0):
                secureio.print_error('no username or UID specified')
                sys.exit(2)
            for username in args:
                try:
                    pw_db = secureio.clpwd.get_pw_by_uid(int(username))
                    for pw in pw_db:
                        cpetc_for_user(pw.pw_name, config)
                        cagefslib.create_utmp_for_user(pw.pw_name)
                except (ValueError, ClPwd.NoSuchUserException):
                    if user_exists(username):
                        cpetc_for_user(username, config)
                        cagefslib.create_utmp_for_user(username)
                    else:
                        secureio.print_error('user or UID', username, 'does not exist')
                        sys.exit(1)
            sys.exit(0)
        elif o in ('--clean-config-dirs',):
            clean_config_dirs()
            sys.exit(0)
        elif o in ('--skip-php-reload',):
            config['skip-php-reload'] = True
        elif o in ('--create-dirs-for-symlink-protection',):
            create_dirs_for_symlink_protection()
            create_files_for_symlink_protection()
            sys.exit(0)
        elif o in ('--sanity-check',):
            import sanity_check
            sanity_check.check()
            sys.exit(0)
        elif o in ('--hook-install',):
            cagefshooks.HooksInstall()
            sys.exit(0)
        elif o in ('--hook-remove',):
            cagefshooks.HooksRemove()
            sys.exit(0)
        elif o in ('--reconfigure-cagefs',):
            cagefsreconfigure.reconfigure_cagefs()
            sys.exit(0)
        elif o in ('--configure-litespeed',):
            cagefsreconfigure.litespeed_configure()
            sys.exit(0)
        elif o in ('--configure-openlitespeed',):
            cagefsreconfigure.configure_open_litespeed()
            sys.exit(0)
        elif o in ('--list-enabled', '--list-disabled'):
            check_exclude()
            list_users(o == '--list-enabled')
            sys.exit(0)
        elif o in ("--cl-selector-reset-modules",):
            users = get_users_from_args(args)
            reset_modules_to_default(users)
            sys.exit(0)
        elif o in ("--rebuild-alt-php-ini",):
            # Retrive user names from args
            users = get_users_from_args(args)
            rebuild_alt_php_ini(users)
            sys.exit(0)
        elif o in ("--validate-alt-php-ini",):
            # Retrive user names from args
            users = get_users_from_args(args)
            check_php_ini_options(users)
            sys.exit(0)
        elif o in ("--cl-selector-reset-versions", "--remove-cl-selector", "--remove-cl-alt"):
            users = get_users_from_args(args)
            if users:
                remove_etc_alternatives(users = users, force = (o == "--cl-selector-reset-versions"))
            else:
                remove_etc_alternatives(all_users = True, force = (o == "--cl-selector-reset-versions"))
                remove_mounts_for_php_selector()
            sys.exit(0)
        elif o in ('-W', '--unmount-all', '--unmount-really-all'):
            # Unmounting for CageFS 2.0 is needed ?
            if (not os.path.exists('/etc/cagefs/etc.safe')) and (not os.path.exists(PROXY_COMMANDS)):
                repair_homes.umount_all()
            else:
                # Do unmounting for CageFS 3.0
                unmount_all(remount_users = True, check_busy = False, all_cagefs_mounts = (o == '--unmount-really-all'))
            sys.exit(0)
        elif o in ("--unmount-dir",):
            exit_if_lve_not_supported()
            # check directory in args list existing
            if (len(args) == 0):
                secureio.print_error('no directory to unmount specified')
                sys.exit(2)
            unmount_dir(args)
            sys.exit(0)
        elif o in ("--migrate-prefixes",):
            migrate_to_new_prefixes()
            sys.exit(0)
        elif o in ('--set-min-uid',):
            set_min_uid(a)
            check_exclude()
            sys.exit(0)
        elif o in ('--get-min-uid',):
            print(MIN_UID)
            sys.exit(0)
        elif o in ('--set-update-period',):
            cagefslib.set_update_period(a)
            sys.exit(0)
        elif o in ('--force-update',):
            config['force-update'] = 1
            config['update'] = 1
            build_jail_flag = True
            check_skeleton()
        elif o in ('--clean-var-cagefs',):
            clean_var_cagefs()
            sys.exit(0)
        elif o in ('--update-wrappers',):
            load_wrappers(update_wrappers = True)
            sys.exit(0)
        elif o in ('--remove-blacklisted',):
            load_black_list(remove = True)
            sys.exit(0)
        elif o in ('--detect-postgres',):
            cagefslib.detect_postgres()
            sys.exit(0)
        elif o in ('--print-suids',):
            cagefslib.mounts = MountpointConfig().common_mounts
            # Print list of SUID and SGID programs in skeleton
            cagefslib.print_suids(SKELETON)
            sys.exit(0)
        elif o in ('--toggle-plugin',):
            toggle_plugin()
            sys.exit(0)
        elif o in ('--display-user-mode',):
            print_user_mode()
            sys.exit(0)
        elif o in ('--user-status',):
            check_exclude()
            print_user_status(a)
            sys.exit(0)
        elif o in ('--cagefs-status',):
            print_cagefs_status()
            sys.exit(0)
        elif o in ('--check-cagefs-initialized',):
            print_cagefs_skeleton_status()
            sys.exit(0)
        elif o in ("--disable-cagefs",):
            old_enabled_users = get_enabled_users()
            disable_cagefs()
            update_users_status(disable_all=True, old_enabled_users=old_enabled_users)
            remount_all()
            sys.exit(0)
        elif o in ("--update-users-status", "--update-users-status-fix-owner"):
            if o == "--update-users-status-fix-owner":
                get_mounted_users(fix_permissions=True)
            if cagefs_is_enabled():
                old_enabled_users = get_enabled_users()
                check_exclude()
                update_users_status(fix_owner=(o == "--update-users-status-fix-owner"),
                                                        old_enabled_users=old_enabled_users)
            sys.exit(0)
        elif o in ("--set-default-user-status",):
            # For use in postwwwacct hook only
            if args:
                mode = get_user_mode()
                if mode == 'Enable All':
                    old_enabled_users = get_enabled_users()
                    for username in args:
                        toggle_user(username, True)
                    update_users_status(users=args, status=True, old_enabled_users=old_enabled_users)
                elif mode == 'Disable All':
                    old_enabled_users = get_enabled_users()
                    for username in args:
                        toggle_user(username, False)
                    update_users_status(users=args, status=False, old_enabled_users=old_enabled_users)
            sys.exit(0)
        elif o in ("--remove-unused-mount-points",):
            remove_unused_mount_points()
            sys.exit(0)
        elif o in ('--create-homeN-dirs-in-skeleton',):
            check_skeleton()
            cagefslib.mounts = MountpointConfig().common_mounts
            create_homeN_dirs_in_skeleton()
            update_homeN_symlinks_in_skeleton()
            sys.exit(0)
        elif o in ("--mount-skel", "--unmount-skel"):
                lock_is_required = False
        elif o in ("-w", "--unmount", "-m", "--remount", "--enable", "--disable"):
            if is_running_without_lve():
                wait_lock = True
            else:
                lock_is_required = False
        elif o in ("--without-lock",):
            lock_is_required = False
        elif o in ('--wait-lock',):
            wait_lock = True
        elif o in ('--enable-cagefs-without-etc-update',):
            old_enabled_users = get_enabled_users()
            enable_cagefs()
            enable_cagefs_for_users_with_duplicate_uids()
            update_users_status(old_enabled_users=old_enabled_users)
            check_skeleton()
            remove_nested_skeleton()
            # Remount skeleton and all users
            mount_skeleton(True)
            sys.exit(0)
        elif o in ("--add-default-rpm-packages",):
            add_default_rpm_packages_to_cagefs()
            sys.exit(0)
        elif o in ('--tmpwatch',):
            tmpwatch()
            sys.exit(0)
        elif o in ('--set-tmpwatch',):
            cagefslib.set_tmpwatch_params(a)
            sys.exit(0)
        elif o in ('--create-virt-mp',):
            if len(sys.argv) != 3:
                secureio.print_error("No username provided")
                sys.exit(1)
            virtmp_mount.create_virtmp(sys.argv[2])
            sys.exit(0)
        elif o in ('--create-virt-mp-all',):
            virtmp_mount.create_virtmp()
            sys.exit(0)
        elif o in ('--remount-virtmp',):
            if len(args) != 1:
                secureio.print_error("No username provided")
                sys.exit(1)
            virtmp_mount.create_virtmp(args[0])
            # --remount USER
            check_save_dir()
            check_skeleton()
            remove_nested_skeleton()
            config['remount'] = 1
            manage_user_flag = True
        elif o in ('--enter','-e'):
            check_save_dir()
            check_skeleton()
            check_kernel()
            remove_nested_skeleton()
            print('You are entering to CageFS for user', a, 'as superuser (root).')
            print('NOTE: You can use "su -s /bin/bash - {}" instead to enter to CageFS as user {}'.format(a, a))
            try:
                subprocess.call(["/sbin/cagefs_enter_user", "--root", a, "/bin/bash"], shell=False)
            except Exception:
                secureio.print_error('executing /sbin/cagefs_enter_user')
                sys.exit(1)
            sys.exit(0)
        elif o in ("--check-mp",):
            check_mp_file()
            sys.exit(0)
        elif o in ("--check-for-unsafe-mounts",):
            if unsafe_mounts_exist():
                create_remount_flag()
            sys.exit(0)
        elif o in ('-l', '--list'):
            exit_if_lve_not_supported()
            users = get_mounted_users()
            print_users(users, 1, 'CageFS currently mounted for users:')
            sys.exit(0)
        elif o in ('--list-logged-in',):
            exit_if_lve_not_supported()
            users = get_logged_in_users()
            print_users(users, 1, 'Users currently logged in CageFS via ssh:')
            sys.exit(0)
        elif o in ('--unmount-cur-ns',):
            # CAG-749: unmount CageFS mounts in all mount namespaces (resolve conflict with systemd)
            umount_skeleton(save_mounts = False, all_cagefs_mounts = True, current_namespace_only=True)
            sys.exit(0)
        elif o in ('--delete-namespace',):
            username_list = _get_username_list_from_args(args)
            sys.exit(int(delete_namespaces(username_list)))
        elif o in ('--delete-namespaces',):
            sys.exit(int(delete_namespaces()))
        elif o in ('--clean-without-lve-environment',):
            sys.exit(clean_without_lve_environment())

    check_kernel()

    if lock_is_required:
        # Acquire lock
        lockfile = acquire_lock(wait=wait_lock)  # @UnusedVariable

    ex_list = get_exclude_user_list()
    check_exclude(ex_list)

    if SKELETON.find('home') != -1:
        if os.path.isdir('/var/cpanel'):
            if invalid_homes_exist():
                print_cpanel_home_warning()

    for o, a in opts:
        if o in ('--mount-skel',):
            check_skeleton()
            remove_nested_skeleton()
            mount_skeleton()
            sys.exit(0)
        elif o in ('--unmount-skel',):
            check_skeleton()
            umount_skeleton()
            # cagefs_fuse('stop')
            sys.exit(0)
        elif o in ("--debug",):
            cagefslib.debug_option = True
        elif o in ("--profiling",):
            config['profiling'] = 1
            build_jail_flag = True
        elif o in ("--do-not-ask",):
            do_not_ask_option = True
        elif o in ("-w", "--unmount"):
            check_save_dir()
            config['unmount'] = 1
            manage_user_flag = True
        elif o in ("--enable-cagefs",):
            old_enabled_users = get_enabled_users()
            enable_cagefs()
            enable_cagefs_for_users_with_duplicate_uids()
            update_users_status(old_enabled_users=old_enabled_users)
            check_skeleton()
            remove_nested_skeleton()
            if etcfs_is_disabled():
                update_etc(config)
            # Remount skeleton and all users
            mount_skeleton(True)
            sys.exit(0)
        elif o in ('--enable',):
            check_save_dir()
            check_skeleton()
            remove_nested_skeleton()
            config['enable'] = 1
            manage_user_flag = True
        elif o in ('--disable',):
            check_save_dir()
            config['disable'] = 1
            manage_user_flag = True
        elif o in ('--remove-all',):
            remove_all()
            sys.exit(0)
        elif o in ('-M', '--remount-all'):
            check_save_dir()
            check_skeleton()
            remove_nested_skeleton()
            # Remount skeleton and all users
            mount_skeleton(True)
            sys.exit(0)
        elif o in ("-m", "--remount"):
            check_save_dir()
            check_skeleton()
            remove_nested_skeleton()
            config['remount'] = 1
            manage_user_flag = True
        elif o in ('--enable-all',):
            old_enabled_users = get_enabled_users()
            check_skeleton()
            remove_nested_skeleton()
            set_user_mode(True)
            update_users_status(old_enabled_users=old_enabled_users)
            if etcfs_is_disabled():
                update_etc(config)
            # Remount skeleton and all users
            mount_skeleton(True)
            sys.exit(0)
        elif o in ('--disable-all',):
            old_enabled_users = get_enabled_users()
            set_user_mode(False)
            update_users_status(old_enabled_users=old_enabled_users)
            remount_all()
            sys.exit(0)
        elif o in ('--toggle-mode',):
            toggle_mode()
            sys.exit(0)
        elif o in ("-f", "--force"):
            config['force'] = 1
            build_jail_flag = True
            check_skeleton()
        elif o in ("-u", "--update"):
            config['update'] = 1
            build_jail_flag = True
            check_skeleton()
        elif o in ("-i", "--init"):
            if os.path.isdir(SKELETON+'/bin'):
                secureio.logging('Error : directory %s already exists.\nUse "%s --reinit" if you want to reinitialize CageFS' % (SKELETON, sys.argv[0]))
                sys.exit(1)
            config['init'] = 1
            build_jail_flag = True
            user_mode = get_user_mode()
            # mode is not set yet and CageFS is enabled ?
            if (user_mode == 'Error' or user_mode == 'Not Initialized') and (not save_dir_exists()):
                # set mode to "Disable All"
                set_user_mode(False)
        elif o in ("-r", "--reinit"):
            config['reinit'] = 1
            build_jail_flag = True
            check_skeleton()
        elif o in ("-k", "--hardlink"):
            config['hardlink'] = 1
            build_jail_flag = True
        elif o in ("--create-mp",):
            create_mp(True)
            sys.exit(0)
        elif o in ("--addrpm",):
            for rpm in args:
                addrpm(rpm)
            sys.exit(0)
        elif o in ("--delrpm",):
            for rpm in args:
                delrpm(rpm)
            sys.exit(0)
        elif o in ("--list-rpm",):
            list_rpm()
            sys.exit(0)
        elif o in ('--update-list',):
            update_list(config)
            sys.exit(0)
        elif o in ("--update-etc", '--force-update-etc'):
            config['force-update-etc'] = (o == '--force-update-etc')
            users = get_users_from_args(args)
            if users:
                update_etc_only(config, users = users)
            else:
                update_etc_only(config)
            sys.exit(0)
        elif o in ("--setup-cl-selector", "--setup-cl-alt"):
            setup_cl_alt(config)
            sys.exit(0)
        elif o in ("--apply-global-php-ini",):
            # alt-php versions are installed ?
            if cagefslib.get_alt_versions():
                cagefsreconfigure.replace_alt_settings(options=args)
            sys.exit(0)
        elif o in ('--create-namespace',):      # for USER
            username_list = _get_username_list_from_args(args)
            sys.exit(int(create_namespaces(username_list, do_mount_skel=True)))
        elif o in ('--create-namespaces',):  # For all users
            sys.exit(int(create_namespaces(do_mount_skel=True)))

    if manage_user_flag and build_jail_flag:
        usage()
        print("\nError: incompatible options specified")
        sys.exit(1)

    if manage_user_flag:
        if (len(args)==0):
            print()
            print('aborted, no username specified')
            sys.exit(2)

        if config['unmount'] != 1 and not is_running_without_lve():
            # CAG-770: unshare mount namespace, so mounting/unmounting cagefs-skeleton will not affect all system
            unshare.unshare(unshare.CLONE_NEWNS)

        try:
            if (config['unmount'] == 1):
                error = False
                for username in args:
                    error = unmount_user(username) or error
                sys.exit(int(error))

            elif (config['disable'] == 1):
                old_enabled_users = get_enabled_users()
                usernames = []
                for username in args:
                    if user_exists(username):
                        toggle_user(username, False)
                        usernames.append(username)
                    else:
                        secureio.print_error('user '+username+' does not exist')
                update_users_status(users=usernames, status=False, old_enabled_users=old_enabled_users)
                if is_running_without_lve():
                    delete_namespaces(usernames)
                else:
                    remount(usernames)

            elif (config['remount'] == 1) or (config['enable'] == 1):
                old_enabled_users = get_enabled_users()
                mount_skeleton()

                usernames = []
                for username in args:
                    if user_exists(username):
                        if username not in ex_list:
                            if (config['enable'] == 1):
                                toggle_user(username, True)
                                if etcfs_is_disabled():
                                    update_etc(config, [username], False)
                            usernames.append(username)
                        elif config['enable'] == 1:
                            secureio.print_error('user '+username+' is excluded')
                    else:
                        secureio.print_error('user '+username+' does not exist')
                if config['enable'] == 1:
                    update_users_status(users=usernames, status=True, old_enabled_users=old_enabled_users)
                remount(usernames)
            else:
                secureio.print_error('No options specified. Nothing to do...')
                sys.exit(2)

        except KeyboardInterrupt:
            print()
            print('aborted.. ')
            sys.exit(1)

    elif build_jail_flag:
        jail = SKELETON
        if (config['dont-clean'] and (config['init'] or config['reinit'])):
            secureio.print_error('cannot specify --dont-clean with --init or --reinit options')
            sys.exit(1)

        count_of_modes = config['init'] + config['reinit'] + config['update'] + config['force']

        if config['profiling'] == 0:
            if count_of_modes != 1:
                secureio.print_error('you should specify one of the --init, --reinit, --update or --force options\n')
                sys.exit(1)

        if config['reinit'] == 1:
            users = get_mounted_users()
            users_count = len(users)
            if users_count != 0:
                print('WARNING: ', users_count, 'CageFS currently mounted.')
                print('If you proceed, CageFS will be temporarily disabled and unmounted.')
                confirm("Do you want to continue (yes/no)? ")

            if cagefs_is_enabled():
                config['cagefs_was_enabled'] = 1
                disable_cagefs()
                # update ISP manager user wrappers
                update_users_status(disable_all=True)

            unmount_all(remount_users=True, check_busy=False)

            try:
                jbuf = os.lstat(jail+'.old')
                if stat.S_ISDIR(jbuf[stat.ST_MODE]):
                    shutil.rmtree(jail+'.old')
                else:
                    os.unlink(jail+'.old')
            except (OSError, IOError, shutil.Error):
                pass
            try:
                os.rename(jail, jail+'.old')
            except (OSError, IOError):
                pass

        try:
            mod_makedirs(jail, 0o755)
        except (OSError, IOError):
            pass

        if config['profiling'] == 1:
            config_copy = config

            # one of the init, reinit, update or force options is specified ?
            if count_of_modes == 1:
                import profile
                profile.run('cagefsctl.do_profiling()', LIBDIR+'/profiling.log')
            import pstats
            p = pstats.Stats(LIBDIR+'/profiling.log')

            print()
            print('--------------------------------------')
            print('Cumulative time:')
            p.sort_stats('cumulative').print_stats(20)
            print()
            print('--------------------------------------')
            print('Total time:')
            p.sort_stats('time').print_stats(20)
        else:
            update_cagefs(config)
            if config['init'] or config['reinit']:
                config['init'] = config['reinit'] = 0
                setup_cl_alt(config)
    else:
        usage()
        sys.exit(1)


if __name__ == "__main__":
    main()

Hacked By AnonymousFox1.0, Coded By AnonymousFox