Merge pull request #401 from Pterodactyl/feature/option-scripts

Feature/option scripts
This commit is contained in:
Dane Everitt 2017-04-21 20:59:55 -04:00 committed by GitHub
commit 4928620aab
19 changed files with 749 additions and 97 deletions

View file

@ -3,6 +3,15 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines.
## v0.6.0-beta.2.1 (Courageous Carniadactylus)
### Fixed
* `[beta.2.1]` — Fixed a bug preventing the deletion of a server.
### Added
* Added new scripts for service options that allows installation of software in a privileged Docker container on the node prior to marking a server as installed.
* Added ability to reinstall a server using the currently assigned service and option.
* Added ability to change a server's service and service option, as well as change pack assignments and other management services in that regard.
## v0.6.0-beta.2.1 (Courageous Carniadactylus)
### Fixed
* `[beta.2]` — Suspended servers now show as suspended.

View file

@ -95,7 +95,7 @@ class OptionController extends Controller
$repo = new VariableRepository;
try {
$variable = $repo->create($id, $request->only([
$variable = $repo->create($id, $request->intersect([
'name', 'description', 'env_variable',
'default_value', 'options', 'rules',
]));
@ -137,6 +137,18 @@ class OptionController extends Controller
return view('admin.services.options.variables', ['option' => ServiceOption::with('variables')->findOrFail($id)]);
}
/**
* Display script management page for an option.
*
* @param Request $request
* @param int $id
* @return \Illuminate\View\View
*/
public function viewScripts(Request $request, $id)
{
return view('admin.services.options.scripts', ['option' => ServiceOption::findOrFail($id)]);
}
/**
* Handles POST when editing a configration for a service option.
*
@ -188,7 +200,7 @@ class OptionController extends Controller
try {
if ($request->input('action') !== 'delete') {
$variable = $repo->update($variable, $request->only([
$variable = $repo->update($variable, $request->intersect([
'name', 'description', 'env_variable',
'default_value', 'options', 'rules',
]));
@ -208,4 +220,30 @@ class OptionController extends Controller
return redirect()->route('admin.services.option.variables', $option);
}
/**
* Handles POST when updating scripts for a service option.
*
* @param Request $request
* @param int $id
* @return \Illuminate\Response\RedirectResponse
*/
public function updateScripts(Request $request, $id)
{
$repo = new OptionRepository;
try {
$repo->scripts($id, $request->only([
'script_install', 'script_entry', 'script_container',
]));
Alert::success('Successfully updated option scripts to be run when servers are installed.')->flash();
} catch (DisplayValidationException $ex) {
return redirect()->route('admin.services.option.scripts', $id)->withErrors(json_decode($ex->getMessage()));
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.')->flash();
}
return redirect()->route('admin.services.option.scripts', $id);
}
}

View file

@ -198,7 +198,24 @@ class ServersController extends Controller
return $item;
});
return view('admin.servers.view.startup', ['server' => $server]);
$services = Models\Service::with('options.packs', 'options.variables')->get();
Javascript::put([
'services' => $services->map(function ($item) {
return array_merge($item->toArray(), [
'options' => $item->options->keyBy('id')->toArray(),
]);
})->keyBy('id'),
'server_variables' => $server->variables->mapWithKeys(function ($item) {
return ['env_' . $item->variable_id => [
'value' => $item->variable_value,
]];
})->toArray(),
]);
return view('admin.servers.view.startup', [
'server' => $server,
'services' => $services,
]);
}
/**
@ -322,6 +339,30 @@ class ServersController extends Controller
return redirect()->route('admin.servers.view.manage', $id);
}
/**
* Reinstalls the server with the currently assigned pack and service.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\RedirectResponse
*/
public function reinstallServer(Request $request, $id)
{
$repo = new ServerRepository;
try {
$repo->reinstall($id);
Alert::success('Server successfully marked for reinstallation.')->flash();
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled exception occured while attemping to perform this reinstallation. This error has been logged.')->flash();
}
return redirect()->route('admin.servers.view.manage', $id);
}
/**
* Setup a server to have a container rebuild.
*
@ -455,9 +496,13 @@ class ServersController extends Controller
$repo = new ServerRepository;
try {
$repo->updateStartup($id, $request->except('_token'), true);
if ($repo->updateStartup($id, $request->except('_token'), true)) {
Alert::success('Service configuration successfully modfied for this server, reinstalling now.')->flash();
Alert::success('Startup variables were successfully modified and assigned for this server.')->flash();
return redirect()->route('admin.servers.view', $id);
} else {
Alert::success('Startup variables were successfully modified and assigned for this server.')->flash();
}
} catch (DisplayValidationException $ex) {
return redirect()->route('admin.servers.view.startup', $id)->withErrors(json_decode($ex->getMessage()));
} catch (DisplayException $ex) {

View file

@ -0,0 +1,59 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Controllers\Daemon;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\ServiceOption;
use Pterodactyl\Http\Controllers\Controller;
class OptionController extends Controller
{
public function details(Request $request, $server)
{
$server = Server::with('allocation', 'option', 'variables.variable')->where('uuid', $server)->firstOrFail();
$environment = $server->variables->map(function ($item) {
return sprintf('%s=%s', $item->variable->env_variable, $item->variable_value);
});
return response()->json([
'scripts' => [
'install' => (! $server->option->script_install) ? null : str_replace(["\r\n", "\n", "\r"], "\n", $server->option->script_install),
'privileged' => $server->option->script_is_privileged,
],
'config' => [
'container' => $server->option->script_container,
'entry' => $server->option->script_entry,
],
'env' => $environment->merge([
'STARTUP=' . $server->startup,
'SERVER_MEMORY=' . $server->memory,
'SERVER_IP=' . $server->allocation->ip,
'SERVER_PORT=' . $server->allocation->port,
])->toArray(),
]);
}
}

View file

@ -72,6 +72,7 @@ class Server extends Model
*/
protected $casts = [
'node_id' => 'integer',
'skip_scripts' => 'boolean',
'suspended' => 'integer',
'owner_id' => 'integer',
'memory' => 'integer',

View file

@ -42,14 +42,15 @@ class ServiceOption extends Model
*/
protected $guarded = ['id', 'created_at', 'updated_at'];
/**
* Cast values to correct type.
*
* @var array
*/
protected $casts = [
'service_id' => 'integer',
];
/**
* Cast values to correct type.
*
* @var array
*/
protected $casts = [
'service_id' => 'integer',
'script_is_privileged' => 'boolean',
];
/**
* Returns the display startup string for the option and will use the parent

View file

@ -154,4 +154,35 @@ class OptionRepository
return $option;
}
/**
* Updates a service option's scripts in the database.
*
* @param int $id
* @param array $data
* @return \Pterodactyl\Models\ServiceOption
*
* @throws \Pterodactyl\Exceptions\DisplayValidationException
*/
public function scripts($id, array $data)
{
$option = ServiceOption::findOrFail($id);
$data['script_install'] = empty($data['script_install']) ? null : $data['script_install'];
$validator = Validator::make($data, [
'script_install' => 'sometimes|nullable|string',
'script_is_privileged' => 'sometimes|required|boolean',
'script_entry' => 'sometimes|required|string',
'script_container' => 'sometimes|required|string',
]);
if ($validator->fails()) {
throw new DisplayValidationException(json_encode($validator->errors()));
}
$option->fill($data)->save();
return $option;
}
}

View file

@ -103,6 +103,7 @@ class ServerRepository
'startup' => 'string',
'auto_deploy' => 'sometimes|required|accepted',
'custom_id' => 'sometimes|required|numeric|unique:servers,id',
'skip_scripts' => 'sometimes|required|boolean',
]);
$validator->sometimes('node_id', 'required|numeric|min:1|exists:nodes,id', function ($input) {
@ -249,14 +250,15 @@ class ServerRepository
'node_id' => $node->id,
'name' => $data['name'],
'description' => $data['description'],
'suspended' => 0,
'skip_scripts' => isset($data['skip_scripts']),
'suspended' => false,
'owner_id' => $user->id,
'memory' => $data['memory'],
'swap' => $data['swap'],
'disk' => $data['disk'],
'io' => $data['io'],
'cpu' => $data['cpu'],
'oom_disabled' => (isset($data['oom_disabled'])) ? true : false,
'oom_disabled' => isset($data['oom_disabled']),
'allocation_id' => $allocation->id,
'service_id' => $data['service_id'],
'option_id' => $data['option_id'],
@ -326,6 +328,7 @@ class ServerRepository
'type' => $service->folder,
'option' => $option->tag,
'pack' => (isset($pack)) ? $pack->uuid : null,
'skip_scripts' => $server->skip_scripts,
],
'keys' => [
(string) $server->daemonSecret => $this->daemonPermissions,
@ -619,13 +622,93 @@ class ServerRepository
}
}
/**
* Update the service configuration for a server.
*
* @param int $id
* @param array $data
* @return void
*
* @throws \GuzzleHttp\Exception\RequestException
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\DisplayValidationException
*/
protected function changeService($id, array $data)
{
}
protected function processVariables(Models\Server $server, $data, $admin = false)
{
$server->load('option.variables');
if ($admin) {
$server->startup = $data['startup'];
$server->save();
}
if ($server->option->variables) {
foreach ($server->option->variables as &$variable) {
$set = isset($data['env_' . $variable->id]);
// If user is not an admin and are trying to edit a non-editable field
// or an invisible field just silently skip the variable.
if (! $admin && (! $variable->user_editable || ! $variable->user_viewable)) {
continue;
}
// Perform Field Validation
$validator = Validator::make([
'variable_value' => ($set) ? $data['env_' . $variable->id] : null,
], [
'variable_value' => $variable->rules,
]);
if ($validator->fails()) {
throw new DisplayValidationException(json_encode(
collect([
'notice' => ['There was a validation error with the `' . $variable->name . '` variable.'],
])->merge($validator->errors()->toArray())
));
}
$svar = Models\ServerVariable::firstOrNew([
'server_id' => $server->id,
'variable_id' => $variable->id,
]);
// Set the value; if one was not passed set it to the default value
if ($set) {
$svar->variable_value = $data['env_' . $variable->id];
// Not passed, check if this record exists if so keep value, otherwise set default
} else {
$svar->variable_value = ($svar->exists) ? $svar->variable_value : $variable->default_value;
}
$svar->save();
}
}
// Reload Variables
$server->load('variables');
return $server->option->variables->map(function ($item, $key) use ($server) {
$display = $server->variables->where('variable_id', $item->id)->pluck('variable_value')->first();
return [
'variable' => $item->env_variable,
'value' => (! is_null($display)) ? $display : $item->default_value,
];
});
}
/**
* Update the startup details for a server.
*
* @param int $id
* @param array $data
* @param bool $admin
* @return void
* @return bool
*
* @throws \GuzzleHttp\Exception\RequestException
* @throws \Pterodactyl\Exceptions\DisplayException
@ -634,78 +717,110 @@ class ServerRepository
public function updateStartup($id, array $data, $admin = false)
{
$server = Models\Server::with('variables', 'option.variables')->findOrFail($id);
$hasServiceChanges = false;
DB::transaction(function () use ($admin, $data, $server) {
if (isset($data['startup']) && $admin) {
$server->startup = $data['startup'];
$server->save();
if ($admin) {
// User is an admin, lots of things to do here.
$validator = Validator::make($data, [
'startup' => 'required|string',
'skip_scripts' => 'sometimes|required|boolean',
'service_id' => 'required|numeric|min:1|exists:services,id',
'option_id' => 'required|numeric|min:1|exists:service_options,id',
'pack_id' => 'sometimes|nullable|numeric|min:0',
]);
if ((int) $data['pack_id'] < 1) {
$data['pack_id'] = null;
}
if ($server->option->variables) {
foreach ($server->option->variables as &$variable) {
$set = isset($data['env_' . $variable->id]);
// If user is not an admin and are trying to edit a non-editable field
// or an invisible field just silently skip the variable.
if (! $admin && (! $variable->user_editable || ! $variable->user_viewable)) {
continue;
}
// Perform Field Validation
$validator = Validator::make([
'variable_value' => ($set) ? $data['env_' . $variable->id] : null,
], [
'variable_value' => $variable->rules,
]);
if ($validator->fails()) {
throw new DisplayValidationException(json_encode(
collect([
'notice' => ['There was a validation error with the `' . $variable->name . '` variable.'],
])->merge($validator->errors()->toArray())
));
}
$svar = Models\ServerVariable::firstOrNew([
'server_id' => $server->id,
'variable_id' => $variable->id,
]);
// Set the value; if one was not passed set it to the default value
if ($set) {
$svar->variable_value = $data['env_' . $variable->id];
// Not passed, check if this record exists if so keep value, otherwise set default
} else {
$svar->variable_value = ($svar->exists) ? $svar->variable_value : $variable->default_value;
}
$svar->save();
}
if ($validator->fails()) {
throw new DisplayValidationException(json_encode($validator->errors()));
}
// Reload Variables
$server->load('variables');
$environment = $server->option->variables->map(function ($item, $key) use ($server) {
$display = $server->variables->where('variable_id', $item->id)->pluck('variable_value')->first();
if (
$server->service_id != $data['service_id'] ||
$server->option_id != $data['option_id'] ||
$server->pack_id != $data['pack_id']
) {
$hasServiceChanges = true;
}
}
return [
'variable' => $item->env_variable,
'value' => (! is_null($display)) ? $display : $item->default_value,
];
// If user isn't an administrator, this function is being access from the front-end
// Just try to update specific variables.
if (! $admin || ! $hasServiceChanges) {
return DB::transaction(function () use ($admin, $data, $server) {
$environment = $this->processVariables($server, $data, $admin);
$server->node->guzzleClient([
'X-Access-Server' => $server->uuid,
'X-Access-Token' => $server->node->daemonSecret,
])->request('PATCH', '/server', [
'json' => [
'build' => [
'env|overwrite' => $environment->pluck('value', 'variable')->merge(['STARTUP' => $server->startup])->toArray(),
],
],
]);
return false;
});
}
// Validate those Service Option Variables
// We know the service and option exists because of the validation.
// We need to verify that the option exists for the service, and then check for
// any required variable fields. (fields are labeled env_<env_variable>)
$option = Models\ServiceOption::where('id', $data['option_id'])->where('service_id', $data['service_id'])->first();
if (! $option) {
throw new DisplayException('The requested service option does not exist for the specified service.');
}
// Validate the Pack
if (! isset($data['pack_id']) || (int) $data['pack_id'] < 1) {
$data['pack_id'] = null;
} else {
$pack = Models\Pack::where('id', $data['pack_id'])->where('option_id', $data['option_id'])->first();
if (! $pack) {
throw new DisplayException('The requested service pack does not seem to exist for this combination.');
}
}
return DB::transaction(function () use ($admin, $data, $server) {
$server->installed = 0;
$server->service_id = $data['service_id'];
$server->option_id = $data['option_id'];
$server->pack_id = $data['pack_id'];
$server->skip_scripts = isset($data['skip_scripts']);
$server->save();
$server->variables->each->delete();
$server->load('service', 'pack');
// Send New Environment
$environment = $this->processVariables($server, $data, $admin);
$server->node->guzzleClient([
'X-Access-Server' => $server->uuid,
'X-Access-Token' => $server->node->daemonSecret,
])->request('PATCH', '/server', [
])->request('POST', '/server/reinstall', [
'json' => [
'build' => [
'env|overwrite' => $environment->pluck('value', 'variable')->merge(['STARTUP' => $server->startup]),
'env|overwrite' => $environment->pluck('value', 'variable')->merge(['STARTUP' => $server->startup])->toArray(),
],
'service' => [
'type' => $server->option->service->folder,
'option' => $server->option->tag,
'pack' => (! is_null($server->pack_id)) ? $server->pack->uuid : null,
'skip_scripts' => $server->skip_scripts,
],
],
]);
return true;
});
}
/**
@ -759,9 +874,7 @@ class ServerRepository
$server->load('subusers.permissions');
$server->subusers->each(function ($subuser) {
$subuser->permissions->each(function ($permission) {
$perm->delete();
});
$subuser->permissions->each->delete();
$subuser->delete();
});
@ -772,7 +885,7 @@ class ServerRepository
// This is the one un-recoverable point where
// transactions will not save us.
$repository = new DatabaseRepository;
$server->databases->each(function ($item) {
$server->databases->each(function ($item) use ($repository) {
$repository->drop($item->id);
});
@ -862,4 +975,25 @@ class ServerRepository
]);
});
}
/**
* Marks a server for reinstallation on the node.
*
* @param int $id
* @return void
*/
public function reinstall($id)
{
$server = Models\Server::with('node')->findOrFail($id);
DB::transaction(function () use ($server) {
$server->installed = 0;
$server->save();
$server->node->guzzleClient([
'X-Access-Token' => $server->node->daemonSecret,
'X-Access-Server' => $server->uuid,
])->request('POST', '/server/reinstall');
});
}
}

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddScriptsToServiceOptions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('service_options', function (Blueprint $table) {
$table->text('script_install')->after('startup')->nullable();
$table->boolean('script_is_privileged')->default(true)->after('startup');
$table->text('script_entry')->default('ash')->after('startup');
$table->text('script_container')->default('alpine:3.4')->after('startup');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('service_options', function (Blueprint $table) {
$table->dropColumn('script_install');
$table->dropColumn('script_is_privileged');
$table->dropColumn('script_entry');
$table->dropColumn('script_container');
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddServiceScriptTrackingToServers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('servers', function (Blueprint $table) {
$table->boolean('skip_scripts')->default(false)->after('description');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('skip_scripts');
});
}
}

File diff suppressed because one or more lines are too long

View file

@ -212,6 +212,13 @@
<select name="pack_id" id="pPackId" class="form-control"></select>
<p class="small text-muted no-margin">Select a service pack to be automatically installed on this server when first created.</p>
</div>
<div class="form-group col-xs-12">
<div class="checkbox checkbox-primary no-margin-bottom">
<input id="pSkipScripting" name="skip_scripting" type="checkbox" value="1" />
<label for="pSkipScripting" class="strong">Skip Service Option Install Script</label>
</div>
<p class="small text-muted no-margin">If the selected <code>Option</code> has an install script attached to it, the script will run during install after the pack is installed. If you would like to skip this step, check this box.</p>
</div>
</div>
</div>
</div>

View file

@ -53,6 +53,22 @@
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Reinstall Server</h3>
</div>
<div class="box-body">
<p>This will reinstall the server with the assigned pack and service scripts. <strong>Danger!</strong> This could overwrite server data.</p>
</div>
<div class="box-footer">
<form action="{{ route('admin.servers.view.manage.reinstall', $server->id) }}" method="POST">
{!! csrf_field() !!}
<button type="submit" class="btn btn-danger">Reinstall Server</button>
</form>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="box box-primary">
<div class="box-header with-border">

View file

@ -64,48 +64,179 @@
<input id="pStartup" name="startup" class="form-control" type="text" value="{{ old('startup', $server->startup) }}" />
<p class="small text-muted">Edit your server's startup command here. The following variables are available by default: <code>@{{SERVER_MEMORY}}</code>, <code>@{{SERVER_IP}}</code>, and <code>@{{SERVER_PORT}}</code>.</p>
</div>
<div class="box-body">
<label for="pDefaultStartupCommand" class="form-label">Default Service Start Command</label>
<input id="pDefaultStartupCommand" class="form-control" type="text" readonly />
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" class="btn btn-primary btn-sm pull-right">Save Modifications</button>
</div>
</div>
</div>
@foreach($server->option->variables as $variable)
<div class="col-xs-12 col-md-4 col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ $variable->name }}</h3>
</div>
<div class="box-body">
<input data-action="match-regex" data-regex="{{ $variable->regex }}" name="env_{{ $variable->id }}" class="form-control" type="text" value="{{ old('env_' . $variable->id, $variable->server_value) }}" />
<p class="no-margin small text-muted">{{ $variable->description }}</p>
<p class="no-margin">
@if($variable->required)<span class="label label-danger">Required</span>@else<span class="label label-default">Optional</span>@endif
@if($variable->user_viewable)<span class="label label-success">Visible</span>@else<span class="label label-primary">Hidden</span>@endif
@if($variable->user_editable)<span class="label label-success">Editable</span>@else<span class="label label-primary">Locked</span>@endif
</div>
<div class="row">
<div class="col-md-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Service Configuration</h3>
</div>
<div class="box-body row">
<div class="col-xs-12">
<p class="small text-danger">
Changing any of the below values will result in the server processing a re-install command. The server will be stopped and will then proceede.
If you are changing the pack, exisiting data <em>may</em> be overwritten. If you would like the service scripts to not run, ensure the box is checked at the bottom.
</p>
<p class="small text-danger">
<strong>This is a destructive operation in many cases. This server will be stopped immediately in order for this action to proceede.</strong>
</p>
</div>
<div class="box-footer">
<p class="no-margin text-muted small"><strong>Startup Command Variable:</strong> <code>{{ $variable->env_variable }}</code></p>
<p class="no-margin text-muted small"><strong>Input Rules:</strong> <code>{{ $variable->rules }}</code></p>
<div class="form-group col-xs-12">
<label for="pServiceId">Service</label>
<select name="service_id" id="pServiceId" class="form-control">
@foreach($services as $service)
<option value="{{ $service->id }}"
@if($service->id === $server->service_id)
selected="selected"
@endif
>{{ $service->name }}</option>
@endforeach
</select>
<p class="small text-muted no-margin">Select the type of service that this server will be running.</p>
</div>
<div class="form-group col-xs-12">
<label for="pOptionId">Option</label>
<select name="option_id" id="pOptionId" class="form-control"></select>
<p class="small text-muted no-margin">Select the type of sub-service that this server will be running.</p>
</div>
<div class="form-group col-xs-12">
<label for="pPackId">Service Pack</label>
<select name="pack_id" id="pPackId" class="form-control"></select>
<p class="small text-muted no-margin">Select a service pack to be automatically installed on this server when first created.</p>
</div>
<div class="form-group col-xs-12">
<div class="checkbox checkbox-primary no-margin-bottom">
<input id="pSkipScripting" name="skip_scripting" type="checkbox" value="1" @if($server->skip_scripts) checked @endif />
<label for="pSkipScripting" class="strong">Skip Service Option Install Script</label>
</div>
<p class="small text-muted no-margin">If the selected <code>Option</code> has an install script attached to it, the script will run during install after the pack is installed. If you would like to skip this step, check this box.</p>
</div>
</div>
</div>
@endforeach
</div>
<div class="col-md-6">
<div class="row" id="appendVariablesTo">
@foreach($server->option->variables as $variable)
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ $variable->name }}</h3>
</div>
<div class="box-body">
<input data-action="match-regex" name="env_{{ $variable->id }}" class="form-control" type="text" value="{{ old('env_' . $variable->id, $variable->server_value) }}" />
<p class="no-margin small text-muted">{{ $variable->description }}</p>
<p class="no-margin">
@if($variable->required)<span class="label label-danger">Required</span>@else<span class="label label-default">Optional</span>@endif
@if($variable->user_viewable)<span class="label label-success">Visible</span>@else<span class="label label-primary">Hidden</span>@endif
@if($variable->user_editable)<span class="label label-success">Editable</span>@else<span class="label label-primary">Locked</span>@endif
</p>
</div>
<div class="box-footer">
<p class="no-margin text-muted small"><strong>Startup Command Variable:</strong> <code>{{ $variable->env_variable }}</code></p>
<p class="no-margin text-muted small"><strong>Input Rules:</strong> <code>{{ $variable->rules }}</code></p>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</form>
@endsection
@section('footer-scripts')
@parent
{!! Theme::js('vendor/lodash/lodash.js') !!}
<script>
$('input[data-action="match-regex"]').on('keyup', function (event) {
if (! $(this).data('regex')) return;
$(document).ready(function () {
$('#pServiceId').select2({
placeholder: 'Select a Service',
}).change();
$('#pOptionId').select2({
placeholder: 'Select a Service Option',
});
$('#pPackId').select2({
placeholder: 'Select a Service Pack',
});
var input = $(this).val();
var regex = new RegExp($(this).data('regex').replace(/^\/|\/$/g, ''));
$('input[data-action="match-regex"]').on('keyup', function (event) {
if (! $(this).data('regex')) return;
$(this).parent().parent().removeClass('has-success has-error').addClass((! regex.test(input)) ? 'has-error' : 'has-success');
var input = $(this).val();
var regex = new RegExp($(this).data('regex').replace(/^\/|\/$/g, ''));
$(this).parent().parent().removeClass('has-success has-error').addClass((! regex.test(input)) ? 'has-error' : 'has-success');
});
});
</script>
<script>
$('#pServiceId').on('change', function (event) {
$('#pOptionId').html('').select2({
data: $.map(_.get(Pterodactyl.services, $(this).val() + '.options', []), function (item) {
console.log(item);
return {
id: item.id,
text: item.name,
};
}),
}).val('{{ $server->option_id }}').change();
});
$('#pOptionId').on('change', function (event) {
var parentChain = _.get(Pterodactyl.services, $('#pServiceId').val(), null);
var objectChain = _.get(parentChain, 'options.' + $(this).val(), null);
$('#pDefaultContainer').val(_.get(objectChain, 'docker_image', 'not defined!'));
if (!_.get(objectChain, 'startup', false)) {
$('#pDefaultStartupCommand').val(_.get(parentChain, 'startup', 'ERROR: Startup Not Defined!'));
} else {
$('#pDefaultStartupCommand').val(_.get(objectChain, 'startup'));
}
$('#pPackId').html('').select2({
data: [{ id: 0, text: 'No Service Pack' }].concat(
$.map(_.get(objectChain, 'packs', []), function (item, i) {
return {
id: item.id,
text: item.name + ' (' + item.version + ')',
};
})
),
}).val('{{ is_null($server->pack_id) ? 0 : $server->pack_id }}');
$('#appendVariablesTo').html('');
$.each(_.get(objectChain, 'variables', []), function (i, item) {
var setValue = _.get(Pterodactyl.server_variables, 'env_' + item.id + '.value', item.default_value);
var isRequired = (item.required === 1) ? '<span class="label label-danger">Required</span> ' : '';
var dataAppend = ' \
<div class="col-xs-12"> \
<div class="box"> \
<div class="box-header with-border"> \
<h3 class="box-title">' + isRequired + item.name + '</h3> \
</div> \
<div class="box-body"> \
<input data-action="match-regex" name="env_' + item.id + '" class="form-control" type="text" value="' + setValue + '" /> \
<p class="no-margin small text-muted">' + item.description + '</p> \
</div> \
<div class="box-footer"> \
<p class="no-margin text-muted small"><strong>Startup Command Variable:</strong> <code>' + item.env_variable + '</code></p> \
<p class="no-margin text-muted small"><strong>Input Rules:</strong> <code>' + item.rules + '</code></p> \
</div> \
</div> \
</div>';
$('#appendVariablesTo').append(dataAppend);
});
});
</script>
@endsection

View file

@ -0,0 +1,103 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
{{-- of this software and associated documentation files (the "Software"), to deal --}}
{{-- in the Software without restriction, including without limitation the rights --}}
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
{{-- copies of the Software, and to permit persons to whom the Software is --}}
{{-- furnished to do so, subject to the following conditions: --}}
{{-- The above copyright notice and this permission notice shall be included in all --}}
{{-- copies or substantial portions of the Software. --}}
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}}
{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}}
{{-- SOFTWARE. --}}
@extends('layouts.admin')
@section('title')
Services &rarr; Option: {{ $option->name }} &rarr; Scripts
@endsection
@section('content-header')
<h1>{{ $option->name }}<small>Manage install and upgrade scripts for this service option.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.services') }}">Services</a></li>
<li><a href="{{ route('admin.services.view', $option->service->id) }}">{{ $option->service->name }}</a></li>
<li class="active">{{ $option->name }}</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li><a href="{{ route('admin.services.option.view', $option->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.services.option.variables', $option->id) }}">Variables</a></li>
<li class="active"><a href="{{ route('admin.services.option.scripts', $option->id) }}">Scripts</a></li>
</ul>
</div>
</div>
</div>
<form action="{{ route('admin.services.option.scripts', $option->id) }}" method="POST">
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Install Script</h3>
</div>
<div class="box-body no-padding">
<div id="editor_install"style="height:300px">{{ $option->script_install }}</div>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-sm-6">
<label class="control-label">Script Container</label>
<input type="text" name="script_container" class="form-control" value="{{ $option->script_container }}" />
<p class="text-muted small">Docker container to use when running this script for the server.</p>
</div>
<div class="form-group col-sm-6">
<label class="control-label">Script Entrypoint Command</label>
<input type="text" name="script_entry" class="form-control" value="{{ $option->script_entry }}" />
<p class="text-muted small">The entrypoint command to use for this script.</p>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<textarea name="script_install" class="hidden"></textarea>
<button type="submit" class="btn btn-primary btn-sm pull-right">Save Scripts</button>
</div>
</div>
</div>
</div>
</form>
@endsection
@section('footer-scripts')
@parent
{!! Theme::js('vendor/ace/ace.js') !!}
{!! Theme::js('vendor/ace/ext-modelist.js') !!}
<script>
$(document).ready(function () {
const InstallEditor = ace.edit('editor_install');
const Modelist = ace.require('ace/ext/modelist')
InstallEditor.setTheme('ace/theme/chrome');
InstallEditor.getSession().setMode('ace/mode/sh');
InstallEditor.getSession().setUseWrapMode(true);
InstallEditor.setShowPrintMargin(false);
$('form').on('submit', function (e) {
$('textarea[name="script_install"]').val(InstallEditor.getValue());
});
});
</script>
@endsection

View file

@ -42,6 +42,7 @@
<li><a href="{{ route('admin.services.option.view', $option->id) }}">Configuration</a></li>
<li class="active"><a href="{{ route('admin.services.option.variables', $option->id) }}">Variables</a></li>
<li class="tab-success"><a href="#modal" data-toggle="modal" data-target="#newVariableModal">New Variable</a></li>
<li><a href="{{ route('admin.services.option.scripts', $option->id) }}">Scripts</a></li>
</ul>
</div>
</div>

View file

@ -40,6 +40,7 @@
<ul class="nav nav-tabs">
<li class="active"><a href="{{ route('admin.services.option.view', $option->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.services.option.variables', $option->id) }}">Variables</a></li>
<li><a href="{{ route('admin.services.option.scripts', $option->id) }}">Scripts</a></li>
</ul>
</div>
</div>

View file

@ -118,6 +118,7 @@ Route::group(['prefix' => 'servers'], function () {
Route::post('/view/{id}/manage/toggle', 'ServersController@toggleInstall')->name('admin.servers.view.manage.toggle');
Route::post('/view/{id}/manage/rebuild', 'ServersController@rebuildContainer')->name('admin.servers.view.manage.rebuild');
Route::post('/view/{id}/manage/suspension', 'ServersController@manageSuspension')->name('admin.servers.view.manage.suspension');
Route::post('/view/{id}/manage/reinstall', 'ServersController@reinstallServer')->name('admin.servers.view.manage.reinstall');
Route::post('/view/{id}/delete', 'ServersController@delete');
Route::patch('/view/{id}/database', 'ServersController@resetDatabasePassword');
@ -169,11 +170,13 @@ Route::group(['prefix' => 'services'], function () {
Route::get('/option/new', 'OptionController@create')->name('admin.services.option.new');
Route::get('/option/{id}', 'OptionController@viewConfiguration')->name('admin.services.option.view');
Route::get('/option/{id}/variables', 'OptionController@viewVariables')->name('admin.services.option.variables');
Route::get('/option/{id}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts');
Route::post('/new', 'ServiceController@store');
Route::post('/view/{id}', 'ServiceController@edit');
Route::post('/option/new', 'OptionController@store');
Route::post('/option/{id}', 'OptionController@editConfiguration');
Route::post('/option/{id}/scripts', 'OptionController@updateScripts');
Route::post('/option/{id}/variables', 'OptionController@createVariable');
Route::post('/option/{id}/variables/{variable}', 'OptionController@editVariable')->name('admin.services.option.variables.edit');

View file

@ -25,3 +25,4 @@ Route::get('/services', 'ServiceController@list')->name('daemon.services');
Route::get('/services/pull/{service}/{file}', 'ServiceController@pull')->name('daemon.pull');
Route::get('/packs/pull/{uuid}', 'PackController@pull')->name('daemon.pack.pull');
Route::get('/packs/pull/{uuid}/hash', 'PackController@hash')->name('daemon.pack.hash');
Route::get('/details/option/{server}', 'OptionController@details')->name('daemon.option.details');