# 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 AccelerateWP daemon
# pylint: disable=no-absolute-import
import json
import logging
import pwd
import os
import stat
import subprocess
import signal
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.cpapi import get_main_username_by_uid
from clwpos.constants import REDIS_SERVER_BIN_FILE
from clwpos.cl_wpos_exceptions import WposError
from clwpos.utils import (
USER_WPOS_DIR,
is_run_under_user,
drop_permissions_if_needed,
run_in_cagefs_if_needed
)
from clcommon.cpapi.cpapiexceptions import NoPackage
from clwpos import gettext as _
logger = logging.getLogger(__name__)
_REDIS_CLI_BIN_FILE = '/opt/alt/redis/bin/redis-cli'
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
"""
# in most cases this is correct path
fuser_binary = '/usr/bin/fuser'
# fallback to prev approach
if not os.path.exists(fuser_binary):
fuser_binary = '/sbin/fuser'
try:
# # /usr/sbin/fuser /home/cltest1/.clwpos/redis.sock
# /home/cltest1/.clwpos/redis.sock: 55882 [105766 251507]
std_out = run_command([fuser_binary, 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) -> 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: Logger, pid: int):
"""
Kill process by pid
:param _logger: Logger to log errors
:param pid: Process pid to kill
"""
if not is_run_under_user():
raise WposError("Internal error! Trying to kill process with root privileges")
try:
os.kill(pid, signal.SIGTERM) # 15
time.sleep(5)
try:
os.kill(pid, signal.SIGKILL) # 9
except OSError:
pass
except OSError as e:
_logger.warning("Can't kill redis process, pid %s; error: %s", pid, str(e))
_logger.info('Killed process with pid=%s', str(pid))
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)
logger.info('Killing redis with pid=%s for user=%s', str(redis_pid_list), username)
with drop_privileges(username):
for redis_pid in redis_pid_list:
kill_process_by_pid(logger, redis_pid)
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 redis_socket_health_check(uid: int) -> bool:
"""
/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:
username = get_main_username_by_uid(uid)
user_pwd = pwd.getpwnam(username)
except KeyError:
logger.warning("Redis check error for user %s. No user with such uid", str(uid))
return False
redis_socket_path = os.path.join(user_pwd.pw_dir, USER_WPOS_DIR, 'redis.sock')
redis_ping_cmd = [_REDIS_CLI_BIN_FILE, '-s', redis_socket_path, 'ping']
with drop_permissions_if_needed(username):
output = run_in_cagefs_if_needed(redis_ping_cmd)
logger.info('Redis health check for user=%s, return code=%s, stdout=%s, stderr=%s',
username,
str(output.returncode),
str(output.stdout),
str(output.stderr))
return output.returncode == 0
def is_user_redis_alive(user_id: int) -> Tuple[bool, bool, dict]:
"""
Check user's redis is alive
: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": "..."})
"""
try:
user_pwd = pwd.getpwuid(user_id)
username = user_pwd.pw_name
except KeyError:
logger.warning("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:
is_redis_alive = redis_socket_health_check(user_id)
except Exception as e:
logger.warning("Redis check error for user %s. Error is: %s", username, str(e))
return False, True, { "result": _("Redis CLI start error %(error)s for user %(user)s"),
"context": { "error": str(e), "user": username } }
if not is_redis_alive:
# Process start error
return False, True, {"result": _("Redis CLI check error %(error)s for user %(user)s"),
"context": {"error": "Redis is not pingable for user, most likely it is not started",
"user": username}}
return True, True, {"result": "success"}
# A pid file holds a single decimal pid; cap the read so a hostile file
# planted at the (user-owned) pid path cannot exhaust the root daemon's memory.
_REDIS_PID_FILE_MAX_BYTES = 32
def _read_pid_from_pid_file(redis_pid_filename: str, expected_uid: Optional[int]) -> int:
"""
Read and parse the redis pid from the pid file in a hostile-input-safe way.
The pid path lives under the user-owned