|
Server : Apache System : Linux cvar2.toservers.com 3.10.0-962.3.2.lve1.5.73.el7.x86_64 #1 SMP Wed Aug 24 21:31:23 UTC 2022 x86_64 User : njnconst ( 1116) PHP Version : 8.4.18 Disable Function : NONE Directory : /proc/self/root/opt/alt-old/python37/lib/python3.7/site-packages/clwpos/ |
Upload File : |
# coding=utf-8
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
# Redis manipulation library for Cloudlinux WPOS daemon
# pylint: disable=no-absolute-import
import json
import pwd
import os
import traceback
import subprocess
import signal
import shutil
import psutil
import time
from logging import Logger
from typing import List, Optional, Tuple
from clcommon.clpwd import drop_privileges
from clcommon.utils import run_command, ExternalProgramFailed, is_user_present
from clcommon.cpapi import cpusers
from clcommon.lib.cledition import is_cl_solo_edition
from clcommon.clpwd import ClPwd
from clwpos.constants import REDIS_SERVER_BIN_FILE
from clwpos.cl_wpos_exceptions import WposError, WpUserMissing
from clwpos.utils import USER_WPOS_DIR, demote
from clcommon.cpapi.cpapiexceptions import NoPackage
_REDIS_CLI_BIN_FILE = '/opt/alt/redis/bin/redis-cli'
def _remove_clwpos_dir_for_user(_logger: Logger, username: str):
"""
Remove /home/<user>/.clwpod dir with all content
:param username: User name to remove dir
"""
try:
with drop_privileges(username):
# We are user here
try:
user_pwd = pwd.getpwnam(username)
# /home/username/.clwpos/
shutil.rmtree(os.path.join(user_pwd.pw_dir, USER_WPOS_DIR))
except (OSError, IOError):
_logger.exception("Can't remove .clwpos directory for user %s", username)
except ClPwd.NoSuchUserException:
raise WpUserMissing(username)
def _get_pids_for_file(file_path: str) -> List[int]:
"""
Retrieves list of PID list processes, which uses file (using fuser utility)
This can find any process (for example php), not only redis service process
:param file_path: Filename to check
:return: PID list
"""
try:
# # /usr/sbin/fuser /home/cltest1/.clwpos/redis.sock
# /home/cltest1/.clwpos/redis.sock: 55882 [105766 251507]
std_out = run_command(['/sbin/fuser', file_path], return_full_output=False)
lines_list = std_out.split('\n')
# Get PID list from output
s_pid_list = lines_list[0].split(':')[1].strip()
pid_list = []
for s_pid in s_pid_list.split(' '):
try:
pid_list.append(int(s_pid.strip()))
except ValueError:
pass
return pid_list
except (ExternalProgramFailed, IndexError):
pass
return []
def _get_user_pids(username: str) -> List[int]:
"""
Update PID list in cache for user using /bin/ps utility
:param: username: Username to scan
:return: None
"""
# /bin/ps -o"pid" -u cltest1
# PID
# 1608661
# 1638657
# ......
# Get user's PID list
try:
std_out = run_command(['/bin/ps', '-o', 'pid', '-u', username], return_full_output=False)
except ExternalProgramFailed:
return []
lines_list = std_out.split('\n')
if len(lines_list) < 2:
return []
# Remove header line
user_pid_list = []
lines_list = lines_list[1:]
for line in lines_list:
line = line.strip()
if line:
try:
user_pid_list.append(int(line.strip()))
except ValueError:
pass
return user_pid_list
def _get_user_redis_pids(username: str, home_dir: str) -> Optional[List[int]]:
"""
Get redis PID list for user
:param username: user name
:param home_dir: User's homedir
:return: PID list or [] if user has no redis
"""
redis_socket_file = os.path.join(home_dir, USER_WPOS_DIR, 'redis.sock')
pid_list_sock = _get_pids_for_file(redis_socket_file)
user_pids = _get_user_pids(username)
pid_list = []
for pid in pid_list_sock:
if pid in user_pids:
pid_list.append(pid)
return pid_list
def kill_process_by_pid(_logger: Optional[Logger], pid: int):
"""
Kill process by pid
:param _logger: Logger to log errors
:param pid: Process pid to kill
"""
try:
os.kill(pid, signal.SIGTERM) # 15
time.sleep(5)
try:
os.kill(pid, signal.SIGKILL) # 9
except OSError:
pass
except OSError as e:
# No such process
if _logger:
_logger.warning("Can't kill redis process, pid %s; error: %s", pid, str(e))
def _kill_all_redises_for_user(logger: Logger, username: str):
"""
Kill all user's redice processes
:param logger: Logger to log errors
:param username: User name
"""
if not is_user_present(username):
return
user_pwd = pwd.getpwnam(username)
redis_pid_list = _get_user_redis_pids(user_pwd.pw_name, user_pwd.pw_dir)
for redis_pid in redis_pid_list:
kill_process_by_pid(logger, redis_pid)
def remove_clwpos_dir_for_all_users(logger: Logger):
"""
Remove .clwpos dir for all panel users
:param logger: Logger to log errors
"""
try:
users = cpusers()
except (OSError, IOError, IndexError, NoPackage) as e:
# Log all cpapi.userdomains errors
logger.warning("Can't get user list from panel: %s", str(e))
return
for username in users:
try:
_remove_clwpos_dir_for_user(logger, username)
except WposError as e:
# Skip user not found error
logger.warning("User %s present in panel but absent in system. Error is %s", username, str(e))
def kill_all_users_redises(logger: Logger):
"""
Find and kill lost redices for all panel users
:param logger: Daemon's logger
"""
try:
users = cpusers()
except (OSError, IOError, IndexError, NoPackage) as e:
logger.warning("Can't get user list from panel: %s", str(e))
return
for username in users:
_kill_all_redises_for_user(logger, username)
def is_user_redis_alive(_logger: Logger, user_id: int) -> Tuple[bool, bool, dict]:
"""
Check user's redis is alive
:param _logger: Daemon's logger
:param user_id: uid to check sockets
return True/False - redis alive/not alive
:return: Tuple: (redis is working/not working, is user present, errors dict)
error - (False, False {"result": "error", "context": "..."})
"""
# TODO: Refactor this in https://cloudlinux.atlassian.net/browse/LU-2506
# # /opt/alt/redis/bin/redis-cli -s /home/cltest1/.clwpos/redis.sock ping
# Could not connect to Redis at /home/cltest1/.clwpos/redis.sock: No such file or directory
# # echo $?
# 1
# # /opt/alt/redis/bin/redis-cli -s /home/cltest1/.clwpos/redis.sock ping
# PONG
# # echo $?
# 0
try:
user_pwd = pwd.getpwuid(user_id)
username = user_pwd.pw_name
except KeyError:
_logger.debug("Redis check error for user %s. No user with such uid", str(user_id))
return False, False, {"result": "Redis check error for user with uid %(uid)s. No such user",
"context": {"uid": str(user_id)}}
try:
redis_socket_path = os.path.join(user_pwd.pw_dir, USER_WPOS_DIR, 'redis.sock')
if is_cl_solo_edition(skip_jwt_check=True):
# CL Solo
cmd = [_REDIS_CLI_BIN_FILE, '-s', redis_socket_path, 'ping']
proc = subprocess.Popen(cmd, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
preexec_fn=demote(user_pwd.pw_uid, user_pwd.pw_gid),
cwd=user_pwd.pw_dir)
else:
proc = subprocess.Popen(['/sbin/cagefs_enter_user', '--no-fork', username,
_REDIS_CLI_BIN_FILE, '-s', redis_socket_path, 'ping'],
cwd=user_pwd.pw_dir,
shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout_data, stderr_data = proc.communicate()
if proc.returncode != 0:
# Process start error
return False, True, {"result": "Redis CLI start error %(error)s for user %(user)s",
"context": {"error": f"Error happened while checking redis "
f"for user '%(user)s'. stdout: '{stdout_data}'; "
f"stderr: '{stderr_data}'",
"user": username}}
return proc.returncode == 0, True, {"result": "success"}
except (ExternalProgramFailed, ClPwd.NoSuchUserException, IOError, OSError) as e:
str_exc = traceback.format_exc()
_logger.debug("Redis check error for user %s. Error is: %s", username, str_exc)
return False, True, {"result": "Redis CLI start error %(error)s for user %(user)s",
"context": {"error": str(e), "user": username}}
def _get_redis_pid_from_pid_file_with_wait(redis_pid_filename: str) -> Optional[int]:
"""
Get redis process PID from redis pid file. Wait up to 10 seconds
:param redis_pid_filename: Redis PID filename
:return: Redis PID or None on error (pid file absent/invalid or redis not started)
"""
for i in range(100):
try:
with open(redis_pid_filename, 'r') as f:
pid = int(f.read().strip())
os.kill(pid, 0)
return pid
except (OSError, IOError, ValueError):
# Error, PID file absent/invalid or redis still absent
time.sleep(0.1)
# Error, redis not started or pid file read/parse error
return None
def reload_redis_for_user_thread(_logger: Logger, username: str,
old_redis_pid: Optional[int]) -> Tuple[Optional[int], dict]:
"""
Reloads redis for supplied user via helper script. Should be trun in thread
:param _logger: Daemon's logger
:param username: Username to setup redis
:param old_redis_pid: Old Redis PID for kill
:return: Tuple:
If redis was started for user - (PID of new redis process, {"result": "success"})
else - redis was not started - (None, {"result": "error", "context": ""})
"""
try:
user_pwd = pwd.getpwnam(username)
except (KeyError, OSError, ):
_logger.debug("Can't reload redis for user '%s'. User not found.", username)
return None, {"result": "Can't reload redis for user '%(user)s'. User not found.",
"context": {"user": username}}
try:
# Run redis_reloader_script
proc = subprocess.Popen(['/usr/share/cloudlinux/wpos/redis_reloader.py', username, str(old_redis_pid)],
shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
stdout, stderr = proc.communicate()
if stderr and stderr != '':
return None, {"result": "Reload redis for user '%(user)s' error: %(error_msg)s",
"context": {"user": username, "error_msg": stderr}}
except (OSError, IOError,) as e:
_logger.debug("Reload redis error for user '%s'. Error is %s", username, str(e))
return None, {"result": "Reload redis error for user '%(user)s'. Error is %(msg)s",
"context": {"user": username, "msg": str(e)}}
try:
reload_result_dict = json.loads(stdout)
if reload_result_dict['result'] != 'success':
return None, {"result": "Reload redis for user '%(user)s' error: %(error_msg)s",
"context": {"user": username, "error_msg": reload_result_dict['result']}}
except (KeyError, json.JSONDecodeError, TypeError):
return None, {"result": "Reload redis for user '%(user)s' decode error: %(error_msg)s",
"context": {"user": username, "error_msg": stdout}}
# Redis was started, get PID
pidfile_path = os.path.join(user_pwd.pw_dir, USER_WPOS_DIR, 'redis.pid')
redis_pid = _get_redis_pid_from_pid_file_with_wait(pidfile_path)
return redis_pid, {"result": "success"}
def parse_redises() -> List[Tuple[int, int]]:
"""
Get redis process by parsing psutil.process_iter
Return list of tuples: [(user_uid, process_pid)]
"""
res = []
for proc in psutil.process_iter(['name']):
if proc.info['name'] == 'redis-server':
res.append(_validate_redis_proc(proc))
return list(filter(None, res))
def _validate_redis_proc(p: psutil.Process) -> Optional[Tuple[int, int]]:
"""
Ensure that redis process is ours:
1. Right binary (alt-redis)
2. Right socket
"""
redis_bin = REDIS_SERVER_BIN_FILE
uid = p.uids().real
pw = pwd.getpwuid(uid)
user_home = pw.pw_dir
sock_abspath = f'unixsocket:{user_home}/.clwpos/redis.sock'
cmd = ' '.join(p.cmdline())
if cmd.startswith(redis_bin) and sock_abspath in cmd:
return uid, p.pid
return None