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" %} +
+ +
+
+ +
+ + MB +
+
{% trans "RAM allocated per website (256 MB - 16 GB)" %}
+
+ +
+ + +
{% trans "Number of CPU cores (1-16)" %}
+
+
+
+ +
+
+ + {% trans "Disk & I/O" %} +
+ +
+
+ +
+ + MB/s +
+
{% trans "Disk I/O bandwidth limit (5-100 MB/s)" %}
+
+ +
+ + +
{% trans "Maximum files/directories (100k - 2M)" %}
+
+
+
+ +
+
+ + {% trans "Process Limits" %} +
+ +
+
+ + +
{% trans "Max concurrent PHP connections (1-100)" %}
+
+ +
+ + +
{% trans "Soft process limit (100-2000)" %}
+
+
+ +
+ + +
{% trans "Hard process limit (100-2000)" %}
+
+
+
+

{% 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 "CPU & Memory" %} +
+ +
+
+ +
+ + MB +
+
{% trans "RAM allocated per website (256 MB - 16 GB)" %}
+
+ +
+ + +
{% trans "Number of CPU cores (1-16)" %}
+
+
+
+ +
+
+ + {% trans "Disk & I/O" %} +
+ +
+
+ +
+ + MB/s +
+
{% trans "Disk I/O bandwidth limit (5-100 MB/s)" %}
+
+ +
+ + +
{% trans "Maximum files/directories (100k - 2M)" %}
+
+
+
+ +
+
+ + {% trans "Process Limits" %} +
+ +
+
+ + +
{% trans "Max concurrent PHP connections (1-100)" %}
+
+ +
+ + +
{% trans "Soft process limit (100-2000)" %}
+
+
+ +
+ + +
{% trans "Hard process limit (100-2000)" %}
+
+
+
+

{% 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])