Skip to content

Commit 30f5838

Browse files
committed
Add support for automatic node assignment
1 parent 09b0a3d commit 30f5838

File tree

4 files changed

+235
-37
lines changed

4 files changed

+235
-37
lines changed

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@ This file is a running track of new features and fixes to each version of the pa
33

44
This project follows [Semantic Versioning](http://semver.org) guidelines.
55

6-
## v0.4.1
6+
## v0.5.0 (Bodacious Boreopterus) [Unreleased]
7+
8+
### Added
9+
* Support for creating server without having to assign a node and allocation manually. Simply select the checkbox or pass `auto_deploy=true` to the API to auto-select a node and allocation given a location.
10+
11+
### Changed
12+
### Fixed
13+
### Deprecated
14+
### Removed
15+
### Security
16+
17+
## v0.4.1 (Articulate Aerotitan)
718

819
### Changed
920
* Overallocate fields are now auto-filled with a value of `0`
@@ -13,7 +24,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
1324
* Server link in navbar directed to 404 link (PR by [@Randomfish132](https://github.com/Randomfish132))
1425
* Composer fails to finish ([#92](https://github.com/Pterodactyl/Panel/issues/92), PR by [@schrej](https://github.com/schrej), thanks [@parkervcp](https://github.com/parkervcp))
1526

16-
## v0.4.0
27+
## v0.4.0 (Arty Aerodactylus)
1728

1829
### Added
1930
* Task scheduler supporting customized CRON syntax or dropdown selected options. (currently only support command and power options)

app/Repositories/ServerRepository.php

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
use Pterodactyl\Models;
3333
use Pterodactyl\Services\UuidService;
34+
use Pterodactyl\Services\DeploymentService;
3435

3536
use Pterodactyl\Exceptions\DisplayException;
3637
use Pterodactyl\Exceptions\AccountNotFoundException;
@@ -88,23 +89,37 @@ public function create(array $data)
8889

8990
// Validate Fields
9091
$validator = Validator::make($data, [
91-
'owner' => 'required|email|exists:users,email',
92-
'node' => 'required|numeric|min:1|exists:nodes,id',
92+
'owner' => 'bail|required|email|exists:users,email',
9393
'name' => 'required|regex:/^([\w -]{4,35})$/',
9494
'memory' => 'required|numeric|min:0',
9595
'swap' => 'required|numeric|min:-1',
9696
'io' => 'required|numeric|min:10|max:1000',
9797
'cpu' => 'required|numeric|min:0',
9898
'disk' => 'required|numeric|min:0',
99-
'allocation' => 'numeric|exists:allocations,id|required_without:ip,port',
100-
'ip' => 'required_without:allocation|ip',
101-
'port' => 'required_without:allocation|numeric|min:1|max:65535',
102-
'service' => 'required|numeric|min:1|exists:services,id',
103-
'option' => 'required|numeric|min:1|exists:service_options,id',
99+
'service' => 'bail|required|numeric|min:1|exists:services,id',
100+
'option' => 'bail|required|numeric|min:1|exists:service_options,id',
104101
'startup' => 'string',
105102
'custom_image_name' => 'required_if:use_custom_image,on',
103+
'auto_deploy' => 'sometimes|boolean'
106104
]);
107105

106+
$validator->sometimes('node', 'bail|required|numeric|min:1|exists:nodes,id', function ($input) {
107+
return !($input->auto_deploy);
108+
});
109+
110+
$validator->sometimes('ip', 'required|ip', function ($input) {
111+
return (!$input->auto_deploy && !$input->allocation);
112+
});
113+
114+
$validator->sometimes('port', 'required|numeric|min:1|max:65535', function ($input) {
115+
return (!$input->auto_deploy && !$input->allocation);
116+
});
117+
118+
$validator->sometimes('allocation', 'numeric|exists:allocations,id', function ($input) {
119+
return !($input->auto_deploy || ($input->port && $input->ip));
120+
});
121+
122+
108123
// Run validator, throw catchable and displayable exception if it fails.
109124
// Exception includes a JSON result of failed validation rules.
110125
if ($validator->fails()) {
@@ -114,15 +129,27 @@ public function create(array $data)
114129
// Get the User ID; user exists since we passed the 'exists:users,email' part of the validation
115130
$user = Models\User::select('id')->where('email', $data['owner'])->first();
116131

117-
// Get Node Information
118-
$node = Models\Node::getByID($data['node']);
132+
$autoDeployed = false;
133+
if (isset($data['auto_deploy']) && in_array($data['auto_deploy'], [true, 1, "1"])) {
134+
// This is an auto-deployment situation
135+
// Ignore any other passed node data
136+
unset($data['node'], $data['ip'], $data['port'], $data['allocation']);
137+
138+
$autoDeployed = true;
139+
$node = DeploymentService::smartRandomNode($data['memory'], $data['disk'], $data['location']);
140+
$allocation = DeploymentService::randomAllocation($node->id);
141+
} else {
142+
$node = Models\Node::getByID($data['node']);
143+
}
119144

120145
// Verify IP & Port are a.) free and b.) assigned to the node.
121146
// We know the node exists because of 'exists:nodes,id' in the validation
122-
if (!isset($data['allocation'])) {
123-
$allocation = Models\Allocation::where('ip', $data['ip'])->where('port', $data['port'])->where('node', $data['node'])->whereNull('assigned_to')->first();
124-
} else {
125-
$allocation = Models\Allocation::where('id' , $data['allocation'])->where('node', $data['node'])->whereNull('assigned_to')->first();
147+
if (!$autoDeployed) {
148+
if (!isset($data['allocation'])) {
149+
$allocation = Models\Allocation::where('ip', $data['ip'])->where('port', $data['port'])->where('node', $data['node'])->whereNull('assigned_to')->first();
150+
} else {
151+
$allocation = Models\Allocation::where('id' , $data['allocation'])->where('node', $data['node'])->whereNull('assigned_to')->first();
152+
}
126153
}
127154

128155
// Something failed in the query, either that combo doesn't exist, or it is in use.
@@ -176,28 +203,29 @@ public function create(array $data)
176203
}
177204

178205
// Check Overallocation
179-
if (is_numeric($node->memory_overallocate) || is_numeric($node->disk_overallocate)) {
206+
if (!$autoDeployed) {
207+
if (is_numeric($node->memory_overallocate) || is_numeric($node->disk_overallocate)) {
180208

181-
$totals = Models\Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node', $node->id)->first();
209+
$totals = Models\Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node', $node->id)->first();
182210

183-
// Check memory limits
184-
if (is_numeric($node->memory_overallocate)) {
185-
$newMemory = $totals->memory + $data['memory'];
186-
$memoryLimit = ($node->memory * (1 + ($node->memory_overallocate / 100)));
187-
if($newMemory > $memoryLimit) {
188-
throw new DisplayException('The amount of memory allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->memory_overallocate + 100) . '% of its assigned ' . $node->memory . 'Mb of memory (' . $memoryLimit . 'Mb) of which ' . (($totals->memory / $node->memory) * 100) . '% (' . $totals->memory . 'Mb) is in use already. By allocating this server the node would be at ' . (($newMemory / $node->memory) * 100) . '% (' . $newMemory . 'Mb) usage.');
211+
// Check memory limits
212+
if (is_numeric($node->memory_overallocate)) {
213+
$newMemory = $totals->memory + $data['memory'];
214+
$memoryLimit = ($node->memory * (1 + ($node->memory_overallocate / 100)));
215+
if($newMemory > $memoryLimit) {
216+
throw new DisplayException('The amount of memory allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->memory_overallocate + 100) . '% of its assigned ' . $node->memory . 'Mb of memory (' . $memoryLimit . 'Mb) of which ' . (($totals->memory / $node->memory) * 100) . '% (' . $totals->memory . 'Mb) is in use already. By allocating this server the node would be at ' . (($newMemory / $node->memory) * 100) . '% (' . $newMemory . 'Mb) usage.');
217+
}
189218
}
190-
}
191219

192-
// Check Disk Limits
193-
if (is_numeric($node->disk_overallocate)) {
194-
$newDisk = $totals->disk + $data['disk'];
195-
$diskLimit = ($node->disk * (1 + ($node->disk_overallocate / 100)));
196-
if($newDisk > $diskLimit) {
197-
throw new DisplayException('The amount of disk allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->disk_overallocate + 100) . '% of its assigned ' . $node->disk . 'Mb of disk (' . $diskLimit . 'Mb) of which ' . (($totals->disk / $node->disk) * 100) . '% (' . $totals->disk . 'Mb) is in use already. By allocating this server the node would be at ' . (($newDisk / $node->disk) * 100) . '% (' . $newDisk . 'Mb) usage.');
220+
// Check Disk Limits
221+
if (is_numeric($node->disk_overallocate)) {
222+
$newDisk = $totals->disk + $data['disk'];
223+
$diskLimit = ($node->disk * (1 + ($node->disk_overallocate / 100)));
224+
if($newDisk > $diskLimit) {
225+
throw new DisplayException('The amount of disk allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->disk_overallocate + 100) . '% of its assigned ' . $node->disk . 'Mb of disk (' . $diskLimit . 'Mb) of which ' . (($totals->disk / $node->disk) * 100) . '% (' . $totals->disk . 'Mb) is in use already. By allocating this server the node would be at ' . (($newDisk / $node->disk) * 100) . '% (' . $newDisk . 'Mb) usage.');
226+
}
198227
}
199228
}
200-
201229
}
202230

203231
DB::beginTransaction();
@@ -211,7 +239,7 @@ public function create(array $data)
211239
$server->fill([
212240
'uuid' => $generatedUuid,
213241
'uuidShort' => $uuid->generateShort('servers', 'uuidShort', $generatedUuid),
214-
'node' => $data['node'],
242+
'node' => $node->id,
215243
'name' => $data['name'],
216244
'suspended' => 0,
217245
'owner' => $user->id,
@@ -226,7 +254,8 @@ public function create(array $data)
226254
'option' => $data['option'],
227255
'startup' => $data['startup'],
228256
'daemonSecret' => $uuid->generate('servers', 'daemonSecret'),
229-
'username' => $this->generateSFTPUsername($data['name'])
257+
'username' => $this->generateSFTPUsername($data['name']),
258+
'sftp_password' => Crypt::encrypt('not set')
230259
]);
231260
$server->save();
232261

@@ -292,7 +321,6 @@ public function create(array $data)
292321
throw new DisplayException('There was an error while attempting to connect to the daemon to add this server.', $ex);
293322
} catch (\Exception $ex) {
294323
DB::rollBack();
295-
Log:error($ex);
296324
throw $ex;
297325
}
298326

app/Services/DeploymentService.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
/**
3+
* Pterodactyl - Panel
4+
* Copyright (c) 2015 - 2016 Dane Everitt <dane@daneeveritt.com>
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
namespace Pterodactyl\Services;
25+
26+
use DB;
27+
28+
use Pterodactyl\Models;
29+
use Pterodactyl\Exceptions\DisplayException;
30+
31+
class DeploymentService
32+
{
33+
34+
public function __constructor()
35+
{
36+
//
37+
}
38+
39+
/**
40+
* Return a random location model. DO NOT USE.
41+
* @return \Pterodactyl\Models\Node
42+
*
43+
* @TODO Actually make this smarter. If we're selecting a random location
44+
* but then it has no nodes we should probably continue attempting all locations
45+
* until we hit one.
46+
*
47+
* Currently you should just pick a location and go from there.
48+
*/
49+
public static function randomLocation()
50+
{
51+
return Models\Location::inRandomOrder()->first();
52+
}
53+
54+
/**
55+
* Return a model instance of a random node.
56+
* @param int $location
57+
* @param array $not
58+
* @return \Pterodactyl\Models\Node
59+
*
60+
* @throws \Pterodactyl\Exceptions\DisplayException
61+
*/
62+
public static function randomNode($location, array $not = [])
63+
{
64+
$useLocation = Models\Location::findOrFail($location);
65+
$node = Models\Node::where('location', $useLocation->id)->where('public', 1)->whereNotIn('id', $not)->inRandomOrder()->first();
66+
67+
if (!$node) {
68+
throw new DisplayException("Unable to find a node in location {$useLocation->short} (id: {$useLocation->id}) that is available and has space.");
69+
}
70+
71+
return $node;
72+
}
73+
74+
/**
75+
* Selects a random node ensuring it does not put the node
76+
* over allocation limits.
77+
* @param int $memory
78+
* @param int $disk
79+
* @param int $location
80+
* @return \Pterodactyl\Models\Node;
81+
*
82+
* @throws \Pterodactyl\Exceptions\DisplayException
83+
*/
84+
public static function smartRandomNode($memory, $disk, $location = null) {
85+
$node = self::randomNode($location);
86+
$notIn = [];
87+
do {
88+
$return = self::checkNodeAllocation($node, $memory, $disk);
89+
if (!$return) {
90+
$notIn = array_merge($notIn, [
91+
$node->id
92+
]);
93+
$node = self::randomNode($location, $notIn);
94+
}
95+
} while (!$return);
96+
97+
return $node;
98+
}
99+
100+
/**
101+
* Returns a random allocation for a node.
102+
* @param int $node
103+
* @return \Models\Pterodactyl\Allocation;
104+
*/
105+
public static function randomAllocation($node)
106+
{
107+
return Models\Allocation::where('node', $node)->whereNull('assigned_to')->inRandomOrder()->first();
108+
}
109+
110+
/**
111+
* Checks that a node's allocation limits will not be passed with the given information.
112+
* @param \Pterodactyl\Models\Node $node
113+
* @param int $memory
114+
* @param int $disk
115+
* @return bool Returns true if this information would not put the node over it's limit.
116+
*/
117+
protected static function checkNodeAllocation(Models\Node $node, $memory, $disk)
118+
{
119+
if (is_numeric($node->memory_overallocate) || is_numeric($node->disk_overallocate)) {
120+
$totals = Models\Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node', $node->id)->first();
121+
122+
// Check memory limits
123+
if (is_numeric($node->memory_overallocate)) {
124+
$limit = ($node->memory * (1 + ($node->memory_overallocate / 100)));
125+
$memoryLimitReached = (($totals->memory + $memory) > $limit);
126+
}
127+
128+
// Check Disk Limits
129+
if (is_numeric($node->disk_overallocate)) {
130+
$limit = ($node->disk * (1 + ($node->disk_overallocate / 100)));
131+
$diskLimitReached = (($totals->disk + $disk) > $limit);
132+
}
133+
134+
return (!$diskLimitReached && !$memoryLimitReached);
135+
}
136+
}
137+
}

resources/views/admin/servers/new.blade.php

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
<p class="text-muted"><small>The location in which this server will be deployed.</small></p>
6666
</div>
6767
</div>
68-
<div class="form-group col-md-6 hidden">
68+
<div class="form-group col-md-6 hidden" id="allocationNode">
6969
<label for="node" class="control-label">Server Node</label>
7070
<div>
7171
<select name="node" id="getNode" class="form-control">
@@ -76,7 +76,7 @@
7676
</div>
7777
</div>
7878
<div class="row">
79-
<div class="form-group col-md-6 hidden">
79+
<div class="form-group col-md-6 hidden" id="allocationIP">
8080
<label for="ip" class="control-label">Server IP</label>
8181
<div>
8282
<select name="ip" id="getIP" class="form-control">
@@ -85,14 +85,25 @@
8585
<p class="text-muted"><small>Select the main IP that this server will be listening on. You can assign additional open IPs and ports below.</small></p>
8686
</div>
8787
</div>
88-
<div class="form-group col-md-6 hidden">
88+
<div class="form-group col-md-6 hidden" id="allocationPort">
8989
<label for="port" class="control-label">Server Port</label>
9090
<div>
9191
<select name="port" id="getPort" class="form-control"></select>
9292
<p class="text-muted"><small>Select the main port that this server will be listening on.</small></p>
9393
</div>
9494
</div>
9595
</div>
96+
<div class="row">
97+
<div class="col-md-12 fuelux">
98+
<hr style="margin-top: 10px;"/>
99+
<div class="checkbox highlight" style="margin: 0;">
100+
<label class="checkbox-custom highlight" data-initialize="checkbox">
101+
<input class="sr-only" name="auto_deploy" type="checkbox" @if(isset($oldInput['auto_deploy']))checked="checked"@endif value="1"> <strong>Enable Automatic Deployment</strong>
102+
<p class="text-muted"><small>Check this box if you want the panel to automatically select a node and allocation for this server in the given location.</small><p>
103+
</label>
104+
</div>
105+
</div>
106+
</div>
96107
</div>
97108
</div>
98109
<div class="well">
@@ -341,6 +352,17 @@
341352
342353
});
343354
355+
$('input[name="auto_deploy"]').change(function () {
356+
if ($(this).is(':checked')) {
357+
$('#allocationPort, #allocationIP, #allocationNode').addClass('hidden');
358+
} else {
359+
currentLocation = null;
360+
$('#getLocation').trigger('change', function (e) {
361+
alert('triggered');
362+
});
363+
}
364+
});
365+
344366
$('#getService').on('change', function (event) {
345367
346368
if ($('#getService').val() === '' || $('#getService').val() === currentService) {

0 commit comments

Comments
 (0)