Hacked By AnonymousFox

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

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

# pylint: disable=no-absolute-import

import os
import time
import subprocess
import signal
from typing import Optional, List, Dict
import traceback
import fcntl
import secureio
from pathlib import Path
import glob

from clcommon.lib import cledition
from clcommon.utils import run_command, ExternalProgramFailed, get_file_lines, write_file_lines
from cldetectlib import is_da
from secureio import write_file_via_tempfile, logging


_CAGEFS_MOUNT_BIN_FILE = '/usr/sbin/cagefs-mount'
_LSNS_BIN_FILE = '/usr/bin/lsns'
_MOUNT_BIN_FILE = '/bin/mount'
_UMOUNT_BIN_FILE = '/bin/umount'
_NSENTER_BIN = '/bin/nsenter'
_GREP_BIN = '/bin/grep'

_LOCK_FILE_NAME_PATTERN = '/var/cagefs/%s/%s.lock'
_CAGEFS_SKELETON_DIR = '/usr/share/cagefs-skeleton'

# /var/cagefs.uid/$PREFIX/$UID/ns.mnt
_NS_MNT_FILE_NAME_PATTERN = '/var/cagefs.uid/%s/%d/ns.mnt'
# /var/cagefs.uid/$PREFIX/$UID/ns.id
_NS_ID_FILE_NAME_PATTERN = '/var/cagefs.uid/%s/%d/ns.id'


class LockFailedException(Exception):
    pass


class NsNotFoundException(Exception):
    pass


class CagefsMountNotStartedException(Exception):
    def __init__(self, msg=''):
        self.msg = f'{msg}'
        if cledition.is_container():
            self.msg += '\nThe Virtuozzo host mounting limit may have been reached.\n' \
                'Check for the presence of the kernel error "reached the limit on mounts" on VZ host.\n' \
                'More info at https://docs.cloudlinux.com/cloudlinux_installation/#known-restrictions-and-issues'

    def __str__(self):
        return self.msg


def _find_command_in_ns(ns_id: str, cmd_to_search: str) -> bool:
    """
    Find proposed command in proposed NS id
    :param ns_id: NS id to search command
    :param cmd_to_search: Command to search
    :return: True - command found in NS, False - not found
    """
    # Example:
    # # lsns --type mnt --list --output pid,command <ns_id>
    #   PID COMMAND
    #  1404 /bin/sh /usr/bin/mysqld_safe --basedir=/usr
    #  1577 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql ....
    try:
        cmd = [_LSNS_BIN_FILE, '--type', 'mnt', '--list', '--output', 'command', ns_id]
        stdout = run_command(cmd)
        for line in stdout.split('\n'):
            line = line.strip()
            if line == '' or 'PID' in line:
                # Skip header line and empty line
                continue
            # line example: '/bin/sh /usr/bin/mysqld_safe --basedir=/usr'
            if line == cmd_to_search:
                return True
    except (ExternalProgramFailed, ):
        pass
    return False


def _find_save_ns_id_for_user(username: str, filename_to_write: str):
    """
    Find user's NS id and write it to file
    :param username: User name
    :param filename_to_write: File name to write
    """
    cmd_to_search = f'{_CAGEFS_MOUNT_BIN_FILE} {username}'
    # 1. Get all user's NS id
    # lsns --type mnt --list --output ns,command
    cmd = [_LSNS_BIN_FILE, '--type', 'mnt', '--list', '--output', 'ns,command']
    stdout = run_command(cmd)
    ns_id_list = []
    for line in stdout.split('\n'):
        line = line.strip()
        if line == '' or 'NS' in line:
            # Skip header line and empty line
            continue
        line_parts = line.split(' ', 1)
        # line_parts example:
        # ['4026532195' '/usr/sbin/pdns_server --socket-dir=/run/pdns --guardian=no --daemon=no ...']
        if line_parts[1].strip() == cmd_to_search:
            # Command found, write NS id
            write_file_via_tempfile(line_parts[0], filename_to_write, 0o600)
            return
        ns_id_list.append(line)
    # Command not found in lsns output, search it in each NS
    for ns_id in ns_id_list:
        # try to find user's NS by process '/usr/sbin/cagefs-mount <username>'
        if _find_command_in_ns(ns_id, cmd_to_search):
            # Command found, write NS id
            write_file_via_tempfile(ns_id, filename_to_write, 0x600)
            return
    raise NsNotFoundException(f"NS not found for user {username} when save NS id to file")


def _get_all_ns() -> Dict[str, str]:
    """
    Get all NS with processes in system
    :return: Dict: {'some_ns_id', 'some_pid_from_ns'}
    """
    ns_dict = {}
    try:
        cmd = [_LSNS_BIN_FILE, '--type', 'mnt', '--list', '--output', 'ns,pid']
        stdout = run_command(cmd)
        for line in stdout.split('\n'):
            line = line.strip()
            if line == '' or 'PID' in line:
                # Skip header line and empty line
                continue
            # line example: '4026532195  1582'
            line_parts = line.split()
            ns_dict[line_parts[0].strip()] = line_parts[1].strip()
    except (ExternalProgramFailed, ):
        pass
    return ns_dict


def _get_pid_list_by_ns_id(ns_id: str, user_homedir: str) -> List[int]:
    """
    Retrieves PID list for user in proposed NS id
    :param ns_id: NS id to retrieve PID list
    :param user_homedir: User homedir
    :return: list PIDs in NS, [] - NS not found/has no processes
    """
    # Get all NS id in system with some PID in NS as dict {ns_id: pid}
    all_ns_id_dict = _get_all_ns()
    if ns_id not in all_ns_id_dict:
        return []
    ns_pid = all_ns_id_dict[ns_id]
    try:
        # Check that NS with ns_id owned by user with proposed homedir
        # /bin/nsenter -m -t PID /bin/grep /usr/share/cagefs-skeleton/$USERHOME /proc/mounts
        user_home_is_skeleton = os.path.join(_CAGEFS_SKELETON_DIR, user_homedir)
        proc = subprocess.run([_NSENTER_BIN, '-m', '-t', ns_pid, _GREP_BIN, user_home_is_skeleton, '/proc/mounts'],
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
        if proc.returncode != 0:
            # user home mount not found in NS
            return []
        pid_list = []
        # NS valid, get PID list from it
        cmd = [_LSNS_BIN_FILE, '--type', 'mnt', '--list', '--output', 'pid', ns_id]
        stdout = run_command(cmd)
        for line in stdout.split('\n'):
            line = line.strip()
            if line == '' or 'PID' in line:
                # Skip header line and empty line
                continue
            pid_list.append(int(line))
        return pid_list
    except (ExternalProgramFailed, OSError, IOError, ):
        pass
    return []


def _get_pid_list_for_user(user_uid: int, cagefs_user_prefix: str, user_homedir: str) -> Optional[List[int]]:
    """
    Retrieve pid list from user's NS
    :param user_uid: User's uid
    :param cagefs_user_prefix: User's cagefs prefix
    :param user_homedir: User's homedir
    :return: List of user's PIDs or None if user has no NS
    """
    try:
        # Read NS id from file
        lines = get_file_lines(_NS_ID_FILE_NAME_PATTERN % (cagefs_user_prefix, user_uid))
        ns_id = lines[0].strip()
        return _get_pid_list_by_ns_id(ns_id, user_homedir)
    except (OSError, IOError, IndexError, ):
        # No NS for this user
        pass
    return None


def _kill_processes_in_list(pid_list: List[int]):
    """
    Kill processes from list
    :param pid_list: PID list to kill
    """
    for pid in pid_list:
        try:
            os.kill(pid, signal.SIGKILL)
        except (OSError, ):
            pass


def _acquire_lock(filename: str) -> int:
    """
    Creates a lock file and acquire lock on it
    :return: File descriptor of created file
    """
    try:
        os.makedirs(os.path.dirname(filename), mode=0o700, exist_ok=True)
        lock_fd = os.open(filename, os.O_CREAT, 0o600)
        fcntl.flock(lock_fd, fcntl.LOCK_EX)
        return lock_fd
    except IOError:
        raise LockFailedException("IO error happened while getting lock")


def _release_lock(lock_fd: int) -> None:
    """
    Release lock and close lock file
    :param lock_fd: Lock file descriptor
    """
    fcntl.flock(lock_fd, fcntl.LOCK_UN)
    os.close(lock_fd)


def _wait_for_pid_file(pid_filename: str) -> bool:
    """
    Waits for cagefs-mount pid file appears up to 10 seconds
    :param pid_filename: PID filename
    :return: True - appears; False - Not
    """
    for i in range(10000):
        try:
            os.stat(pid_filename)
            return True
        except (OSError, IOError):
            # Error, PID file absent
            time.sleep(0.001)
    # Error, cagefs-mount was not started
    return False


def _create_namespace_user(username: str) -> bool:
    """
    Create namespace for single user
    :param username: User name to create namespace
    """
    from cagefsctl import get_user_prefix
    error = False
    background_processes = []
    try:
        cagefs_user_prefix = get_user_prefix(username)
        user_uid = secureio.clpwd.get_uid(username)
        # To create namespace for USER, use:
        # if /var/cagefs.uid/$PREFIX/$UID/ns.mnt bind mount exists already - do nothing
        # mkdir -p /var/cagefs.uid/$PREFIX/$UID
        # create lockfile /var/cagefs/$PREFIX/$USER.lock, acquire lock
        # touch /var/cagefs.uid/$PREFIX/$UID/ns.mnt
        # /usr/sbin/cagefs-mount $USER
        # mount --bind /proc/$PID/ns/mnt /var/cagefs.uid/$PREFIX/$UID/ns.mnt
        # release lockfile /var/cagefs/$PREFIX/$USER.lock
        # _NS_MNT_FILE_NAME_PATTERN = '/var/cagefs.uid/$PREFIX/$UID/ns.mnt'
        # kill $PID (where $PID = pid of cagefs-mount process)
        ns_mnt_filename = _NS_MNT_FILE_NAME_PATTERN % (cagefs_user_prefix, user_uid)
        if os.path.exists(ns_mnt_filename):
            return False
        os.makedirs(os.path.dirname(ns_mnt_filename), mode=0o700, exist_ok=True)
        # touch /var/cagefs.uid/$PREFIX/$UID/ns.mnt
        Path(ns_mnt_filename).touch()
        cagefs_pid_file = f'/var/cagefs.uid/{cagefs_user_prefix}/{user_uid}/cagefs-mount.pid'
        try:
            os.unlink(cagefs_pid_file)
        except (OSError, IOError,):
            pass
        cmd = [_CAGEFS_MOUNT_BIN_FILE, username]
        proc = subprocess.Popen(cmd, shell=False)
        cagefs_mount_pid = proc.pid
        background_processes.append(cagefs_mount_pid)
        # We should wait while binary creates cagefs mounts
        if not _wait_for_pid_file(cagefs_pid_file):
            raise CagefsMountNotStartedException(f'{_CAGEFS_MOUNT_BIN_FILE} not started for user {username}')
        # /bin/mount --bind /proc/$PID/ns/mnt /var/cagefs.uid/$PREFIX/$UID/ns.mnt
        res = subprocess.run(
            [_MOUNT_BIN_FILE, '--bind', f'/proc/{cagefs_mount_pid}/ns/mnt', ns_mnt_filename],
            shell=False
        )
        if res.returncode != 0:
            raise CagefsMountNotStartedException(f'Can\'t mount {ns_mnt_filename} for user {username}')
        # Find user's NS id and save it to file /var/cagefs.uid/$PREFIX/$UID/ns.id
        ns_id_filename = _NS_ID_FILE_NAME_PATTERN % (cagefs_user_prefix, user_uid)
        _find_save_ns_id_for_user(username, ns_id_filename)
    except KeyboardInterrupt:
        raise
    except CagefsMountNotStartedException as e:
        error = True
        logging(str(e))
    except:
        error = True
        msg = traceback.format_exc()
        logging(f"General error while attempting to create a namespace for user {username}. Error is: {msg}")
    finally:
        # Kill cagefs-mount process
        _kill_processes_in_list(background_processes)
    return error


def _delete_namespace_user(username: str) -> bool:
    """
    Delete namespace for single user
    :param username: User name to delete namespace
    """
    from cagefsctl import get_user_prefix
    lock_obj = None
    error = False
    try:
        cagefs_user_prefix = get_user_prefix(username)
        user_uid = secureio.clpwd.get_uid(username)
        user_homedir = secureio.clpwd.get_homedir(username)
        ns_mnt_filename = _NS_MNT_FILE_NAME_PATTERN % (cagefs_user_prefix, user_uid)
        if not os.path.exists(ns_mnt_filename):
            # User has no NS
            return False
        lock_obj = _acquire_lock(_LOCK_FILE_NAME_PATTERN % (cagefs_user_prefix, username))
        if not os.path.exists(ns_mnt_filename):
            # Check if User has no NS again after acquiring lock
            return False
        user_pids = _get_pid_list_for_user(user_uid, cagefs_user_prefix, user_homedir)
        if user_pids:
            _kill_processes_in_list(user_pids)
        # umount /var/cagefs.uid/$PREFIX/$UID/ns.mnt
        cmd = [_UMOUNT_BIN_FILE, ns_mnt_filename]
        subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=False)
        # rf -f /var/cagefs.uid/$PREFIX/$UID/ns.mnt
        os.unlink(ns_mnt_filename)
    except (LockFailedException, ) as e:
        print(f"Can't acqure lock for user {username}. Error is: {str(e)}")
        error = True
    except:
        msg = traceback.format_exc()
        print(f"Error during delete namespace for user {username}. Error is: {msg}")
        error = True
    if lock_obj:
        _release_lock(lock_obj)
    return error


def create_namespace_user_list(username_list: List[str], verbose = False) -> bool:
    """
    Create namespace for users from list
    :param username_list: username list for prosess
    """
    error = False
    for username in username_list:
        if verbose:
            print("Creating NS for user:", username)
        if _create_namespace_user(username):
            error = True
    return error


def delete_namespace_user_list(username_list: List[str], verbose = False) -> bool:
    """
    Delete namespace for users from list
    :param username_list: username list for prosess
    :param verbose: If True, print messages to stdout
    """
    error = False
    for username in username_list:
        if verbose:
            print("Deleting NS for user:", username)
        if _delete_namespace_user(username):
            error = True
    return error


def _get_httpd_php_fpm_service_override_files() -> Dict[str, str]:
    """
    Get list of all php-fpm services override files on server
    :return Dict. Example:
        {'ea-php74-php-fpm.service': '/etc/systemd/system/ea-php74-php-fpm.service.d/override.conf',
         'ea-php56-php-fpm.service': '/etc/systemd/system/ea-php56-php-fpm.service.d/override.conf'
        }
    """
    systemd_dir = '/usr/lib/systemd/system'
    # Scan available ea-php fpm services
    mask_to_search = os.path.join(systemd_dir, 'ea-php*-fpm.service')
    service_names = [os.path.basename(x) for x in glob.glob(mask_to_search)]
    # Scan available alt-php fpm services
    mask_to_search = os.path.join(systemd_dir, 'alt-php*-fpm.service')
    service_names.extend([os.path.basename(x) for x in glob.glob(mask_to_search)])
    # Add additional services - native php-fpm and httpd
    # DA doesn't register service in /usr/lib/systemd/service, so we need to check /etc/systemd/system
    add_services_names = ['php-fpm.service', 'httpd.service']
    for service_name in add_services_names:
        if os.path.exists(os.path.join(systemd_dir, service_name)) or (
            is_da() and os.path.exists(os.path.join('/etc/systemd/system', service_name))
        ):
            service_names.append(service_name)
    # Create override configs list
    override_file_dict = {service_name: '/etc/systemd/system/%s.d/zzz-cagefs.conf' % service_name
                          for service_name in service_names}
    return override_file_dict


def fix_httpd_php_fpm_services():
    """
    Reconfigure httpd and ea-php-fpm services to work in without LVE
    Write to each systemd service file directives:
            PrivateDevices=false
            PrivateMounts=false
            PrivateTmp=false
    """
    try:
        override_files_dict = _get_httpd_php_fpm_service_override_files()
        lines_to_write = ['[Service]\n', 'PrivateDevices=false\n', 'PrivateMounts=false\n', 'PrivateTmp=false\n',
                          'ProtectSystem=false\n', 'ReadOnlyDirectories=\n', 'ReadWriteDirectories=\n',
                          'InaccessibleDirectories=\n', 'ProtectHome=false\n'
                          ]
        for override_file in override_files_dict.values():
            # Create /etc/systemd/system/%s.d directory if need
            os.makedirs(os.path.dirname(override_file), mode=0o700, exist_ok=True)
            write_file_lines(override_file, lines_to_write, 'w')
        os.system('/bin/systemctl daemon-reload 2> /dev/null')
        # Restart all need services
        for service_name in override_files_dict.keys():
            os.system(f'/sbin/service {service_name} restart 2> /dev/null')
    except (OSError, IOError,):
        pass


def restore_httpd_php_fpm_services():
    try:
        override_files_dict = _get_httpd_php_fpm_service_override_files()
        # override_files_dict example:
        # {'ea-php74-php-fpm.service': '/etc/systemd/system/ea-php74-php-fpm.service.d/zzz-cagefs.conf',
        #  'ea-php56-php-fpm.service': '/etc/systemd/system/ea-php56-php-fpm.service.d/zzz-cagefs.conf'}
        for override_file in override_files_dict.values():
            # Remove override file
            try:
                os.unlink(override_file)
            except (OSError, IOError, ):
                pass
            # Remove override dir if it empty
            try:
                os.rmdir(os.path.dirname(override_file))
            except (OSError, IOError, ):
                pass
        os.system('/bin/systemctl daemon-reload 2> /dev/null')
        # Restart all need services
        for service_name in override_files_dict.keys():
            os.system(f'/sbin/service {service_name} restart 2> /dev/null')
    except (OSError, IOError,):
        pass


Hacked By AnonymousFox1.0, Coded By AnonymousFox