Implement cgroup v2 limits and sync
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
A modern web hosting control panel for WordPress and general PHP hosting. Built with Laravel 12, Filament v5, Livewire 4, and Tailwind CSS v4.
|
A modern web hosting control panel for WordPress and general PHP hosting. Built with Laravel 12, Filament v5, Livewire 4, and Tailwind CSS v4.
|
||||||
|
|
||||||
Version: 0.9-rc8 (release candidate)
|
Version: 0.9-rc9 (release candidate)
|
||||||
|
|
||||||
This is a release candidate. Expect rapid iteration and breaking changes until 1.0.
|
This is a release candidate. Expect rapid iteration and breaking changes until 1.0.
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,13 @@ class CollectUserUsage extends Command
|
|||||||
$bandwidthTotal = $this->getBandwidthTotal($agent, $user);
|
$bandwidthTotal = $this->getBandwidthTotal($agent, $user);
|
||||||
$resourceStats = $this->getUserResourceStats($agent, $user);
|
$resourceStats = $this->getUserResourceStats($agent, $user);
|
||||||
$cpuPercent = (int) round($resourceStats['cpu_percent'] ?? 0);
|
$cpuPercent = (int) round($resourceStats['cpu_percent'] ?? 0);
|
||||||
|
$cpuUsageTotal = (int) ($resourceStats['cpu_usage_usec_total'] ?? 0);
|
||||||
$memoryBytes = (int) ($resourceStats['memory_bytes'] ?? 0);
|
$memoryBytes = (int) ($resourceStats['memory_bytes'] ?? 0);
|
||||||
$diskIoTotal = (int) ($resourceStats['disk_io_total_bytes'] ?? 0);
|
$diskIoTotal = (int) ($resourceStats['disk_io_total_bytes'] ?? 0);
|
||||||
$lastBandwidthTotal = (int) UserSetting::getForUser($user->id, 'bandwidth_total_bytes', 0);
|
$lastBandwidthTotal = (int) UserSetting::getForUser($user->id, 'bandwidth_total_bytes', 0);
|
||||||
$lastDiskIoTotal = (int) UserSetting::getForUser($user->id, 'disk_io_total_bytes', 0);
|
$lastDiskIoTotal = (int) UserSetting::getForUser($user->id, 'disk_io_total_bytes', 0);
|
||||||
|
$lastCpuTotal = (int) UserSetting::getForUser($user->id, 'cpu_usage_usec_total', 0);
|
||||||
|
$lastCpuAt = (int) UserSetting::getForUser($user->id, 'cpu_usage_captured_at', 0);
|
||||||
$bandwidthDelta = $bandwidthTotal >= $lastBandwidthTotal
|
$bandwidthDelta = $bandwidthTotal >= $lastBandwidthTotal
|
||||||
? $bandwidthTotal - $lastBandwidthTotal
|
? $bandwidthTotal - $lastBandwidthTotal
|
||||||
: $bandwidthTotal;
|
: $bandwidthTotal;
|
||||||
@@ -60,6 +63,14 @@ class CollectUserUsage extends Command
|
|||||||
? $diskIoTotal - $lastDiskIoTotal
|
? $diskIoTotal - $lastDiskIoTotal
|
||||||
: $diskIoTotal;
|
: $diskIoTotal;
|
||||||
|
|
||||||
|
if ($cpuUsageTotal > 0 && $lastCpuAt > 0) {
|
||||||
|
$elapsed = $capturedAt->timestamp - $lastCpuAt;
|
||||||
|
if ($elapsed > 0) {
|
||||||
|
$delta = $cpuUsageTotal >= $lastCpuTotal ? $cpuUsageTotal - $lastCpuTotal : $cpuUsageTotal;
|
||||||
|
$cpuPercent = (int) round(($delta / ($elapsed * 1_000_000)) * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->storeMetric($user->id, 'disk_bytes', $diskBytes, $capturedAt);
|
$this->storeMetric($user->id, 'disk_bytes', $diskBytes, $capturedAt);
|
||||||
$this->storeMetric($user->id, 'mail_bytes', $mailBytes, $capturedAt);
|
$this->storeMetric($user->id, 'mail_bytes', $mailBytes, $capturedAt);
|
||||||
$this->storeMetric($user->id, 'database_bytes', $dbBytes, $capturedAt);
|
$this->storeMetric($user->id, 'database_bytes', $dbBytes, $capturedAt);
|
||||||
@@ -70,6 +81,8 @@ class CollectUserUsage extends Command
|
|||||||
|
|
||||||
UserSetting::setForUser($user->id, 'bandwidth_total_bytes', $bandwidthTotal);
|
UserSetting::setForUser($user->id, 'bandwidth_total_bytes', $bandwidthTotal);
|
||||||
UserSetting::setForUser($user->id, 'disk_io_total_bytes', $diskIoTotal);
|
UserSetting::setForUser($user->id, 'disk_io_total_bytes', $diskIoTotal);
|
||||||
|
UserSetting::setForUser($user->id, 'cpu_usage_usec_total', $cpuUsageTotal);
|
||||||
|
UserSetting::setForUser($user->id, 'cpu_usage_captured_at', $capturedAt->timestamp);
|
||||||
|
|
||||||
$this->line("Collected usage for {$user->username}");
|
$this->line("Collected usage for {$user->username}");
|
||||||
}
|
}
|
||||||
@@ -119,7 +132,7 @@ class CollectUserUsage extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{cpu_percent: float, memory_bytes: int, disk_io_total_bytes: int}
|
* @return array{cpu_percent: float, cpu_usage_usec_total: int, memory_bytes: int, disk_io_total_bytes: int}
|
||||||
*/
|
*/
|
||||||
protected function getUserResourceStats(AgentClient $agent, User $user): array
|
protected function getUserResourceStats(AgentClient $agent, User $user): array
|
||||||
{
|
{
|
||||||
@@ -131,6 +144,7 @@ class CollectUserUsage extends Command
|
|||||||
if ($result['success'] ?? false) {
|
if ($result['success'] ?? false) {
|
||||||
return [
|
return [
|
||||||
'cpu_percent' => (float) ($result['cpu_percent'] ?? 0),
|
'cpu_percent' => (float) ($result['cpu_percent'] ?? 0),
|
||||||
|
'cpu_usage_usec_total' => (int) ($result['cpu_usage_usec_total'] ?? 0),
|
||||||
'memory_bytes' => (int) ($result['memory_bytes'] ?? 0),
|
'memory_bytes' => (int) ($result['memory_bytes'] ?? 0),
|
||||||
'disk_io_total_bytes' => (int) ($result['disk_io_total_bytes'] ?? 0),
|
'disk_io_total_bytes' => (int) ($result['disk_io_total_bytes'] ?? 0),
|
||||||
];
|
];
|
||||||
@@ -141,6 +155,7 @@ class CollectUserUsage extends Command
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'cpu_percent' => 0.0,
|
'cpu_percent' => 0.0,
|
||||||
|
'cpu_usage_usec_total' => 0,
|
||||||
'memory_bytes' => 0,
|
'memory_bytes' => 0,
|
||||||
'disk_io_total_bytes' => 0,
|
'disk_io_total_bytes' => 0,
|
||||||
];
|
];
|
||||||
|
|||||||
60
app/Console/Commands/JabaliSyncCgroups.php
Normal file
60
app/Console/Commands/JabaliSyncCgroups.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\Agent\AgentClient;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class JabaliSyncCgroups extends Command
|
||||||
|
{
|
||||||
|
public function __construct(private AgentClient $agent)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'jabali:sync-cgroups {--user=}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Sync user processes into Jabali cgroups';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$user = (string) ($this->option('user') ?? '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $user !== ''
|
||||||
|
? $this->agent->cgroupSyncUserProcesses($user)
|
||||||
|
: $this->agent->cgroupSyncAllProcesses();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! ($result['success'] ?? false)) {
|
||||||
|
$this->error($result['error'] ?? 'Unknown error');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moved = (int) ($result['moved'] ?? 0);
|
||||||
|
$this->info("Synced cgroups, moved {$moved} process(es).");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ use App\Filament\Admin\Widgets\Settings\DnssecTable;
|
|||||||
use App\Filament\Admin\Widgets\Settings\NotificationLogTable;
|
use App\Filament\Admin\Widgets\Settings\NotificationLogTable;
|
||||||
use App\Filament\Concerns\HasPageTour;
|
use App\Filament\Concerns\HasPageTour;
|
||||||
use App\Models\DnsSetting;
|
use App\Models\DnsSetting;
|
||||||
|
use App\Models\UserResourceLimit;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
|
use App\Services\System\ResourceLimitService;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
@@ -197,6 +199,7 @@ class ServerSettings extends Page implements HasActions, HasForms
|
|||||||
$this->quotaData = [
|
$this->quotaData = [
|
||||||
'quotas_enabled' => (bool) ($settings['quotas_enabled'] ?? false),
|
'quotas_enabled' => (bool) ($settings['quotas_enabled'] ?? false),
|
||||||
'default_quota_mb' => (int) ($settings['default_quota_mb'] ?? 5120),
|
'default_quota_mb' => (int) ($settings['default_quota_mb'] ?? 5120),
|
||||||
|
'resource_limits_enabled' => (bool) ($settings['resource_limits_enabled'] ?? true),
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->fileManagerData = [
|
$this->fileManagerData = [
|
||||||
@@ -419,6 +422,10 @@ class ServerSettings extends Page implements HasActions, HasForms
|
|||||||
->numeric()
|
->numeric()
|
||||||
->placeholder('5120')
|
->placeholder('5120')
|
||||||
->helperText(__('Default disk quota for new users (5120 MB = 5 GB)')),
|
->helperText(__('Default disk quota for new users (5120 MB = 5 GB)')),
|
||||||
|
Toggle::make('quotaData.resource_limits_enabled')
|
||||||
|
->label(__('Enable CPU/Memory/IO Limits'))
|
||||||
|
->helperText(__('Apply cgroup limits from hosting packages (CloudLinux-style)'))
|
||||||
|
->columnSpanFull(),
|
||||||
]),
|
]),
|
||||||
Actions::make([
|
Actions::make([
|
||||||
FormAction::make('saveQuotaSettings')
|
FormAction::make('saveQuotaSettings')
|
||||||
@@ -834,9 +841,11 @@ class ServerSettings extends Page implements HasActions, HasForms
|
|||||||
{
|
{
|
||||||
$data = $this->quotaData;
|
$data = $this->quotaData;
|
||||||
$wasEnabled = (bool) DnsSetting::get('quotas_enabled', false);
|
$wasEnabled = (bool) DnsSetting::get('quotas_enabled', false);
|
||||||
|
$wasLimitsEnabled = (bool) DnsSetting::get('resource_limits_enabled', true);
|
||||||
|
|
||||||
DnsSetting::set('quotas_enabled', $data['quotas_enabled'] ? '1' : '0');
|
DnsSetting::set('quotas_enabled', $data['quotas_enabled'] ? '1' : '0');
|
||||||
DnsSetting::set('default_quota_mb', (string) $data['default_quota_mb']);
|
DnsSetting::set('default_quota_mb', (string) $data['default_quota_mb']);
|
||||||
|
DnsSetting::set('resource_limits_enabled', ! empty($data['resource_limits_enabled']) ? '1' : '0');
|
||||||
DnsSetting::clearCache();
|
DnsSetting::clearCache();
|
||||||
|
|
||||||
if ($data['quotas_enabled'] && ! $wasEnabled) {
|
if ($data['quotas_enabled'] && ! $wasEnabled) {
|
||||||
@@ -850,9 +859,24 @@ class ServerSettings extends Page implements HasActions, HasForms
|
|||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Notification::make()->title(__('Settings saved'))->body(__('Warning: Could not enable quota system.'))->warning()->send();
|
Notification::make()->title(__('Settings saved'))->body(__('Warning: Could not enable quota system.'))->warning()->send();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Notification::make()->title(__('Quota settings saved'))->success()->send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! empty($data['resource_limits_enabled']) && ! $wasLimitsEnabled) {
|
||||||
|
$limits = UserResourceLimit::query()->where('is_active', true)->get();
|
||||||
|
foreach ($limits as $limit) {
|
||||||
|
app(ResourceLimitService::class)->apply($limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($data['resource_limits_enabled']) && $wasLimitsEnabled) {
|
||||||
|
try {
|
||||||
|
$this->getAgent()->send('cgroup.clear_all_limits', []);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Notification::make()->title(__('Settings saved'))->body(__('Warning: Could not clear cgroup limits.'))->warning()->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification::make()->title(__('Quota settings saved'))->success()->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveFileManagerSettings(): void
|
public function saveFileManagerSettings(): void
|
||||||
|
|||||||
@@ -1371,6 +1371,23 @@ class AgentClient
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function cgroupSyncUserProcesses(string $username): array
|
||||||
|
{
|
||||||
|
return $this->send('cgroup.sync_user_processes', [
|
||||||
|
'username' => $username,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cgroupSyncAllProcesses(): array
|
||||||
|
{
|
||||||
|
return $this->send('cgroup.sync_all_processes');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cgroupClearAllLimits(): array
|
||||||
|
{
|
||||||
|
return $this->send('cgroup.clear_all_limits');
|
||||||
|
}
|
||||||
|
|
||||||
public function databasePersistTuning(string $name, string $value): array
|
public function databasePersistTuning(string $name, string $value): array
|
||||||
{
|
{
|
||||||
return $this->send('database.persist_tuning', [
|
return $this->send('database.persist_tuning', [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Services\System;
|
namespace App\Services\System;
|
||||||
|
|
||||||
|
use App\Models\DnsSetting;
|
||||||
use App\Models\UserResourceLimit;
|
use App\Models\UserResourceLimit;
|
||||||
use App\Services\Agent\AgentClient;
|
use App\Services\Agent\AgentClient;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
@@ -12,6 +13,10 @@ class ResourceLimitService
|
|||||||
{
|
{
|
||||||
public function apply(UserResourceLimit $limit): void
|
public function apply(UserResourceLimit $limit): void
|
||||||
{
|
{
|
||||||
|
if (! DnsSetting::get('resource_limits_enabled', true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$user = $limit->user;
|
$user = $limit->user;
|
||||||
if (! $user) {
|
if (! $user) {
|
||||||
throw new RuntimeException('User not found for resource limit.');
|
throw new RuntimeException('User not found for resource limit.');
|
||||||
|
|||||||
502
bin/jabali-agent
502
bin/jabali-agent
@@ -549,6 +549,9 @@ function handleAction(array $request): array
|
|||||||
'geo.apply_rules' => geoApplyRules($params),
|
'geo.apply_rules' => geoApplyRules($params),
|
||||||
'cgroup.apply_user_limits' => cgroupApplyUserLimits($params),
|
'cgroup.apply_user_limits' => cgroupApplyUserLimits($params),
|
||||||
'cgroup.clear_user_limits' => cgroupClearUserLimits($params),
|
'cgroup.clear_user_limits' => cgroupClearUserLimits($params),
|
||||||
|
'cgroup.sync_user_processes' => cgroupSyncUserProcesses($params),
|
||||||
|
'cgroup.sync_all_processes' => cgroupSyncAllProcesses($params),
|
||||||
|
'cgroup.clear_all_limits' => cgroupClearAllUserLimits($params),
|
||||||
'database.persist_tuning' => databasePersistTuning($params),
|
'database.persist_tuning' => databasePersistTuning($params),
|
||||||
'server.export_config' => serverExportConfig($params),
|
'server.export_config' => serverExportConfig($params),
|
||||||
'server.import_config' => serverImportConfig($params),
|
'server.import_config' => serverImportConfig($params),
|
||||||
@@ -815,6 +818,11 @@ function createUser(array $params): array
|
|||||||
logger("Warning: Failed to create Redis user for $username: " . ($redisResult['error'] ?? 'Unknown error'));
|
logger("Warning: Failed to create Redis user for $username: " . ($redisResult['error'] ?? 'Unknown error'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cgroupResult = ensureUserCgroup($username);
|
||||||
|
if (!($cgroupResult['success'] ?? false)) {
|
||||||
|
logger("Warning: Failed to initialize cgroup for $username: " . ($cgroupResult['error'] ?? 'Unknown error'));
|
||||||
|
}
|
||||||
|
|
||||||
logger("Created user $username with home directory $homeDir");
|
logger("Created user $username with home directory $homeDir");
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -1032,6 +1040,8 @@ function deleteUser(array $params): array
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cgroupRemoveUser($username);
|
||||||
|
|
||||||
// Reload services
|
// Reload services
|
||||||
exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>/dev/null');
|
exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_DOMAINS) . ' 2>/dev/null');
|
||||||
exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES) . ' 2>/dev/null');
|
exec('postmap ' . escapeshellarg(POSTFIX_VIRTUAL_MAILBOXES) . ' 2>/dev/null');
|
||||||
@@ -3048,6 +3058,313 @@ function getRootBlockDevice(): ?string
|
|||||||
return $resolvedPath ?: null;
|
return $resolvedPath ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCgroupV2Available(): bool
|
||||||
|
{
|
||||||
|
return is_file('/sys/fs/cgroup/cgroup.controllers');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJabaliCgroupRoot(): string
|
||||||
|
{
|
||||||
|
return '/sys/fs/cgroup/jabali.slice';
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableCgroupControllers(string $path, array $controllers): void
|
||||||
|
{
|
||||||
|
$controllersFile = $path . '/cgroup.controllers';
|
||||||
|
$subtreeFile = $path . '/cgroup.subtree_control';
|
||||||
|
|
||||||
|
if (!is_readable($controllersFile) || !is_writable($subtreeFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$available = preg_split('/\s+/', trim(file_get_contents($controllersFile)));
|
||||||
|
$toEnable = [];
|
||||||
|
foreach ($controllers as $controller) {
|
||||||
|
if (in_array($controller, $available, true)) {
|
||||||
|
$toEnable[] = '+' . $controller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($toEnable)) {
|
||||||
|
@file_put_contents($subtreeFile, implode(' ', $toEnable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureJabaliCgroupRoot(): array
|
||||||
|
{
|
||||||
|
if (!isCgroupV2Available()) {
|
||||||
|
return ['success' => false, 'error' => 'cgroup v2 is not available'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = getJabaliCgroupRoot();
|
||||||
|
if (!is_dir($root)) {
|
||||||
|
exec('systemctl start jabali.slice 2>/dev/null', $output, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($root)) {
|
||||||
|
return ['success' => false, 'error' => 'Failed to initialize jabali.slice'];
|
||||||
|
}
|
||||||
|
|
||||||
|
enableCgroupControllers($root, ['cpu', 'memory', 'io']);
|
||||||
|
|
||||||
|
return ['success' => true, 'path' => $root];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserCgroupPath(string $username): string
|
||||||
|
{
|
||||||
|
return getJabaliCgroupRoot() . '/user-' . $username;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureUserCgroup(string $username): array
|
||||||
|
{
|
||||||
|
$rootResult = ensureJabaliCgroupRoot();
|
||||||
|
if (!($rootResult['success'] ?? false)) {
|
||||||
|
return $rootResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = getUserCgroupPath($username);
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
mkdir($path, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
return ['success' => false, 'error' => 'Failed to create user cgroup'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => true, 'path' => $path];
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCgroupValue(string $path, string $file, string $value): bool
|
||||||
|
{
|
||||||
|
$target = $path . '/' . $file;
|
||||||
|
if (!is_writable($target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_put_contents($target, $value) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBlockDeviceId(?string $device): ?string
|
||||||
|
{
|
||||||
|
if (!$device) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
exec('lsblk -no MAJ:MIN ' . escapeshellarg($device) . ' 2>/dev/null', $output, $code);
|
||||||
|
$id = trim($output[0] ?? '');
|
||||||
|
|
||||||
|
return $id !== '' ? $id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cgroupWriteCpuMax(string $path, int $cpuPercent): void
|
||||||
|
{
|
||||||
|
$period = 100000;
|
||||||
|
|
||||||
|
if ($cpuPercent <= 0) {
|
||||||
|
writeCgroupValue($path, 'cpu.max', 'max ' . $period);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$quota = (int) round($period * ($cpuPercent / 100));
|
||||||
|
$quota = max($quota, 1000);
|
||||||
|
writeCgroupValue($path, 'cpu.max', $quota . ' ' . $period);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cgroupWriteMemoryMax(string $path, int $memoryMb): void
|
||||||
|
{
|
||||||
|
if ($memoryMb <= 0) {
|
||||||
|
writeCgroupValue($path, 'memory.max', 'max');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes = $memoryMb * 1024 * 1024;
|
||||||
|
writeCgroupValue($path, 'memory.max', (string) $bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cgroupWriteIoMax(string $path, int $ioMb): void
|
||||||
|
{
|
||||||
|
$device = getRootBlockDevice();
|
||||||
|
$deviceId = getBlockDeviceId($device);
|
||||||
|
|
||||||
|
if (!$deviceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ioMb <= 0) {
|
||||||
|
writeCgroupValue($path, 'io.max', $deviceId . ' rbps=max wbps=max');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes = $ioMb * 1024 * 1024;
|
||||||
|
writeCgroupValue($path, 'io.max', $deviceId . ' rbps=' . $bytes . ' wbps=' . $bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveUserProcessesToCgroup(int $uid, string $cgroupPath): int
|
||||||
|
{
|
||||||
|
$moved = 0;
|
||||||
|
foreach (glob('/proc/[0-9]*') as $procPath) {
|
||||||
|
$statusFile = $procPath . '/status';
|
||||||
|
if (!is_readable($statusFile)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = file($statusFile, FILE_IGNORE_NEW_LINES);
|
||||||
|
if (!$status) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$matchesUid = false;
|
||||||
|
foreach ($status as $line) {
|
||||||
|
if (str_starts_with($line, 'Uid:')) {
|
||||||
|
$parts = preg_split('/\s+/', trim($line));
|
||||||
|
$matchesUid = isset($parts[1]) && (int) $parts[1] === $uid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$matchesUid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pid = basename($procPath);
|
||||||
|
if (!ctype_digit($pid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@file_put_contents($cgroupPath . '/cgroup.procs', $pid) !== false) {
|
||||||
|
$moved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $moved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cgroupSyncUserProcesses(array $params): array
|
||||||
|
{
|
||||||
|
$username = $params['username'] ?? '';
|
||||||
|
if (!validateUsername($username)) {
|
||||||
|
return ['success' => false, 'error' => 'Invalid username format'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInfo = posix_getpwnam($username);
|
||||||
|
if (!$userInfo) {
|
||||||
|
return ['success' => false, 'error' => 'User not found'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$cgroup = ensureUserCgroup($username);
|
||||||
|
if (!($cgroup['success'] ?? false)) {
|
||||||
|
return $cgroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moved = moveUserProcessesToCgroup((int) $userInfo['uid'], $cgroup['path']);
|
||||||
|
|
||||||
|
return ['success' => true, 'moved' => $moved];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cgroupSyncAllProcesses(array $params): array
|
||||||
|
{
|
||||||
|
$rootResult = ensureJabaliCgroupRoot();
|
||||||
|
if (!($rootResult['success'] ?? false)) {
|
||||||
|
return $rootResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
$movedTotal = 0;
|
||||||
|
foreach (glob('/home/*', GLOB_ONLYDIR) as $homeDir) {
|
||||||
|
$username = basename($homeDir);
|
||||||
|
if (!validateUsername($username)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = cgroupSyncUserProcesses(['username' => $username]);
|
||||||
|
if ($result['success'] ?? false) {
|
||||||
|
$movedTotal += (int) ($result['moved'] ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => true, 'moved' => $movedTotal];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cgroupClearAllUserLimits(array $params): array
|
||||||
|
{
|
||||||
|
$rootResult = ensureJabaliCgroupRoot();
|
||||||
|
if (!($rootResult['success'] ?? false)) {
|
||||||
|
return $rootResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleared = 0;
|
||||||
|
foreach (glob(getJabaliCgroupRoot() . '/user-*') as $cgroupPath) {
|
||||||
|
if (!is_dir($cgroupPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cgroupWriteCpuMax($cgroupPath, 0);
|
||||||
|
cgroupWriteMemoryMax($cgroupPath, 0);
|
||||||
|
cgroupWriteIoMax($cgroupPath, 0);
|
||||||
|
$cleared++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => true, 'cleared' => $cleared];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cgroupRemoveUser(string $username): void
|
||||||
|
{
|
||||||
|
$path = getUserCgroupPath($username);
|
||||||
|
if (is_dir($path)) {
|
||||||
|
@rmdir($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCgroupStatValue(string $path, string $key): ?int
|
||||||
|
{
|
||||||
|
$file = $path . '/cpu.stat';
|
||||||
|
if (!is_readable($file)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = file($file, FILE_IGNORE_NEW_LINES);
|
||||||
|
if (!$lines) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (str_starts_with($line, $key . ' ')) {
|
||||||
|
$parts = explode(' ', trim($line));
|
||||||
|
return isset($parts[1]) ? (int) $parts[1] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCgroupIoTotal(string $path): int
|
||||||
|
{
|
||||||
|
$file = $path . '/io.stat';
|
||||||
|
if (!is_readable($file)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = 0;
|
||||||
|
$lines = file($file, FILE_IGNORE_NEW_LINES);
|
||||||
|
if (!$lines) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (!str_contains($line, 'rbytes=') && !str_contains($line, 'wbytes=')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/rbytes=(\d+)/', $line, $m)) {
|
||||||
|
$total += (int) $m[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/wbytes=(\d+)/', $line, $m)) {
|
||||||
|
$total += (int) $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
|
|
||||||
function cgroupApplyUserLimits(array $params): array
|
function cgroupApplyUserLimits(array $params): array
|
||||||
{
|
{
|
||||||
$username = $params['username'] ?? '';
|
$username = $params['username'] ?? '';
|
||||||
@@ -3055,46 +3372,28 @@ function cgroupApplyUserLimits(array $params): array
|
|||||||
return ['success' => false, 'error' => 'Invalid username format'];
|
return ['success' => false, 'error' => 'Invalid username format'];
|
||||||
}
|
}
|
||||||
|
|
||||||
exec('id -u ' . escapeshellarg($username) . ' 2>/dev/null', $uidOutput, $uidCode);
|
if (!posix_getpwnam($username)) {
|
||||||
if ($uidCode !== 0) {
|
|
||||||
return ['success' => false, 'error' => 'User not found'];
|
return ['success' => false, 'error' => 'User not found'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$uid = trim($uidOutput[0] ?? '');
|
|
||||||
$slice = "user-{$uid}.slice";
|
|
||||||
|
|
||||||
$cpu = isset($params['cpu_limit_percent']) ? (int) $params['cpu_limit_percent'] : 0;
|
$cpu = isset($params['cpu_limit_percent']) ? (int) $params['cpu_limit_percent'] : 0;
|
||||||
$memory = isset($params['memory_limit_mb']) ? (int) $params['memory_limit_mb'] : 0;
|
$memory = isset($params['memory_limit_mb']) ? (int) $params['memory_limit_mb'] : 0;
|
||||||
$io = isset($params['io_limit_mb']) ? (int) $params['io_limit_mb'] : 0;
|
$io = isset($params['io_limit_mb']) ? (int) $params['io_limit_mb'] : 0;
|
||||||
|
|
||||||
$properties = [];
|
$cgroup = ensureUserCgroup($username);
|
||||||
if ($cpu > 0) {
|
if (!($cgroup['success'] ?? false)) {
|
||||||
$properties[] = "CPUQuota={$cpu}%";
|
return $cgroup;
|
||||||
}
|
|
||||||
if ($memory > 0) {
|
|
||||||
$properties[] = "MemoryMax={$memory}M";
|
|
||||||
}
|
|
||||||
if ($io > 0) {
|
|
||||||
$device = getRootBlockDevice();
|
|
||||||
if ($device) {
|
|
||||||
$properties[] = "IOReadBandwidthMax={$device} {$io}M";
|
|
||||||
$properties[] = "IOWriteBandwidthMax={$device} {$io}M";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($properties)) {
|
$path = $cgroup['path'];
|
||||||
return cgroupClearUserLimits(['username' => $username]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$command = 'systemctl set-property ' . escapeshellarg($slice) . ' ' . implode(' ', array_map('escapeshellarg', $properties)) . ' 2>&1';
|
cgroupWriteCpuMax($path, $cpu);
|
||||||
exec($command, $output, $code);
|
cgroupWriteMemoryMax($path, $memory);
|
||||||
if ($code !== 0) {
|
cgroupWriteIoMax($path, $io);
|
||||||
return ['success' => false, 'error' => implode("\n", $output)];
|
|
||||||
}
|
|
||||||
|
|
||||||
exec('systemctl daemon-reload 2>&1');
|
cgroupSyncUserProcesses(['username' => $username]);
|
||||||
|
|
||||||
return ['success' => true, 'message' => 'Resource limits applied'];
|
return ['success' => true, 'message' => 'Resource limits applied', 'path' => $path];
|
||||||
}
|
}
|
||||||
|
|
||||||
function cgroupClearUserLimits(array $params): array
|
function cgroupClearUserLimits(array $params): array
|
||||||
@@ -3104,22 +3403,21 @@ function cgroupClearUserLimits(array $params): array
|
|||||||
return ['success' => false, 'error' => 'Invalid username format'];
|
return ['success' => false, 'error' => 'Invalid username format'];
|
||||||
}
|
}
|
||||||
|
|
||||||
exec('id -u ' . escapeshellarg($username) . ' 2>/dev/null', $uidOutput, $uidCode);
|
if (!posix_getpwnam($username)) {
|
||||||
if ($uidCode !== 0) {
|
|
||||||
return ['success' => false, 'error' => 'User not found'];
|
return ['success' => false, 'error' => 'User not found'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$uid = trim($uidOutput[0] ?? '');
|
$cgroup = ensureUserCgroup($username);
|
||||||
$slice = "user-{$uid}.slice";
|
if (!($cgroup['success'] ?? false)) {
|
||||||
|
return $cgroup;
|
||||||
exec('systemctl revert ' . escapeshellarg($slice) . ' 2>&1', $output, $code);
|
|
||||||
exec('systemctl daemon-reload 2>&1');
|
|
||||||
|
|
||||||
if ($code !== 0) {
|
|
||||||
return ['success' => false, 'error' => implode("\n", $output)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['success' => true, 'message' => 'Resource limits cleared'];
|
$path = $cgroup['path'];
|
||||||
|
cgroupWriteCpuMax($path, 0);
|
||||||
|
cgroupWriteMemoryMax($path, 0);
|
||||||
|
cgroupWriteIoMax($path, 0);
|
||||||
|
|
||||||
|
return ['success' => true, 'message' => 'Resource limits cleared', 'path' => $path];
|
||||||
}
|
}
|
||||||
|
|
||||||
function databasePersistTuning(array $params): array
|
function databasePersistTuning(array $params): array
|
||||||
@@ -23745,71 +24043,99 @@ function usageUserResources(array $params): array
|
|||||||
|
|
||||||
$uid = (int) $userInfo['uid'];
|
$uid = (int) $userInfo['uid'];
|
||||||
|
|
||||||
$cpuTotal = 0.0;
|
$cpuPercent = 0.0;
|
||||||
|
$cpuUsageUsec = null;
|
||||||
$memoryBytes = 0;
|
$memoryBytes = 0;
|
||||||
|
$diskIoTotal = 0;
|
||||||
|
|
||||||
exec("ps -u " . escapeshellarg($username) . " -o %cpu=,rss= 2>/dev/null", $psOut, $psCode);
|
$cgroupPath = getUserCgroupPath($username);
|
||||||
if ($psCode === 0) {
|
if (is_dir($cgroupPath)) {
|
||||||
foreach ($psOut as $line) {
|
$memoryCurrent = @file_get_contents($cgroupPath . '/memory.current');
|
||||||
$parts = preg_split('/\\s+/', trim($line));
|
if ($memoryCurrent !== false) {
|
||||||
if (count($parts) < 2) {
|
$memoryBytes = (int) trim($memoryCurrent);
|
||||||
continue;
|
}
|
||||||
|
|
||||||
|
$cpuUsageUsec = readCgroupStatValue($cgroupPath, 'usage_usec');
|
||||||
|
$diskIoTotal = readCgroupIoTotal($cgroupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cpuUsageUsec === null || $memoryBytes === 0) {
|
||||||
|
$cpuTotal = 0.0;
|
||||||
|
$rssTotal = 0;
|
||||||
|
|
||||||
|
exec("ps -u " . escapeshellarg($username) . " -o %cpu=,rss= 2>/dev/null", $psOut, $psCode);
|
||||||
|
if ($psCode === 0) {
|
||||||
|
foreach ($psOut as $line) {
|
||||||
|
$parts = preg_split('/\\s+/', trim($line));
|
||||||
|
if (count($parts) < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$cpuTotal += (float) $parts[0];
|
||||||
|
$rssTotal += (int) $parts[1] * 1024;
|
||||||
}
|
}
|
||||||
$cpuTotal += (float) $parts[0];
|
}
|
||||||
$memoryBytes += (int) $parts[1] * 1024;
|
|
||||||
|
$cpuPercent = round($cpuTotal, 2);
|
||||||
|
if ($memoryBytes === 0) {
|
||||||
|
$memoryBytes = $rssTotal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$diskRead = 0;
|
if ($diskIoTotal === 0) {
|
||||||
$diskWrite = 0;
|
$diskRead = 0;
|
||||||
|
$diskWrite = 0;
|
||||||
|
|
||||||
foreach (glob('/proc/[0-9]*') as $procPath) {
|
foreach (glob('/proc/[0-9]*') as $procPath) {
|
||||||
$statusFile = $procPath . '/status';
|
$statusFile = $procPath . '/status';
|
||||||
if (!is_readable($statusFile)) {
|
if (!is_readable($statusFile)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$status = file($statusFile, FILE_IGNORE_NEW_LINES);
|
$status = file($statusFile, FILE_IGNORE_NEW_LINES);
|
||||||
if (!$status) {
|
if (!$status) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$matchesUid = false;
|
$matchesUid = false;
|
||||||
foreach ($status as $line) {
|
foreach ($status as $line) {
|
||||||
if (str_starts_with($line, 'Uid:')) {
|
if (str_starts_with($line, 'Uid:')) {
|
||||||
$parts = preg_split('/\\s+/', trim($line));
|
$parts = preg_split('/\\s+/', trim($line));
|
||||||
$matchesUid = isset($parts[1]) && (int) $parts[1] === $uid;
|
$matchesUid = isset($parts[1]) && (int) $parts[1] === $uid;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$matchesUid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ioFile = $procPath . '/io';
|
||||||
|
if (!is_readable($ioFile)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ioLines = file($ioFile, FILE_IGNORE_NEW_LINES);
|
||||||
|
if (!$ioLines) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($ioLines as $line) {
|
||||||
|
if (str_starts_with($line, 'read_bytes:')) {
|
||||||
|
$diskRead += (int) trim(substr($line, 11));
|
||||||
|
} elseif (str_starts_with($line, 'write_bytes:')) {
|
||||||
|
$diskWrite += (int) trim(substr($line, 12));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$matchesUid) {
|
$diskIoTotal = $diskRead + $diskWrite;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ioFile = $procPath . '/io';
|
|
||||||
if (!is_readable($ioFile)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ioLines = file($ioFile, FILE_IGNORE_NEW_LINES);
|
|
||||||
if (!$ioLines) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($ioLines as $line) {
|
|
||||||
if (str_starts_with($line, 'read_bytes:')) {
|
|
||||||
$diskRead += (int) trim(substr($line, 11));
|
|
||||||
} elseif (str_starts_with($line, 'write_bytes:')) {
|
|
||||||
$diskWrite += (int) trim(substr($line, 12));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'cpu_percent' => round($cpuTotal, 2),
|
'cpu_percent' => $cpuPercent,
|
||||||
|
'cpu_usage_usec_total' => $cpuUsageUsec,
|
||||||
'memory_bytes' => $memoryBytes,
|
'memory_bytes' => $memoryBytes,
|
||||||
'disk_io_total_bytes' => $diskRead + $diskWrite,
|
'disk_io_total_bytes' => $diskIoTotal,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
install.sh
33
install.sh
@@ -2519,6 +2519,33 @@ SERVICE
|
|||||||
log "Jabali Agent service configured"
|
log "Jabali Agent service configured"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setup_cgroup_limits() {
|
||||||
|
header "Setting Up cgroup v2 Resource Limits"
|
||||||
|
|
||||||
|
if [[ ! -f /sys/fs/cgroup/cgroup.controllers ]]; then
|
||||||
|
warn "cgroup v2 not detected - resource limits will be disabled"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/jabali.slice << 'SLICE'
|
||||||
|
[Slice]
|
||||||
|
Delegate=yes
|
||||||
|
CPUAccounting=yes
|
||||||
|
MemoryAccounting=yes
|
||||||
|
IOAccounting=yes
|
||||||
|
SLICE
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable jabali.slice 2>/dev/null || true
|
||||||
|
systemctl start jabali.slice 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ -w /sys/fs/cgroup/jabali.slice/cgroup.subtree_control ]]; then
|
||||||
|
echo "+cpu +memory +io" > /sys/fs/cgroup/jabali.slice/cgroup.subtree_control || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "cgroup v2 slice configured"
|
||||||
|
}
|
||||||
|
|
||||||
setup_queue_service() {
|
setup_queue_service() {
|
||||||
header "Setting Up Jabali Queue Worker"
|
header "Setting Up Jabali Queue Worker"
|
||||||
|
|
||||||
@@ -2891,6 +2918,11 @@ uninstall() {
|
|||||||
rm -f /etc/systemd/system/jabali-queue.service
|
rm -f /etc/systemd/system/jabali-queue.service
|
||||||
rm -rf /etc/systemd/system/jabali-queue.service.d
|
rm -rf /etc/systemd/system/jabali-queue.service.d
|
||||||
|
|
||||||
|
systemctl stop jabali.slice 2>/dev/null || true
|
||||||
|
rm -f /etc/systemd/system/jabali.slice
|
||||||
|
rm -rf /sys/fs/cgroup/jabali.slice/user-* 2>/dev/null || true
|
||||||
|
rmdir /sys/fs/cgroup/jabali.slice 2>/dev/null || true
|
||||||
|
|
||||||
local services=(
|
local services=(
|
||||||
nginx
|
nginx
|
||||||
php-fpm
|
php-fpm
|
||||||
@@ -3221,6 +3253,7 @@ main() {
|
|||||||
configure_redis
|
configure_redis
|
||||||
setup_jabali
|
setup_jabali
|
||||||
setup_agent_service
|
setup_agent_service
|
||||||
|
setup_cgroup_limits
|
||||||
setup_queue_service
|
setup_queue_service
|
||||||
setup_scheduler_cron
|
setup_scheduler_cron
|
||||||
setup_logrotate
|
setup_logrotate
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ Schedule::command('jabali:collect-user-usage')
|
|||||||
->runInBackground()
|
->runInBackground()
|
||||||
->appendOutputTo(storage_path('logs/user-usage.log'));
|
->appendOutputTo(storage_path('logs/user-usage.log'));
|
||||||
|
|
||||||
|
// Cgroup Sync - runs every minute to keep user processes assigned to cgroups
|
||||||
|
Schedule::command('jabali:sync-cgroups')
|
||||||
|
->everyMinute()
|
||||||
|
->withoutOverlapping()
|
||||||
|
->runInBackground()
|
||||||
|
->appendOutputTo(storage_path('logs/cgroup-sync.log'));
|
||||||
|
|
||||||
// Audit Log Rotation - runs daily to prune old audit logs (default: 90 days retention)
|
// Audit Log Rotation - runs daily to prune old audit logs (default: 90 days retention)
|
||||||
Schedule::call(function () {
|
Schedule::call(function () {
|
||||||
$deleted = AuditLog::prune();
|
$deleted = AuditLog::prune();
|
||||||
|
|||||||
50
tests/Feature/JabaliSyncCgroupsTest.php
Normal file
50
tests/Feature/JabaliSyncCgroupsTest.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Services\Agent\AgentClient;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class JabaliSyncCgroupsTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_syncs_all_processes(): void
|
||||||
|
{
|
||||||
|
$this->app->instance(AgentClient::class, new FakeAgentClient([
|
||||||
|
'success' => true,
|
||||||
|
'moved' => 4,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->artisan('jabali:sync-cgroups')
|
||||||
|
->expectsOutput('Synced cgroups, moved 4 process(es).')
|
||||||
|
->assertExitCode(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_syncs_single_user(): void
|
||||||
|
{
|
||||||
|
$this->app->instance(AgentClient::class, new FakeAgentClient([
|
||||||
|
'success' => true,
|
||||||
|
'moved' => 1,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->artisan('jabali:sync-cgroups --user=testuser')
|
||||||
|
->expectsOutput('Synced cgroups, moved 1 process(es).')
|
||||||
|
->assertExitCode(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeAgentClient extends AgentClient
|
||||||
|
{
|
||||||
|
public function __construct(private array $result) {}
|
||||||
|
|
||||||
|
public function cgroupSyncUserProcesses(string $username): array
|
||||||
|
{
|
||||||
|
return $this->result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cgroupSyncAllProcesses(): array
|
||||||
|
{
|
||||||
|
return $this->result;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user