Mini Shell
Direktori : /usr/share/cagefs/ |
|
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
import psutil
from clcommon.lib import cledition
from clcommon.utils import run_command, ExternalProgramFailed, get_file_lines, write_file_lines
from cldetectlib import is_da
from clsentry import CLLIB_DSN, init_sentry_client
from clsentry.utils import get_pkg_version
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'
sentry_client = init_sentry_client(
'cagefs',
release=get_pkg_version('cagefs'),
dsn=CLLIB_DSN,
handle=False,
)
class LockFailedException(Exception):
pass
class NsNotFoundException(Exception):
pass
class MountCommandFailedException(Exception):
pass
class CagefsMountInvalidUserException(Exception):
pass
class CagefsMountPIDWriteFailed(Exception):
pass
class CagefsMountJailCallFailed(Exception):
pass
_EXIT_VALIDATION_ERROR = 1
_EXIT_JAIL_MOUNT_ERROR = 2
_EXIT_PID_WRITE_ERROR = 3
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 command <ns_id>
# COMMAND
# /bin/sh /usr/bin/mysqld_safe --basedir=/usr
# /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 'COMMAND' 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 example:
# '4026532195 /usr/sbin/pdns_server --socket-dir=/run/pdns --guardian=no --daemon=no ...'
ns_id, ns_command = line.split(' ', 1)
if ns_command.strip() == cmd_to_search:
# Command found, write NS id
write_file_via_tempfile(ns_id, filename_to_write, 0o600)
return
ns_id_list.append(ns_id)
# 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:
parent = psutil.Process(pid)
except psutil.NoSuchProcess:
logging(f'Process with PID {pid} is already dead, nothing to kill')
continue
children = parent.children(recursive=True)
children.append(parent)
for p in children:
try:
p.terminate()
except psutil.NoSuchProcess:
pass
_, alive = psutil.wait_procs(children, timeout=3)
if not alive:
continue
for p in alive:
try:
p.kill()
except psutil.NoSuchProcess:
pass
_, still_alive = psutil.wait_procs(alive, timeout=3)
if still_alive:
logging("Some processes are still alive after sending "
f"the SIGKILL signal: {still_alive}")
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 _is_pid_file_created_successfully(
pid_filename: str,
process: subprocess.Popen
) -> bool:
"""
Waits for cagefs-mount pid file appears up to 60 seconds
:param pid_filename: PID filename
:param process: subprocess that creates the PID file
:return: True - appears; False - Not
"""
for i in range(60000):
try:
os.stat(pid_filename)
return True
except (OSError, IOError):
# Error, PID file absent
# Check if subprocess is still alive
if process.poll() is not None:
return False
time.sleep(0.001)
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 = []
pid_file_created = False
cagefs_mount_time_taken = 0
cagefs_mount_timeout = 20 # seconds
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
# 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
proc = subprocess.Popen(
[_CAGEFS_MOUNT_BIN_FILE, username],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
text=True,
)
cagefs_mount_pid = proc.pid
background_processes.append(cagefs_mount_pid)
# We should wait while binary creates cagefs mounts
start_time = time.time()
pid_file_created = _is_pid_file_created_successfully(cagefs_pid_file, proc)
cagefs_mount_time_taken = time.time() - start_time
if not pid_file_created:
_kill_processes_in_list(background_processes)
stdout, stderr = proc.communicate()
stdout, stderr = stdout.strip(), stderr.strip()
if proc.returncode == _EXIT_VALIDATION_ERROR:
raise CagefsMountInvalidUserException(
f"Input arguments validation failed in cagefs-mount for user={username}.\n"
f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"'
)
elif proc.returncode == _EXIT_JAIL_MOUNT_ERROR:
raise CagefsMountJailCallFailed(
f"Can't create namespace {ns_mnt_filename} for user {username}.\n"
f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"'
)
elif proc.returncode == _EXIT_PID_WRITE_ERROR:
raise CagefsMountPIDWriteFailed(
f"cagefs-mount created namespace for user={username}, but was unable to write pidfile.\n"
f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"'
)
raise CagefsMountNotStartedException(
f'{cagefs_pid_file} was not created within the expected time frame.\n'
f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"'
)
# /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],
capture_output=True,
text=True,
shell=False,
)
if res.returncode != 0:
raise MountCommandFailedException(
f"Can't mount {ns_mnt_filename} for user {username}.\n"
f'STDOUT: "{res.stdout}"\nSTDERR: "{res.stderr}"'
)
# 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 (
CagefsMountNotStartedException,
MountCommandFailedException,
CagefsMountInvalidUserException,
CagefsMountJailCallFailed,
CagefsMountPIDWriteFailed,
) as e:
error = True
logging(str(e))
sentry_client.captureException()
except Exception:
error = True
msg = traceback.format_exc()
logging(f"General error while attempting to create a namespace for user {username}. Error is: {msg}")
sentry_client.captureException()
finally:
# Kill cagefs-mount process
_kill_processes_in_list(background_processes)
if pid_file_created and cagefs_mount_time_taken > cagefs_mount_timeout:
stdout, stderr = proc.communicate()
sentry_client.captureMessage(
f'{cagefs_pid_file} file was created after {cagefs_mount_time_taken:.2f}.\n'
f'STDOUT: "{stdout}"\nSTDERR: "{stderr}"'
)
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
res = subprocess.run(
[_UMOUNT_BIN_FILE, ns_mnt_filename],
capture_output=True,
text=True,
shell=False,
env={**os.environ, **{'LC_ALL': 'C'}},
)
if res.returncode != 0 and 'not mounted' not in res.stderr:
raise MountCommandFailedException(
f"Can't umount {ns_mnt_filename} for user {username}.\n"
f'STDOUT: "{res.stdout}"\nSTDERR: "{res.stderr}"'
)
# rf -f /var/cagefs.uid/$PREFIX/$UID/ns.mnt
os.unlink(ns_mnt_filename)
except MountCommandFailedException as e:
# NOTE: Don't set `error` to `True` to preserve the previous behavior,
# but send errors to Sentry to determine if there are any actual issues
# with this command and decide if something needs to be fixed
logging(str(e))
sentry_client.captureException()
except (LockFailedException, ) as e:
logging(f"Can't acqure lock for user {username}. Error is: {str(e)}")
error = True
sentry_client.captureException()
except Exception:
msg = traceback.format_exc()
logging(f"Error during delete namespace for user {username}. Error is:\n{msg}")
error = True
sentry_client.captureException()
if lock_obj:
_release_lock(lock_obj)
return error
def create_namespace_user_list(username_list: list[str], verbose = False) -> int:
"""
Create namespace for users from list
:param username_list: username list for prosess
:param verbose: prints log in stdout if set
"""
errors = 0
for username in username_list:
if verbose:
print("Creating NS for user:", username)
if _create_namespace_user(username):
errors += 1
return errors
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
Zerion Mini Shell 1.0