diff --git a/packages/models.py b/packages/models.py
index 431c2b866..44a687035 100644
--- a/packages/models.py
+++ b/packages/models.py
@@ -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")
diff --git a/packages/packagesManager.py b/packages/packagesManager.py
index a48f1a5f1..c39894c3a 100644
--- a/packages/packagesManager.py
+++ b/packages/packagesManager.py
@@ -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
diff --git a/packages/static/packages/packages.js b/packages/static/packages/packages.js
index b07b7d4eb..dd182307d 100644
--- a/packages/static/packages/packages.js
+++ b/packages/static/packages/packages.js
@@ -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 = {
diff --git a/packages/templates/packages/createPackage.html b/packages/templates/packages/createPackage.html
index e83290aec..2cee1cfd1 100644
--- a/packages/templates/packages/createPackage.html
+++ b/packages/templates/packages/createPackage.html
@@ -435,6 +435,99 @@
+
+
{% trans "Advanced Resource Limits" %}
+
+
+
+
+ {% trans "These limits are enforced via cgroups v2 and OpenLiteSpeed to prevent resource abuse and ensure server stability." %}
+
+
+
+
+
+
+ {% trans "CPU & Memory" %}
+
+
+
+
+
+
+
+
+ {% trans "Disk & I/O" %}
+
+
+
+
+
+
+
+
+ {% trans "Process Limits" %}
+
+
+
+
+
+
+
+
{% trans "Additional Features" %}
diff --git a/packages/templates/packages/modifyPackage.html b/packages/templates/packages/modifyPackage.html
index a647d1832..de6d1c919 100644
--- a/packages/templates/packages/modifyPackage.html
+++ b/packages/templates/packages/modifyPackage.html
@@ -471,7 +471,93 @@
-
+
+
+
{% trans "Advanced Resource Limits" %}
+
+
+
+
+
+
+
+
{% trans "Additional Features" %}
diff --git a/plogical/resourceLimits.py b/plogical/resourceLimits.py
new file mode 100644
index 000000000..3b01f8d4a
--- /dev/null
+++ b/plogical/resourceLimits.py
@@ -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()
diff --git a/plogical/vhost.py b/plogical/vhost.py
index 990e9d7f9..94b65f47f 100644
--- a/plogical/vhost.py
+++ b/plogical/vhost.py
@@ -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"')
diff --git a/plogical/vhostConfs.py b/plogical/vhostConfs.py
index d012e6bb1..da521b75e 100644
--- a/plogical/vhostConfs.py
+++ b/plogical/vhostConfs.py
@@ -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 {
diff --git a/plogical/virtualHostUtilities.py b/plogical/virtualHostUtilities.py
index 041c640d1..c08d62d14 100644
--- a/plogical/virtualHostUtilities.py
+++ b/plogical/virtualHostUtilities.py
@@ -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])