Add comprehensive resource limits with automatic OpenLiteSpeed cgroups setup

This commit implements per-package resource limits for CyberPanel shared hosting
using OpenLiteSpeed's native cgroups v2 integration with automatic server configuration.

Features:
- 7 new package fields: memoryLimitMB, cpuCores, ioLimitMBPS, inodeLimit,
  maxConnections, procSoftLimit, procHardLimit
- Automatic OLS cgroups setup (no manual server configuration required)
- Multi-layer enforcement: OLS config + kernel cgroups v2 + filesystem quotas
- Per-user enforcement (subdomains/addon domains share parent's limits)
- Graceful degradation if cgroups unavailable
- Automatic backup of OLS config before modification

Backend Changes:
- packages/models.py: Added 7 resource limit fields with defaults
- packages/packagesManager.py: CRUD operations for resource limits
- plogical/resourceLimits.py: NEW - Resource manager with auto-setup
  * _ensure_cgroups_enabled(): Automatic OLS cgroups configuration
  * set_user_limits(): Apply limits via lscgctl
  * remove_user_limits(): Cleanup on deletion
  * set_inode_limit(): Filesystem quota management
- plogical/vhostConfs.py: Parameterized hardcoded resource limits
- plogical/vhost.py: Updated signatures to accept resource limits
- plogical/virtualHostUtilities.py: Extract and apply package limits


Frontend Changes:
- packages/templates/packages/createPackage.html: Resource limits UI
- packages/templates/packages/modifyPackage.html: Resource limits UI
- packages/static/packages/packages.js: AngularJS controller updates

Automatic Setup Flow:
When creating a website with enforceDiskLimits=True:
1. Check kernel cgroups v2 support
2. Run lssetup if lscgctl missing
3. Enable cgroups in OLS config if needed
4. Backup and modify /usr/local/lsws/conf/httpd_config.conf
5. Graceful restart of OpenLiteSpeed
6. Apply per-user limits via lscgctl
7. Set inode quotas via setquota

Requirements:
- Linux kernel 5.2+ (cgroups v2)
- OpenLiteSpeed 1.8+ (with lsns support)
- quota tools (optional, for inode limits)

Backward Compatibility:
- Existing packages receive default values via migration
- No manual setup required for new installations
- Graceful fallback if cgroups unavailable
This commit is contained in:
usmannasir
2025-11-11 17:14:39 +05:00
parent 5b7bcb462f
commit c679d6ab10
9 changed files with 827 additions and 26 deletions

View File

@@ -17,3 +17,12 @@ class Package(models.Model):
allowedDomains = models.IntegerField(default=0)
allowFullDomain = models.IntegerField(default=1)
enforceDiskLimits = models.IntegerField(default=0)
# Resource Limits - enforced via cgroups v2 and OpenLiteSpeed
memoryLimitMB = models.IntegerField(default=1024, help_text="Memory limit in MB")
cpuCores = models.IntegerField(default=1, help_text="Number of CPU cores")
ioLimitMBPS = models.IntegerField(default=10, help_text="I/O limit in MB/s")
inodeLimit = models.IntegerField(default=400000, help_text="Maximum number of files/directories")
maxConnections = models.IntegerField(default=10, help_text="Max concurrent PHP connections")
procSoftLimit = models.IntegerField(default=400, help_text="Soft process limit")
procHardLimit = models.IntegerField(default=500, help_text="Hard process limit")

View File

@@ -71,12 +71,53 @@ class PackagesManager:
except:
enforceDiskLimits = 0
# Resource Limits - with backward compatibility
try:
memoryLimitMB = int(data['memoryLimitMB'])
except:
memoryLimitMB = 1024
try:
cpuCores = int(data['cpuCores'])
except:
cpuCores = 1
try:
ioLimitMBPS = int(data['ioLimitMBPS'])
except:
ioLimitMBPS = 10
try:
inodeLimit = int(data['inodeLimit'])
except:
inodeLimit = 400000
try:
maxConnections = int(data['maxConnections'])
except:
maxConnections = 10
try:
procSoftLimit = int(data['procSoftLimit'])
except:
procSoftLimit = 400
try:
procHardLimit = int(data['procHardLimit'])
except:
procHardLimit = 500
if packageSpace < 0 or packageBandwidth < 0 or packageDatabases < 0 or ftpAccounts < 0 or emails < 0 or allowedDomains < 0:
data_ret = {'saveStatus': 0, 'error_message': "All values should be positive or 0."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
# Validate resource limits
if memoryLimitMB < 256 or cpuCores < 1 or ioLimitMBPS < 1 or inodeLimit < 10000 or maxConnections < 1 or procSoftLimit < 1 or procHardLimit < 1:
data_ret = {'saveStatus': 0, 'error_message': "Resource limits must be positive and within valid ranges."}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
admin = Administrator.objects.get(pk=userID)
if api == '0':
@@ -84,7 +125,10 @@ class PackagesManager:
package = Package(admin=admin, packageName=packageName, diskSpace=packageSpace,
bandwidth=packageBandwidth, ftpAccounts=ftpAccounts, dataBases=packageDatabases,
emailAccounts=emails, allowedDomains=allowedDomains, allowFullDomain=allowFullDomain, enforceDiskLimits=enforceDiskLimits)
emailAccounts=emails, allowedDomains=allowedDomains, allowFullDomain=allowFullDomain,
enforceDiskLimits=enforceDiskLimits, memoryLimitMB=memoryLimitMB, cpuCores=cpuCores,
ioLimitMBPS=ioLimitMBPS, inodeLimit=inodeLimit, maxConnections=maxConnections,
procSoftLimit=procSoftLimit, procHardLimit=procHardLimit)
package.save()
@@ -162,7 +206,12 @@ class PackagesManager:
data_ret = {'emails': emails, 'modifyStatus': 1, 'error_message': "None",
"diskSpace": diskSpace, "bandwidth": bandwidth, "ftpAccounts": ftpAccounts,
"dataBases": dataBases, "allowedDomains": modifyPack.allowedDomains, 'allowFullDomain': modifyPack.allowFullDomain, 'enforceDiskLimits': modifyPack.enforceDiskLimits}
"dataBases": dataBases, "allowedDomains": modifyPack.allowedDomains,
'allowFullDomain': modifyPack.allowFullDomain, 'enforceDiskLimits': modifyPack.enforceDiskLimits,
'memoryLimitMB': modifyPack.memoryLimitMB, 'cpuCores': modifyPack.cpuCores,
'ioLimitMBPS': modifyPack.ioLimitMBPS, 'inodeLimit': modifyPack.inodeLimit,
'maxConnections': modifyPack.maxConnections, 'procSoftLimit': modifyPack.procSoftLimit,
'procHardLimit': modifyPack.procHardLimit}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@@ -213,6 +262,42 @@ class PackagesManager:
except:
modifyPack.enforceDiskLimits = 0
# Update resource limits
try:
modifyPack.memoryLimitMB = int(data['memoryLimitMB'])
except:
pass # Keep existing value
try:
modifyPack.cpuCores = int(data['cpuCores'])
except:
pass # Keep existing value
try:
modifyPack.ioLimitMBPS = int(data['ioLimitMBPS'])
except:
pass # Keep existing value
try:
modifyPack.inodeLimit = int(data['inodeLimit'])
except:
pass # Keep existing value
try:
modifyPack.maxConnections = int(data['maxConnections'])
except:
pass # Keep existing value
try:
modifyPack.procSoftLimit = int(data['procSoftLimit'])
except:
pass # Keep existing value
try:
modifyPack.procHardLimit = int(data['procHardLimit'])
except:
pass # Keep existing value
modifyPack.save()
## Fix https://github.com/usmannasir/cyberpanel/issues/998

View File

@@ -64,7 +64,15 @@ app.controller('createPackage', function ($scope, $http) {
dataBases: dataBases,
emails: emails,
allowedDomains: $scope.allowedDomains,
enforceDiskLimits: $scope.enforceDiskLimits
enforceDiskLimits: $scope.enforceDiskLimits,
// Resource Limits
memoryLimitMB: $scope.memoryLimitMB || 1024,
cpuCores: $scope.cpuCores || 1,
ioLimitMBPS: $scope.ioLimitMBPS || 10,
inodeLimit: $scope.inodeLimit || 400000,
maxConnections: $scope.maxConnections || 10,
procSoftLimit: $scope.procSoftLimit || 400,
procHardLimit: $scope.procHardLimit || 500
};
var config = {
@@ -236,6 +244,15 @@ app.controller('modifyPackages', function ($scope, $http) {
$scope.allowFullDomain = response.data.allowFullDomain === 1;
$scope.enforceDiskLimits = response.data.enforceDiskLimits === 1;
// Load resource limits
$scope.memoryLimitMB = response.data.memoryLimitMB || 1024;
$scope.cpuCores = response.data.cpuCores || 1;
$scope.ioLimitMBPS = response.data.ioLimitMBPS || 10;
$scope.inodeLimit = response.data.inodeLimit || 400000;
$scope.maxConnections = response.data.maxConnections || 10;
$scope.procSoftLimit = response.data.procSoftLimit || 400;
$scope.procHardLimit = response.data.procHardLimit || 500;
$scope.modifyButton = "Save Details";
$("#packageDetailsToBeModified").fadeIn();
@@ -283,6 +300,14 @@ app.controller('modifyPackages', function ($scope, $http) {
allowedDomains: $scope.allowedDomains,
allowFullDomain: $scope.allowFullDomain,
enforceDiskLimits: $scope.enforceDiskLimits,
// Resource Limits
memoryLimitMB: $scope.memoryLimitMB || 1024,
cpuCores: $scope.cpuCores || 1,
ioLimitMBPS: $scope.ioLimitMBPS || 10,
inodeLimit: $scope.inodeLimit || 400000,
maxConnections: $scope.maxConnections || 10,
procSoftLimit: $scope.procSoftLimit || 400,
procHardLimit: $scope.procHardLimit || 500
};
var config = {

View File

@@ -435,6 +435,99 @@
</div>
</div>
<div class="content-card">
<h2 class="card-title">{% trans "Advanced Resource Limits" %}</h2>
<div class="info-box">
<i class="fas fa-server"></i>
<div class="info-box-text">
{% trans "These limits are enforced via cgroups v2 and OpenLiteSpeed to prevent resource abuse and ensure server stability." %}
</div>
</div>
<div class="resource-section">
<div class="resource-title">
<i class="fas fa-microchip"></i>
{% trans "CPU & Memory" %}
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">{% trans "Memory Limit" %}</label>
<div class="input-group">
<input name="memoryLimitMB" type="number" class="form-control" ng-model="memoryLimitMB"
value="1024" min="256" max="16384" step="256" required>
<span class="input-suffix">MB</span>
</div>
<div class="help-text">{% trans "RAM allocated per website (256 MB - 16 GB)" %}</div>
</div>
<div class="form-group">
<label class="form-label">{% trans "CPU Cores" %}</label>
<input name="cpuCores" type="number" class="form-control" ng-model="cpuCores"
value="1" min="1" max="16" required>
<div class="help-text">{% trans "Number of CPU cores (1-16)" %}</div>
</div>
</div>
</div>
<div class="resource-section">
<div class="resource-title">
<i class="fas fa-hdd"></i>
{% trans "Disk & I/O" %}
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">{% trans "I/O Limit" %}</label>
<div class="input-group">
<input name="ioLimitMBPS" type="number" class="form-control" ng-model="ioLimitMBPS"
value="10" min="5" max="100" required>
<span class="input-suffix">MB/s</span>
</div>
<div class="help-text">{% trans "Disk I/O bandwidth limit (5-100 MB/s)" %}</div>
</div>
<div class="form-group">
<label class="form-label">{% trans "Inode Limit" %}</label>
<input name="inodeLimit" type="number" class="form-control" ng-model="inodeLimit"
value="400000" min="100000" max="2000000" step="100000" required>
<div class="help-text">{% trans "Maximum files/directories (100k - 2M)" %}</div>
</div>
</div>
</div>
<div class="resource-section">
<div class="resource-title">
<i class="fas fa-cogs"></i>
{% trans "Process Limits" %}
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">{% trans "Max Connections" %}</label>
<input name="maxConnections" type="number" class="form-control" ng-model="maxConnections"
value="10" min="1" max="100" required>
<div class="help-text">{% trans "Max concurrent PHP connections (1-100)" %}</div>
</div>
<div class="form-group">
<label class="form-label">{% trans "Process Soft Limit" %}</label>
<input name="procSoftLimit" type="number" class="form-control" ng-model="procSoftLimit"
value="400" min="100" max="2000" required>
<div class="help-text">{% trans "Soft process limit (100-2000)" %}</div>
</div>
</div>
<div class="form-group">
<label class="form-label">{% trans "Process Hard Limit" %}</label>
<input name="procHardLimit" type="number" class="form-control" ng-model="procHardLimit"
value="500" min="100" max="2000" required>
<div class="help-text">{% trans "Hard process limit (100-2000)" %}</div>
</div>
</div>
</div>
<div class="content-card" ng-hide="installationDetailsForm">
<h2 class="card-title">{% trans "Additional Features" %}</h2>

View File

@@ -471,7 +471,93 @@
</div>
</form>
</div>
<div class="content-card">
<h2 class="card-title">{% trans "Advanced Resource Limits" %}</h2>
<div class="form-section">
<div class="form-section-title">
<i class="fas fa-microchip"></i>
{% trans "CPU & Memory" %}
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">{% trans "Memory Limit" %}</label>
<div class="input-group">
<input name="memoryLimitMB" type="number" class="form-control" ng-model="memoryLimitMB"
min="256" max="16384" step="256" required aria-label="{% trans 'Memory limit in MB' %}">
<span class="input-suffix">MB</span>
</div>
<div class="help-text">{% trans "RAM allocated per website (256 MB - 16 GB)" %}</div>
</div>
<div class="form-group">
<label class="form-label">{% trans "CPU Cores" %}</label>
<input name="cpuCores" type="number" class="form-control" ng-model="cpuCores"
min="1" max="16" required aria-label="{% trans 'Number of CPU cores' %}">
<div class="help-text">{% trans "Number of CPU cores (1-16)" %}</div>
</div>
</div>
</div>
<div class="form-section">
<div class="form-section-title">
<i class="fas fa-hdd"></i>
{% trans "Disk & I/O" %}
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">{% trans "I/O Limit" %}</label>
<div class="input-group">
<input name="ioLimitMBPS" type="number" class="form-control" ng-model="ioLimitMBPS"
min="5" max="100" required aria-label="{% trans 'I/O limit in MB/s' %}">
<span class="input-suffix">MB/s</span>
</div>
<div class="help-text">{% trans "Disk I/O bandwidth limit (5-100 MB/s)" %}</div>
</div>
<div class="form-group">
<label class="form-label">{% trans "Inode Limit" %}</label>
<input name="inodeLimit" type="number" class="form-control" ng-model="inodeLimit"
min="100000" max="2000000" step="100000" required aria-label="{% trans 'Maximum files/directories' %}">
<div class="help-text">{% trans "Maximum files/directories (100k - 2M)" %}</div>
</div>
</div>
</div>
<div class="form-section">
<div class="form-section-title">
<i class="fas fa-cogs"></i>
{% trans "Process Limits" %}
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">{% trans "Max Connections" %}</label>
<input name="maxConnections" type="number" class="form-control" ng-model="maxConnections"
min="1" max="100" required aria-label="{% trans 'Max concurrent PHP connections' %}">
<div class="help-text">{% trans "Max concurrent PHP connections (1-100)" %}</div>
</div>
<div class="form-group">
<label class="form-label">{% trans "Process Soft Limit" %}</label>
<input name="procSoftLimit" type="number" class="form-control" ng-model="procSoftLimit"
min="100" max="2000" required aria-label="{% trans 'Soft process limit' %}">
<div class="help-text">{% trans "Soft process limit (100-2000)" %}</div>
</div>
</div>
<div class="form-group">
<label class="form-label">{% trans "Process Hard Limit" %}</label>
<input name="procHardLimit" type="number" class="form-control" ng-model="procHardLimit"
min="100" max="2000" required aria-label="{% trans 'Hard process limit' %}">
<div class="help-text">{% trans "Hard process limit (100-2000)" %}</div>
</div>
</div>
</div>
<div class="content-card" ng-hide="installationDetailsForm">
<h2 class="card-title">{% trans "Additional Features" %}</h2>

438
plogical/resourceLimits.py Normal file
View File

@@ -0,0 +1,438 @@
#!/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 _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:
# 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+)")
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
}
try:
# 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()

View File

@@ -190,7 +190,9 @@ class vhost:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + " [finalizeVhostCreation]")
@staticmethod
def createDirectoryForVirtualHost(virtualHostName,administratorEmail,virtualHostUser, phpVersion, openBasedir):
def createDirectoryForVirtualHost(virtualHostName,administratorEmail,virtualHostUser, phpVersion, openBasedir,
memSoftLimit=2047, memHardLimit=2047, maxConnections=10,
procSoftLimit=400, procHardLimit=500):
if not os.path.exists('/usr/local/lsws/Example/html/.well-known/acme-challenge'):
command = 'mkdir -p /usr/local/lsws/Example/html/.well-known/acme-challenge'
@@ -218,13 +220,16 @@ class vhost:
## Creating Per vhost Configuration File
if vhost.perHostVirtualConf(completePathToConfigFile,administratorEmail,virtualHostUser,phpVersion, virtualHostName, openBasedir) == 1:
if vhost.perHostVirtualConf(completePathToConfigFile,administratorEmail,virtualHostUser,phpVersion, virtualHostName, openBasedir,
memSoftLimit, memHardLimit, maxConnections, procSoftLimit, procHardLimit) == 1:
return [1,"None"]
else:
return [0,"[61 Not able to create per host virtual configurations [perHostVirtualConf]"]
@staticmethod
def perHostVirtualConf(vhFile, administratorEmail,virtualHostUser, phpVersion, virtualHostName, openBasedir):
def perHostVirtualConf(vhFile, administratorEmail,virtualHostUser, phpVersion, virtualHostName, openBasedir,
memSoftLimit=2047, memHardLimit=2047, maxConnections=10,
procSoftLimit=400, procHardLimit=500):
# General Configurations tab
if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
try:
@@ -240,6 +245,13 @@ class vhost:
currentConf = currentConf.replace('{adminEmails}', administratorEmail)
currentConf = currentConf.replace('{php}', php)
# Replace resource limits
currentConf = currentConf.replace('{memSoftLimit}', str(memSoftLimit))
currentConf = currentConf.replace('{memHardLimit}', str(memHardLimit))
currentConf = currentConf.replace('{maxConnections}', str(maxConnections))
currentConf = currentConf.replace('{procSoftLimit}', str(procSoftLimit))
currentConf = currentConf.replace('{procHardLimit}', str(procHardLimit))
if openBasedir == 1:
currentConf = currentConf.replace('{open_basedir}', 'php_admin_value open_basedir "/tmp:$VH_ROOT"')
else:
@@ -474,6 +486,12 @@ class vhost:
if os.path.exists(gitPath):
shutil.rmtree(gitPath)
## Remove resource limits for this user (OLS cgroups)
try:
from plogical.resourceLimits import resource_manager
resource_manager.remove_user_limits(externalApp)
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to remove resource limits for user {externalApp}: {str(e)}")
### Delete Acme folder
@@ -962,7 +980,8 @@ class vhost:
@staticmethod
def createDirectoryForDomain(masterDomain, domain, phpVersion, path, administratorEmail, virtualHostUser,
openBasedir):
openBasedir, memSoftLimit=2047, memHardLimit=2047, maxConnections=10,
procSoftLimit=400, procHardLimit=500):
FNULL = open(os.devnull, 'w')
@@ -1007,7 +1026,8 @@ class vhost:
#return [0, "[351 Not able to directories for virtual host [createDirectoryForDomain]]"]
if vhost.perHostDomainConf(path, masterDomain, domain, completePathToConfigFile,
administratorEmail, phpVersion, virtualHostUser, openBasedir) == 1:
administratorEmail, phpVersion, virtualHostUser, openBasedir,
memSoftLimit, memHardLimit, maxConnections, procSoftLimit, procHardLimit) == 1:
return [1, "None"]
else:
pass
@@ -1016,7 +1036,9 @@ class vhost:
return [1, "None"]
@staticmethod
def perHostDomainConf(path, masterDomain, domain, vhFile, administratorEmail, phpVersion, virtualHostUser, openBasedir):
def perHostDomainConf(path, masterDomain, domain, vhFile, administratorEmail, phpVersion, virtualHostUser, openBasedir,
memSoftLimit=2047, memHardLimit=2047, maxConnections=10,
procSoftLimit=400, procHardLimit=500):
if ProcessUtilities.decideServer() == ProcessUtilities.OLS:
try:
php = PHPManager.getPHPString(phpVersion)
@@ -1032,6 +1054,12 @@ class vhost:
currentConf = currentConf.replace('{adminEmails}', administratorEmail)
currentConf = currentConf.replace('{php}', php)
# Replace resource limits (child domains share parent's limits)
currentConf = currentConf.replace('{memSoftLimit}', str(memSoftLimit))
currentConf = currentConf.replace('{memHardLimit}', str(memHardLimit))
currentConf = currentConf.replace('{maxConnections}', str(maxConnections))
currentConf = currentConf.replace('{procSoftLimit}', str(procSoftLimit))
currentConf = currentConf.replace('{procHardLimit}', str(procHardLimit))
if openBasedir == 1:
currentConf = currentConf.replace('{open_basedir}', 'php_admin_value open_basedir "/tmp:$VH_ROOT"')

View File

@@ -43,8 +43,8 @@ scripthandler {
extprocessor {virtualHostUser} {
type lsapi
address UDS://tmp/lshttpd/{virtualHostUser}.sock
maxConns 10
env LSAPI_CHILDREN=10
maxConns {maxConnections}
env LSAPI_CHILDREN={maxConnections}
initTimeout 600
retryTimeout 0
persistConn 1
@@ -54,10 +54,10 @@ extprocessor {virtualHostUser} {
path /usr/local/lsws/lsphp{php}/bin/lsphp
extUser {virtualHostUser}
extGroup {virtualHostUser}
memSoftLimit 2047M
memHardLimit 2047M
procSoftLimit 400
procHardLimit 500
memSoftLimit {memSoftLimit}M
memHardLimit {memHardLimit}M
procSoftLimit {procSoftLimit}
procHardLimit {procHardLimit}
}
phpIniOverride {
@@ -140,8 +140,8 @@ scripthandler {
extprocessor {externalApp} {
type lsapi
address UDS://tmp/lshttpd/{externalApp}.sock
maxConns 10
env LSAPI_CHILDREN=10
maxConns {maxConnections}
env LSAPI_CHILDREN={maxConnections}
initTimeout 60
retryTimeout 0
persistConn 1
@@ -151,10 +151,10 @@ extprocessor {externalApp} {
path /usr/local/lsws/lsphp{php}/bin/lsphp
extUser {externalAppMaster}
extGroup {externalAppMaster}
memSoftLimit 2047M
memHardLimit 2047M
procSoftLimit 400
procHardLimit 500
memSoftLimit {memSoftLimit}M
memHardLimit {memHardLimit}M
procSoftLimit {procSoftLimit}
procHardLimit {procHardLimit}
}
rewrite {

View File

@@ -661,8 +661,20 @@ local_name %s {
if retValues[0] == 0:
raise BaseException(retValues[1])
# Get package to retrieve resource limits
selectedPackage = Package.objects.get(packageName=packageName)
# Extract resource limits from package
memSoftLimit = selectedPackage.memoryLimitMB
memHardLimit = selectedPackage.memoryLimitMB
maxConnections = selectedPackage.maxConnections
procSoftLimit = selectedPackage.procSoftLimit
procHardLimit = selectedPackage.procHardLimit
retValues = vhost.createDirectoryForVirtualHost(virtualHostName, administratorEmail,
virtualHostUser, phpVersion, openBasedir)
virtualHostUser, phpVersion, openBasedir,
memSoftLimit, memHardLimit, maxConnections,
procSoftLimit, procHardLimit)
if retValues[0] == 0:
raise BaseException(retValues[1])
@@ -673,8 +685,6 @@ local_name %s {
if retValues[0] == 0:
raise BaseException(retValues[1])
selectedPackage = Package.objects.get(packageName=packageName)
if LimitsCheck:
website = Websites(admin=admin, package=selectedPackage, domain=virtualHostName,
adminEmail=administratorEmail,
@@ -765,6 +775,23 @@ local_name %s {
command = f'setquota -u {virtualHostUser} {spaceString} 0 0 /'
ProcessUtilities.executioner(command)
# Apply OpenLiteSpeed cgroups v2 resource limits and inode quotas
if selectedPackage.enforceDiskLimits:
try:
from plogical.resourceLimits import resource_manager
# Set per-user resource limits using OLS native cgroups API
success = resource_manager.set_user_limits(virtualHostUser, selectedPackage)
if not success:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to set resource limits for user {virtualHostUser}")
# Set inode limit using filesystem quotas
success = resource_manager.set_inode_limit(virtualHostName, virtualHostUser, selectedPackage.inodeLimit)
if not success:
logging.CyberCPLogFileWriter.writeToFile(f"Warning: Failed to set inode limit for {virtualHostName}")
except Exception as e:
logging.CyberCPLogFileWriter.writeToFile(f"Error applying resource limits for {virtualHostName}: {str(e)}")
logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Website successfully created. [200]')
@@ -1574,8 +1601,18 @@ local_name %s {
logging.CyberCPLogFileWriter.statusWriter(tempStatusPath, 'Creating configurations..,50')
# Get resource limits from master website's package (child domains share parent's limits)
masterPackage = master.package
memSoftLimit = masterPackage.memoryLimitMB
memHardLimit = masterPackage.memoryLimitMB
maxConnections = masterPackage.maxConnections
procSoftLimit = masterPackage.procSoftLimit
procHardLimit = masterPackage.procHardLimit
retValues = vhost.createDirectoryForDomain(masterDomain, virtualHostName, phpVersion, path,
master.adminEmail, master.externalApp, openBasedir)
master.adminEmail, master.externalApp, openBasedir,
memSoftLimit, memHardLimit, maxConnections,
procSoftLimit, procHardLimit)
if retValues[0] == 0:
raise BaseException(retValues[1])