Add CPU limit to node (#239) to resolve #233

* add node cpu limit to backend

* update makenodecommand

* add node cpu limit to frontend

* add migration and update mysql schema

* run pint

* fix typo in mysql schema

* forgot this assert

* forgot to setCpu here

* run pint

* adjust migration

* Fix db migration

* make cpu optional

* set default value for cpu in node deployment

* update mysql schema

---------

Co-authored-by: notCharles <charles@pelican.dev>
This commit is contained in:
Boy132 2024-05-22 08:34:43 +02:00 committed by GitHub
parent eadaec1b30
commit 4dd833562b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 195 additions and 28 deletions

View File

@ -20,6 +20,8 @@ class MakeNodeCommand extends Command
{--overallocateMemory= : Enter the amount of ram to overallocate (% or -1 to overallocate the maximum).}
{--maxDisk= : Set the max disk amount.}
{--overallocateDisk= : Enter the amount of disk to overallocate (% or -1 to overallocate the maximum).}
{--maxCpu= : Set the max cpu amount.}
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
{--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
@ -58,6 +60,8 @@ class MakeNodeCommand extends Command
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'));
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'));
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'));
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(__('commands.make_node.cpu'));
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(__('commands.make_node.cpu_overallocate'));
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '100');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022');

View File

@ -311,6 +311,45 @@ class CreateNode extends CreateRecord
->default(0)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
]),
]),
]);

View File

@ -318,6 +318,47 @@ class EditNode extends EditRecord
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->required()
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
]),
Tabs\Tab::make('Configuration File')
->icon('tabler-code')

View File

@ -52,6 +52,12 @@ class ListNodes extends ListRecords
->suffix(' GiB')
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
->sortable(),
Tables\Columns\TextColumn::make('cpu')
->visibleFrom('sm')
->icon('tabler-file')
->numeric()
->suffix(' %')
->sortable(),
Tables\Columns\IconColumn::make('scheme')
->visibleFrom('xl')
->label('SSL')

View File

@ -36,7 +36,7 @@ class NodeController extends ApplicationApiController
{
$nodes = QueryBuilder::for(Node::query())
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
->allowedSorts(['id', 'uuid', 'memory', 'disk'])
->allowedSorts(['id', 'uuid', 'memory', 'disk', 'cpu'])
->paginate($request->query('per_page') ?? 50);
return $this->fractal->collection($nodes)

View File

@ -30,6 +30,7 @@ class NodeDeploymentController extends ApplicationApiController
$nodes = $this->viableNodesService
->setMemory($data['memory'])
->setDisk($data['disk'])
->setCpu($data['cpu'] ?? 0)
->handle((int) $request->query('per_page'), (int) $request->query('page'));
return $this->fractal->collection($nodes)

View File

@ -10,6 +10,7 @@ class GetDeployableNodesRequest extends GetNodesRequest
'page' => 'integer',
'memory' => 'required|integer|min:0',
'disk' => 'required|integer|min:0',
'cpu' => 'sometimes|integer|min:0',
];
}
}

View File

@ -28,6 +28,8 @@ class StoreNodeRequest extends ApplicationApiRequest
'memory_overallocate',
'disk',
'disk_overallocate',
'cpu',
'cpu_overallocate',
'upload_size',
'daemon_listen',
'daemon_sftp',

View File

@ -26,6 +26,8 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
* @property int $memory_overallocate
* @property int $disk
* @property int $disk_overallocate
* @property int $cpu
* @property int $cpu_overallocate
* @property int $upload_size
* @property string $daemon_token_id
* @property string $daemon_token
@ -63,6 +65,7 @@ class Node extends Model
public int $sum_memory;
public int $sum_disk;
public int $sum_cpu;
/**
* Fields that are mass assignable.
@ -71,7 +74,8 @@ class Node extends Model
'public', 'name',
'fqdn', 'scheme', 'behind_proxy',
'memory', 'memory_overallocate', 'disk',
'disk_overallocate', 'upload_size', 'daemon_base',
'disk_overallocate', 'cpu', 'cpu_overallocate',
'upload_size', 'daemon_base',
'daemon_sftp', 'daemon_listen',
'description', 'maintenance_mode',
];
@ -87,6 +91,8 @@ class Node extends Model
'memory_overallocate' => 'required|numeric|min:-1',
'disk' => 'required|numeric|min:0',
'disk_overallocate' => 'required|numeric|min:-1',
'cpu' => 'required|numeric|min:0',
'cpu_overallocate' => 'required|numeric|min:-1',
'daemon_base' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
'daemon_sftp' => 'required|numeric|between:1,65535',
'daemon_listen' => 'required|numeric|between:1,65535',
@ -104,6 +110,8 @@ class Node extends Model
'memory_overallocate' => 0,
'disk' => 0,
'disk_overallocate' => 0,
'cpu' => 0,
'cpu_overallocate' => 0,
'daemon_base' => '/var/lib/pelican/volumes',
'daemon_sftp' => 2022,
'daemon_listen' => 8080,
@ -116,6 +124,7 @@ class Node extends Model
return [
'memory' => 'integer',
'disk' => 'integer',
'cpu' => 'integer',
'daemon_listen' => 'integer',
'daemon_sftp' => 'integer',
'behind_proxy' => 'boolean',
@ -239,12 +248,13 @@ class Node extends Model
/**
* Returns a boolean if the node is viable for an additional server to be placed on it.
*/
public function isViable(int $memory, int $disk): bool
public function isViable(int $memory, int $disk, int $cpu): bool
{
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
$cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100));
return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit;
return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit && ($this->sum_cpu + $cpu) <= $cpuLimit;
}
public static function getForServerCreation()

View File

@ -10,8 +10,20 @@ use App\Exceptions\Service\Deployment\NoViableNodeException;
class FindViableNodesService
{
protected ?int $disk = null;
protected ?int $memory = null;
protected ?int $disk = null;
protected ?int $cpu = null;
/**
* Set the amount of memory that this server will be using. As with disk space, nodes that
* do not have enough free memory will be filtered out.
*/
public function setMemory(int $memory): self
{
$this->memory = $memory;
return $this;
}
/**
* Set the amount of disk that will be used by the server being created. Nodes will be
@ -26,12 +38,13 @@ class FindViableNodesService
}
/**
* Set the amount of memory that this server will be using. As with disk space, nodes that
* do not have enough free memory will be filtered out.
* Set the amount of cpu that will be used by the server being created. Nodes will be
* filtered out if they do not have enough available free cpu for this server
* to be placed on.
*/
public function setMemory(int $memory): self
public function setCpu(int $cpu): self
{
$this->memory = $memory;
$this->cpu = $cpu;
return $this;
}
@ -41,8 +54,8 @@ class FindViableNodesService
* be passed to the AllocationSelectionService to return a single allocation.
*
* This functionality is used for automatic deployments of servers and will
* attempt to find all nodes in the defined locations that meet the disk and
* memory availability requirements. Any nodes not meeting those requirements
* attempt to find all nodes in the defined locations that meet the memory, disk
* and cpu availability requirements. Any nodes not meeting those requirements
* are tossed out, as are any nodes marked as non-public, meaning automatic
* deployments should not be done against them.
*
@ -55,18 +68,21 @@ class FindViableNodesService
*/
public function handle(int $perPage = null, int $page = null): LengthAwarePaginator|Collection
{
Assert::integer($this->disk, 'Disk space must be an int, got %s');
Assert::integer($this->memory, 'Memory usage must be an int, got %s');
Assert::integer($this->disk, 'Disk space must be an int, got %s');
Assert::integer($this->cpu, 'CPU must be an int, got %s');
$query = Node::query()->select('nodes.*')
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory')
->selectRaw('IFNULL(SUM(servers.disk), 0) as sum_disk')
->selectRaw('IFNULL(SUM(servers.cpu), 0) as sum_cpu')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.public', 1);
$results = $query->groupBy('nodes.id')
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [$this->memory])
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk]);
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk])
->havingRaw('(IFNULL(SUM(servers.cpu), 0) + ?) <= (nodes.cpu * (1 + (nodes.cpu_overallocate / 100)))', [$this->cpu]);
if (!is_null($page)) {
$results = $results->paginate($perPage ?? 50, ['*'], 'page', $page);

View File

@ -111,8 +111,9 @@ class ServerCreationService
{
/** @var \Illuminate\Support\Collection $nodes */
$nodes = $this->findViableNodesService
->setDisk(Arr::get($data, 'disk'))
->setMemory(Arr::get($data, 'memory'))
->setDisk(Arr::get($data, 'disk'))
->setCpu(Arr::get($data, 'cpu'))
->handle();
return $this->allocationSelectionService->setDedicated($deployment->isDedicated())

View File

@ -57,13 +57,13 @@ class TransferServerService
// Check if the node is viable for the transfer.
$node = Node::query()
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk, IFNULL(SUM(servers.cpu), 0) as sum_cpu')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.id', $node_id)
->first();
if (!$node->isViable($server->memory, $server->disk)) {
if (!$node->isViable($server->memory, $server->disk, $server->cpu)) {
return false;
}

View File

@ -34,11 +34,12 @@ class NodeTransformer extends BaseTransformer
$response[$node->getUpdatedAtColumn()] = $this->formatTimestamp($node->updated_at);
$response[$node->getCreatedAtColumn()] = $this->formatTimestamp($node->created_at);
$resources = $node->servers()->select(['memory', 'disk'])->get();
$resources = $node->servers()->select(['memory', 'disk', 'cpu'])->get();
$response['allocated_resources'] = [
'memory' => $resources->sum('memory'),
'disk' => $resources->sum('disk'),
'cpu' => $resources->sum('cpu'),
];
return $response;

View File

@ -33,6 +33,8 @@ class NodeFactory extends Factory
'memory_overallocate' => 0,
'disk' => 10240,
'disk_overallocate' => 0,
'cpu' => 100,
'cpu_overallocate' => 0,
'upload_size' => 100,
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
'daemon_token' => Crypt::encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH)),

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->unsignedInteger('cpu')->default(0)->after('disk_overallocate');
$table->integer('cpu_overallocate')->default(0)->after('cpu');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->dropColumn('cpu');
$table->dropColumn('cpu_overallocate');
});
}
};

View File

@ -359,6 +359,8 @@ CREATE TABLE `nodes` (
`memory_overallocate` int NOT NULL DEFAULT '0',
`disk` int unsigned NOT NULL,
`disk_overallocate` int NOT NULL DEFAULT '0',
`cpu` int unsigned NOT NULL DEFAULT '0',
`cpu_overallocate` int NOT NULL DEFAULT '0',
`upload_size` int unsigned NOT NULL DEFAULT '100',
`daemon_token_id` char(16) COLLATE utf8mb4_unicode_ci NOT NULL,
`daemon_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
@ -845,3 +847,4 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (198,'2024_03_14_05
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (201,'2024_04_20_214441_add_egg_var_sort',3);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (203,'2024_04_14_002250_update_column_names',4);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (204,'2024_05_08_094823_rename_oom_disabled_column_to_oom_killer',1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (205,'2024_05_16_091207_add_cpu_columns_to_nodes_table',1);

View File

@ -31,7 +31,9 @@ return [
'memory' => 'Enter the maximum amount of memory',
'memory_overallocate' => 'Enter the amount of memory to over allocate by, -1 will disable checking and 0 will prevent creating new servers',
'disk' => 'Enter the maximum amount of disk space',
'disk_overallocate' => 'Enter the amount of memory to over allocate by, -1 will disable checking and 0 will prevent creating new server',
'disk_overallocate' => 'Enter the amount of disk to over allocate by, -1 will disable checking and 0 will prevent creating new server',
'cpu' => 'Enter the maximum amount of cpu',
'cpu_overallocate' => 'Enter the amount of cpu to over allocate by, -1 will disable checking and 0 will prevent creating new server',
'upload_size' => "'Enter the maximum filesize upload",
'daemonListen' => 'Enter the daemon listening port',
'daemonSFTP' => 'Enter the daemon SFTP listening port',

View File

@ -19,20 +19,28 @@ class FindViableNodesServiceTest extends IntegrationTestCase
Node::query()->delete();
}
public function testExceptionIsThrownIfNoDiskSpaceHasBeenSet(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Disk space must be an int, got NULL');
$this->getService()->handle();
}
public function testExceptionIsThrownIfNoMemoryHasBeenSet(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Memory usage must be an int, got NULL');
$this->getService()->setDisk(10)->handle();
$this->getService()->setDisk(10)->setCpu(10)->handle();
}
public function testExceptionIsThrownIfNoDiskSpaceHasBeenSet(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Disk space must be an int, got NULL');
$this->getService()->setMemory(10)->setCpu(10)->handle();
}
public function testExceptionIsThrownIfNoCpuHasBeenSet(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('CPU must be an int, got NULL');
$this->getService()->setMemory(10)->setDisk(10)->handle();
}
private function getService(): FindViableNodesService