mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-12-15 12:59:42 +01:00
Detect RHEL 8, AlmaLinux 8, Rocky Linux 8, and CloudLinux 8 systems and provide clear instructions when cgroups v2 needs manual enablement. These systems have cgroups v2 backported to kernel 4.18 but it's disabled by default. When detected without cgroups v2 enabled, the system now: 1. Detects RHEL 8 family by checking /etc/redhat-release 2. Verifies if cgroups v2 is mounted (checks 'mount' output for 'cgroup2') 3. If not enabled, logs detailed instructions: - grubby command to add kernel parameter - Reboot instruction - Verification command - Clear step-by-step guide Changes: - _check_rhel8_cgroups_v2(): New method for RHEL 8 family detection - _ensure_cgroups_enabled(): Calls RHEL 8 check before general checks - check_cgroup_support(): Returns RHEL 8 status in support dict - rhel8_family: bool (detected RHEL 8 family) - rhel8_needs_enablement: bool (cgroups v2 not mounted) - os_name: str (full OS name from release file) OS Support Status: ✅ Ubuntu 20.04+ - Native cgroups v2 (kernel 5.4+) ✅ RHEL/Alma/Rocky 9+ - Native cgroups v2 (kernel 5.14+) ⚠️ RHEL/Alma/Rocky/CloudLinux 8 - Needs manual enable (kernel 4.18 backported)
557 lines
21 KiB
Python
557 lines
21 KiB
Python
#!/usr/local/CyberCP/bin/python
|
|
"""
|
|
CyberPanel Resource Limits Manager
|
|
Handles resource limits using OpenLiteSpeed native cgroups v2 integration
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import logging as log
|
|
from pathlib import Path
|
|
|
|
# Django imports
|
|
import sys
|
|
sys.path.append('/usr/local/CyberCP')
|
|
import django
|
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings")
|
|
django.setup()
|
|
|
|
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
|
|
|
|
|
|
class ResourceLimitsManager:
|
|
"""
|
|
Manages resource limits for websites using OpenLiteSpeed native cgroups v2 API
|
|
This uses the lscgctl command to set per-user limits, which OLS enforces automatically
|
|
"""
|
|
|
|
# Path to OLS cgroups control tool
|
|
LSCGCTL_PATH = "/usr/local/lsws/lsns/bin/lscgctl"
|
|
LSSETUP_PATH = "/usr/local/lsws/lsns/bin/lssetup"
|
|
OLS_CONF_PATH = "/usr/local/lsws/conf/httpd_config.conf"
|
|
|
|
def __init__(self):
|
|
"""Initialize the resource limits manager"""
|
|
self._initialized = False
|
|
|
|
def _check_rhel8_cgroups_v2(self):
|
|
"""
|
|
Check if RHEL 8 family needs manual cgroups v2 enablement
|
|
|
|
RHEL 8, AlmaLinux 8, Rocky Linux 8, and CloudLinux 8 have cgroups v2
|
|
backported to kernel 4.18 but it's disabled by default.
|
|
|
|
Returns:
|
|
bool: True if cgroups v2 is available or not RHEL 8, False if needs enablement
|
|
"""
|
|
try:
|
|
# Check if this is a RHEL 8 family system
|
|
redhat_release_paths = ['/etc/redhat-release', '/etc/system-release']
|
|
is_rhel8 = False
|
|
os_name = "Unknown"
|
|
|
|
for release_file in redhat_release_paths:
|
|
if os.path.exists(release_file):
|
|
try:
|
|
with open(release_file, 'r') as f:
|
|
release_content = f.read().lower()
|
|
os_name = release_content.strip()
|
|
|
|
# Check for RHEL 8 family (RHEL, AlmaLinux, Rocky, CloudLinux, CentOS 8)
|
|
if ('release 8' in release_content or
|
|
'release 8.' in release_content):
|
|
if any(distro in release_content for distro in
|
|
['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']):
|
|
is_rhel8 = True
|
|
break
|
|
except:
|
|
pass
|
|
|
|
if not is_rhel8:
|
|
# Not RHEL 8 family, no special handling needed
|
|
return True
|
|
|
|
# This is RHEL 8 family - check if cgroups v2 is actually enabled
|
|
logging.writeToFile(f"Detected RHEL 8 family system: {os_name}")
|
|
|
|
# Check if cgroups v2 is mounted (indicates it's enabled)
|
|
try:
|
|
result = subprocess.run(
|
|
['mount'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5
|
|
)
|
|
|
|
if 'cgroup2' in result.stdout:
|
|
logging.writeToFile("cgroups v2 is enabled on RHEL 8 family system")
|
|
return True
|
|
else:
|
|
# cgroups v2 is not enabled - provide instructions
|
|
logging.writeToFile("=" * 80)
|
|
logging.writeToFile("RHEL 8 FAMILY: cgroups v2 MANUAL ENABLEMENT REQUIRED")
|
|
logging.writeToFile("=" * 80)
|
|
logging.writeToFile(f"System: {os_name}")
|
|
logging.writeToFile(f"Kernel: {os.uname().release}")
|
|
logging.writeToFile("")
|
|
logging.writeToFile("RHEL 8, AlmaLinux 8, Rocky Linux 8, and CloudLinux 8 have cgroups v2")
|
|
logging.writeToFile("backported but disabled by default. To enable, run these commands:")
|
|
logging.writeToFile("")
|
|
logging.writeToFile("1. Enable cgroups v2 in boot parameters:")
|
|
logging.writeToFile(" grubby --update-kernel=ALL --args='systemd.unified_cgroup_hierarchy=1'")
|
|
logging.writeToFile("")
|
|
logging.writeToFile("2. Reboot the system:")
|
|
logging.writeToFile(" reboot")
|
|
logging.writeToFile("")
|
|
logging.writeToFile("3. After reboot, verify cgroups v2 is enabled:")
|
|
logging.writeToFile(" mount | grep cgroup2")
|
|
logging.writeToFile("")
|
|
logging.writeToFile("4. Then create websites with resource limits")
|
|
logging.writeToFile("=" * 80)
|
|
return False
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error checking cgroups v2 mount status: {str(e)}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error checking RHEL 8 family status: {str(e)}")
|
|
# If we can't detect, assume it's OK and let the normal checks proceed
|
|
return True
|
|
|
|
def _ensure_cgroups_enabled(self):
|
|
"""
|
|
Ensure OpenLiteSpeed cgroups are enabled
|
|
This performs automatic setup if needed
|
|
|
|
Returns:
|
|
bool: True if cgroups are enabled, False otherwise
|
|
"""
|
|
if self._initialized:
|
|
return True
|
|
|
|
try:
|
|
# Special check for RHEL 8 family systems
|
|
if not self._check_rhel8_cgroups_v2():
|
|
return False
|
|
|
|
# Check kernel support first
|
|
if not os.path.exists('/sys/fs/cgroup/cgroup.controllers'):
|
|
logging.writeToFile("cgroups v2 not available on this system (requires kernel 5.2+)")
|
|
logging.writeToFile("For RHEL 8 family, see instructions above to enable cgroups v2")
|
|
return False
|
|
|
|
# Check if lscgctl exists
|
|
if not os.path.exists(self.LSCGCTL_PATH):
|
|
logging.writeToFile("lscgctl not found, attempting to run lssetup...")
|
|
|
|
# Try to run lssetup
|
|
if os.path.exists(self.LSSETUP_PATH):
|
|
result = subprocess.run(
|
|
[self.LSSETUP_PATH],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
logging.writeToFile("lssetup completed successfully")
|
|
else:
|
|
logging.writeToFile(f"lssetup failed: {result.stderr}")
|
|
return False
|
|
else:
|
|
logging.writeToFile(f"lssetup not found at {self.LSSETUP_PATH}")
|
|
return False
|
|
|
|
# Check if cgroups are enabled in OLS config
|
|
if not self._check_ols_cgroups_enabled():
|
|
logging.writeToFile("Enabling cgroups in OpenLiteSpeed configuration...")
|
|
if not self._enable_ols_cgroups():
|
|
return False
|
|
|
|
self._initialized = True
|
|
logging.writeToFile("OpenLiteSpeed cgroups support ready")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error ensuring cgroups enabled: {str(e)}")
|
|
return False
|
|
|
|
def _check_ols_cgroups_enabled(self):
|
|
"""
|
|
Check if cgroups are enabled in OpenLiteSpeed config
|
|
|
|
Returns:
|
|
bool: True if enabled, False otherwise
|
|
"""
|
|
try:
|
|
if not os.path.exists(self.OLS_CONF_PATH):
|
|
logging.writeToFile(f"OLS config not found at {self.OLS_CONF_PATH}")
|
|
return False
|
|
|
|
with open(self.OLS_CONF_PATH, 'r') as f:
|
|
config = f.read()
|
|
|
|
# Look for CGIRLimit section and check cgroups value
|
|
# Pattern: cgroups followed by whitespace and value
|
|
import re
|
|
|
|
# Find CGIRLimit section
|
|
cgirlimit_match = re.search(r'CGIRLimit\s*\{([^}]+)\}', config, re.DOTALL)
|
|
if not cgirlimit_match:
|
|
logging.writeToFile("CGIRLimit section not found in OLS config")
|
|
return False
|
|
|
|
cgirlimit_section = cgirlimit_match.group(1)
|
|
|
|
# Check for cgroups setting
|
|
cgroups_match = re.search(r'cgroups\s+(\d+)', cgirlimit_section)
|
|
if cgroups_match:
|
|
value = int(cgroups_match.group(1))
|
|
# 1 = On, 0 = Off, 2 = Disabled
|
|
if value == 1:
|
|
logging.writeToFile("cgroups already enabled in OLS config")
|
|
return True
|
|
else:
|
|
logging.writeToFile(f"cgroups is set to {value} (need 1 for enabled)")
|
|
return False
|
|
else:
|
|
logging.writeToFile("cgroups setting not found in CGIRLimit section")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error checking OLS cgroups config: {str(e)}")
|
|
return False
|
|
|
|
def _enable_ols_cgroups(self):
|
|
"""
|
|
Enable cgroups in OpenLiteSpeed configuration
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
try:
|
|
if not os.path.exists(self.OLS_CONF_PATH):
|
|
return False
|
|
|
|
# Read the config file
|
|
with open(self.OLS_CONF_PATH, 'r') as f:
|
|
config = f.read()
|
|
|
|
import re
|
|
|
|
# Find CGIRLimit section
|
|
cgirlimit_match = re.search(r'(CGIRLimit\s*\{[^}]+\})', config, re.DOTALL)
|
|
if not cgirlimit_match:
|
|
logging.writeToFile("CGIRLimit section not found, cannot enable cgroups")
|
|
return False
|
|
|
|
old_section = cgirlimit_match.group(1)
|
|
|
|
# Check if cgroups line exists
|
|
if re.search(r'cgroups\s+\d+', old_section):
|
|
# Replace existing cgroups value with 1
|
|
new_section = re.sub(r'cgroups\s+\d+', 'cgroups 1', old_section)
|
|
else:
|
|
# Add cgroups line before the closing brace
|
|
new_section = old_section.replace('}', ' cgroups 1\n}')
|
|
|
|
# Replace in config
|
|
new_config = config.replace(old_section, new_section)
|
|
|
|
# Backup original config
|
|
backup_path = self.OLS_CONF_PATH + '.backup'
|
|
with open(backup_path, 'w') as f:
|
|
f.write(config)
|
|
|
|
# Write new config
|
|
with open(self.OLS_CONF_PATH, 'w') as f:
|
|
f.write(new_config)
|
|
|
|
logging.writeToFile("Enabled cgroups in OLS config, restarting OpenLiteSpeed...")
|
|
|
|
# Graceful restart of OLS
|
|
result = subprocess.run(
|
|
['/usr/local/lsws/bin/lswsctrl', 'restart'],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
logging.writeToFile("OpenLiteSpeed restarted successfully")
|
|
return True
|
|
else:
|
|
logging.writeToFile(f"Failed to restart OpenLiteSpeed: {result.stderr}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error enabling OLS cgroups: {str(e)}")
|
|
return False
|
|
|
|
def set_user_limits(self, username, package):
|
|
"""
|
|
Set resource limits for a Linux user using OpenLiteSpeed lscgctl
|
|
|
|
Args:
|
|
username (str): Linux username (e.g., website owner)
|
|
package (Package): Package model instance with resource limits
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
# Skip if limits not enforced
|
|
if not package.enforceDiskLimits:
|
|
logging.writeToFile(f"Resource limits not enforced for {username} (enforceDiskLimits=0)")
|
|
return True
|
|
|
|
# Ensure cgroups are enabled (auto-setup if needed)
|
|
if not self._ensure_cgroups_enabled():
|
|
logging.writeToFile(f"cgroups not available, skipping resource limits for {username}")
|
|
return False
|
|
|
|
try:
|
|
# Convert package limits to lscgctl format
|
|
# CPU: convert cores to percentage (1 core = 100%, 2 cores = 200%, etc.)
|
|
cpu_percent = package.cpuCores * 100
|
|
|
|
# Memory: convert MB to format with M suffix
|
|
memory_limit = f"{package.memoryLimitMB}M"
|
|
|
|
# Tasks: use procHardLimit as max tasks
|
|
max_tasks = package.procHardLimit
|
|
|
|
# Build lscgctl command
|
|
# Format: lscgctl set username --cpu 100 --mem 1024M --tasks 500
|
|
cmd = [
|
|
self.LSCGCTL_PATH,
|
|
'set',
|
|
username,
|
|
'--cpu', str(cpu_percent),
|
|
'--mem', memory_limit,
|
|
'--tasks', str(max_tasks)
|
|
]
|
|
|
|
# Note: I/O limits may require additional configuration
|
|
# Check if lscgctl supports --io parameter
|
|
|
|
logging.writeToFile(f"Setting limits for user {username}: CPU={cpu_percent}%, MEM={memory_limit}, TASKS={max_tasks}")
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
logging.writeToFile(f"Successfully set resource limits for {username}")
|
|
return True
|
|
else:
|
|
error_msg = result.stderr if result.stderr else result.stdout
|
|
logging.writeToFile(f"Failed to set limits for {username}: {error_msg}")
|
|
return False
|
|
|
|
except subprocess.TimeoutExpired:
|
|
logging.writeToFile(f"Timeout setting resource limits for {username}")
|
|
return False
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error setting resource limits for {username}: {str(e)}")
|
|
return False
|
|
|
|
def remove_user_limits(self, username):
|
|
"""
|
|
Remove resource limits for a Linux user
|
|
|
|
Args:
|
|
username (str): Linux username
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
if not os.path.exists(self.LSCGCTL_PATH):
|
|
logging.writeToFile(f"lscgctl not available, skipping limit removal for {username}")
|
|
return False
|
|
|
|
try:
|
|
# Use lscgctl to remove limits
|
|
# Format: lscgctl remove username
|
|
cmd = [self.LSCGCTL_PATH, 'remove', username]
|
|
|
|
logging.writeToFile(f"Removing resource limits for user {username}")
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
logging.writeToFile(f"Successfully removed resource limits for {username}")
|
|
return True
|
|
else:
|
|
error_msg = result.stderr if result.stderr else result.stdout
|
|
# It's not critical if removal fails (user may not have had limits)
|
|
logging.writeToFile(f"Note: Could not remove limits for {username}: {error_msg}")
|
|
return True
|
|
|
|
except subprocess.TimeoutExpired:
|
|
logging.writeToFile(f"Timeout removing resource limits for {username}")
|
|
return False
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error removing resource limits for {username}: {str(e)}")
|
|
return False
|
|
|
|
def get_user_limits(self, username):
|
|
"""
|
|
Get current resource limits for a Linux user
|
|
|
|
Args:
|
|
username (str): Linux username
|
|
|
|
Returns:
|
|
dict: Current limits or None
|
|
"""
|
|
if not os.path.exists(self.LSCGCTL_PATH):
|
|
return None
|
|
|
|
try:
|
|
# Use lscgctl to get limits
|
|
# Format: lscgctl get username
|
|
cmd = [self.LSCGCTL_PATH, 'get', username]
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
# Parse the output (format may vary)
|
|
return {'output': result.stdout.strip()}
|
|
else:
|
|
return None
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error getting resource limits for {username}: {str(e)}")
|
|
return None
|
|
|
|
def set_inode_limit(self, domain, username, inode_limit):
|
|
"""
|
|
Set inode (file count) limit for a website using filesystem quotas
|
|
|
|
Args:
|
|
domain (str): Website domain name
|
|
username (str): System username for the website
|
|
inode_limit (int): Maximum number of files/directories
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
try:
|
|
# Check if quota tools are available
|
|
result = subprocess.run(
|
|
['which', 'setquota'],
|
|
capture_output=True,
|
|
timeout=5
|
|
)
|
|
if result.returncode != 0:
|
|
logging.writeToFile("setquota command not found, skipping inode limit")
|
|
return False
|
|
|
|
# Set inode quota using setquota
|
|
# Format: setquota -u username 0 0 soft_inode hard_inode /
|
|
result = subprocess.run(
|
|
['setquota', '-u', username, '0', '0',
|
|
str(inode_limit), str(inode_limit), '/'],
|
|
check=True,
|
|
capture_output=True,
|
|
timeout=10
|
|
)
|
|
logging.writeToFile(f"Set inode limit for {domain} ({username}): {inode_limit}")
|
|
return True
|
|
except subprocess.TimeoutExpired:
|
|
logging.writeToFile(f"Timeout setting inode limit for {domain}")
|
|
return False
|
|
except subprocess.CalledProcessError as e:
|
|
logging.writeToFile(f"Failed to set inode limit: {e.stderr.decode() if e.stderr else str(e)}")
|
|
return False
|
|
except Exception as e:
|
|
logging.writeToFile(f"Failed to set inode limit: {str(e)}")
|
|
return False
|
|
|
|
|
|
def check_cgroup_support(self):
|
|
"""
|
|
Check if OpenLiteSpeed cgroups v2 support is available
|
|
|
|
Returns:
|
|
dict: Support status for various features
|
|
"""
|
|
support = {
|
|
'cgroups_v2': False,
|
|
'lscgctl_available': False,
|
|
'memory_controller': False,
|
|
'cpu_controller': False,
|
|
'io_controller': False,
|
|
'quota_tools': False,
|
|
'rhel8_family': False,
|
|
'rhel8_needs_enablement': False,
|
|
'os_name': 'Unknown'
|
|
}
|
|
|
|
try:
|
|
# Check for RHEL 8 family
|
|
redhat_release_paths = ['/etc/redhat-release', '/etc/system-release']
|
|
for release_file in redhat_release_paths:
|
|
if os.path.exists(release_file):
|
|
try:
|
|
with open(release_file, 'r') as f:
|
|
release_content = f.read()
|
|
support['os_name'] = release_content.strip()
|
|
|
|
# Check for RHEL 8 family
|
|
if ('release 8' in release_content.lower() or
|
|
'release 8.' in release_content.lower()):
|
|
if any(distro in release_content.lower() for distro in
|
|
['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']):
|
|
support['rhel8_family'] = True
|
|
|
|
# Check if cgroups v2 is actually mounted
|
|
result = subprocess.run(['mount'], capture_output=True,
|
|
text=True, timeout=5)
|
|
if 'cgroup2' not in result.stdout:
|
|
support['rhel8_needs_enablement'] = True
|
|
break
|
|
except:
|
|
pass
|
|
|
|
# Check cgroups v2
|
|
if os.path.exists('/sys/fs/cgroup/cgroup.controllers'):
|
|
support['cgroups_v2'] = True
|
|
|
|
# Check controllers
|
|
with open('/sys/fs/cgroup/cgroup.controllers', 'r') as f:
|
|
controllers = f.read().strip().split()
|
|
support['memory_controller'] = 'memory' in controllers
|
|
support['cpu_controller'] = 'cpu' in controllers
|
|
support['io_controller'] = 'io' in controllers
|
|
|
|
# Check lscgctl tool
|
|
support['lscgctl_available'] = os.path.exists(self.LSCGCTL_PATH)
|
|
|
|
# Check quota tools
|
|
result = subprocess.run(['which', 'setquota'], capture_output=True, timeout=5)
|
|
support['quota_tools'] = result.returncode == 0
|
|
|
|
except Exception as e:
|
|
logging.writeToFile(f"Error checking cgroup support: {str(e)}")
|
|
|
|
return support
|
|
|
|
|
|
# Singleton instance
|
|
resource_manager = ResourceLimitsManager()
|