Files
CyberPanel/plogical/resourceLimits.py
usmannasir 6d66e5739a Add RHEL 8 family cgroups v2 detection and enablement instructions
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)
2025-11-11 17:27:11 +05:00

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()