diff --git a/CHANGELOG.md b/CHANGELOG.md index 960d838af..57096d1ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index d4f383a92..f7c5ac6db 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -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); + } } diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 1a8a8cfbc..64b30e163 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -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) { diff --git a/app/Http/Controllers/Daemon/OptionController.php b/app/Http/Controllers/Daemon/OptionController.php new file mode 100644 index 000000000..9a27c1c64 --- /dev/null +++ b/app/Http/Controllers/Daemon/OptionController.php @@ -0,0 +1,59 @@ +. + * + * 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(), + ]); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 729ae4f0d..8efad3845 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -72,6 +72,7 @@ class Server extends Model */ protected $casts = [ 'node_id' => 'integer', + 'skip_scripts' => 'boolean', 'suspended' => 'integer', 'owner_id' => 'integer', 'memory' => 'integer', diff --git a/app/Models/ServiceOption.php b/app/Models/ServiceOption.php index 9e29fe8a6..bbd18e675 100644 --- a/app/Models/ServiceOption.php +++ b/app/Models/ServiceOption.php @@ -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 diff --git a/app/Repositories/OptionRepository.php b/app/Repositories/OptionRepository.php index 518858928..53dab8c57 100644 --- a/app/Repositories/OptionRepository.php +++ b/app/Repositories/OptionRepository.php @@ -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; + } } diff --git a/app/Repositories/ServerRepository.php b/app/Repositories/ServerRepository.php index 674afa670..6aac53a0c 100644 --- a/app/Repositories/ServerRepository.php +++ b/app/Repositories/ServerRepository.php @@ -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_) + $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'); + }); + } } diff --git a/database/migrations/2017_04_20_171943_AddScriptsToServiceOptions.php b/database/migrations/2017_04_20_171943_AddScriptsToServiceOptions.php new file mode 100644 index 000000000..fb6d2da81 --- /dev/null +++ b/database/migrations/2017_04_20_171943_AddScriptsToServiceOptions.php @@ -0,0 +1,38 @@ +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'); + }); + } +} diff --git a/database/migrations/2017_04_21_151432_AddServiceScriptTrackingToServers.php b/database/migrations/2017_04_21_151432_AddServiceScriptTrackingToServers.php new file mode 100644 index 000000000..07afdfeea --- /dev/null +++ b/database/migrations/2017_04_21_151432_AddServiceScriptTrackingToServers.php @@ -0,0 +1,32 @@ +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'); + }); + } +} diff --git a/public/themes/pterodactyl/vendor/ace/mode-sh.js b/public/themes/pterodactyl/vendor/ace/mode-sh.js new file mode 100644 index 000000000..95e556e13 --- /dev/null +++ b/public/themes/pterodactyl/vendor/ace/mode-sh.js @@ -0,0 +1 @@ +define("ace/mode/sh_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=t.reservedKeywords="!|{|}|case|do|done|elif|else|esac|fi|for|if|in|then|until|while|&|;|export|local|read|typeset|unset|elif|select|set|function|declare|readonly",o=t.languageConstructs="[|]|alias|bg|bind|break|builtin|cd|command|compgen|complete|continue|dirs|disown|echo|enable|eval|exec|exit|fc|fg|getopts|hash|help|history|jobs|kill|let|logout|popd|printf|pushd|pwd|return|set|shift|shopt|source|suspend|test|times|trap|type|ulimit|umask|unalias|wait",u=function(){var e=this.createKeywordMapper({keyword:s,"support.function.builtin":o,"invalid.deprecated":"debugger"},"identifier"),t="(?:(?:[1-9]\\d*)|(?:0))",n="(?:\\.\\d+)",r="(?:\\d+)",i="(?:(?:"+r+"?"+n+")|(?:"+r+"\\.))",u="(?:(?:"+i+"|"+r+")"+")",a="(?:"+u+"|"+i+")",f="(?:&"+r+")",l="[a-zA-Z_][a-zA-Z0-9_]*",c="(?:"+l+"=)",h="(?:\\$(?:SHLVL|\\$|\\!|\\?))",p="(?:"+l+"\\s*\\(\\))";this.$rules={start:[{token:"constant",regex:/\\./},{token:["text","comment"],regex:/(^|\s)(#.*)$/},{token:"string.start",regex:'"',push:[{token:"constant.language.escape",regex:/\\(?:[$`"\\]|$)/},{include:"variables"},{token:"keyword.operator",regex:/`/},{token:"string.end",regex:'"',next:"pop"},{defaultToken:"string"}]},{token:"string",regex:"\\$'",push:[{token:"constant.language.escape",regex:/\\(?:[abeEfnrtv\\'"]|x[a-fA-F\d]{1,2}|u[a-fA-F\d]{4}([a-fA-F\d]{4})?|c.|\d{1,3})/},{token:"string",regex:"'",next:"pop"},{defaultToken:"string"}]},{regex:"<<<",token:"keyword.operator"},{stateName:"heredoc",regex:"(<<-?)(\\s*)(['\"`]?)([\\w\\-]+)(['\"`]?)",onMatch:function(e,t,n){var r=e[2]=="-"?"indentedHeredoc":"heredoc",i=e.split(this.splitRegex);return n.push(r,i[4]),[{type:"constant",value:i[1]},{type:"text",value:i[2]},{type:"string",value:i[3]},{type:"support.class",value:i[4]},{type:"string",value:i[5]}]},rules:{heredoc:[{onMatch:function(e,t,n){return e===n[1]?(n.shift(),n.shift(),this.next=n[0]||"start","support.class"):(this.next="","string")},regex:".*$",next:"start"}],indentedHeredoc:[{token:"string",regex:"^ +"},{onMatch:function(e,t,n){return e===n[1]?(n.shift(),n.shift(),this.next=n[0]||"start","support.class"):(this.next="","string")},regex:".*$",next:"start"}]}},{regex:"$",token:"empty",next:function(e,t){return t[0]==="heredoc"||t[0]==="indentedHeredoc"?t[0]:e}},{token:["keyword","text","text","text","variable"],regex:/(declare|local|readonly)(\s+)(?:(-[fixar]+)(\s+))?([a-zA-Z_][a-zA-Z0-9_]*\b)/},{token:"variable.language",regex:h},{token:"variable",regex:c},{include:"variables"},{token:"support.function",regex:p},{token:"support.function",regex:f},{token:"string",start:"'",end:"'"},{token:"constant.numeric",regex:a},{token:"constant.numeric",regex:t+"\\b"},{token:e,regex:"[a-zA-Z_][a-zA-Z0-9_]*\\b"},{token:"keyword.operator",regex:"\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|~|<|>|<=|=>|=|!=|[%&|`]"},{token:"punctuation.operator",regex:";"},{token:"paren.lparen",regex:"[\\[\\(\\{]"},{token:"paren.rparen",regex:"[\\]]"},{token:"paren.rparen",regex:"[\\)\\}]",next:"pop"}],variables:[{token:"variable",regex:/(\$)(\w+)/},{token:["variable","paren.lparen"],regex:/(\$)(\()/,push:"start"},{token:["variable","paren.lparen","keyword.operator","variable","keyword.operator"],regex:/(\$)(\{)([#!]?)(\w+|[*@#?\-$!0_])(:[?+\-=]?|##?|%%?|,,?\/|\^\^?)?/,push:"start"},{token:"variable",regex:/\$[*@#?\-$!0_]/},{token:["variable","paren.lparen"],regex:/(\$)(\{)/,push:"start"}]},this.normalizeRules()};r.inherits(u,i),t.ShHighlightRules=u}),define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.singleLineBlockCommentRe=/^\s*(\/\*).*\*\/\s*$/,this.tripleStarBlockCommentRe=/^\s*(\/\*\*\*).*\*\/\s*$/,this.startRegionRe=/^\s*(\/\*|\/\/)#?region\b/,this._getFoldWidgetBase=this.getFoldWidget,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);if(this.singleLineBlockCommentRe.test(r)&&!this.startRegionRe.test(r)&&!this.tripleStarBlockCommentRe.test(r))return"";var i=this._getFoldWidgetBase(e,t,n);return!i&&this.startRegionRe.test(r)?"start":i},this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n);if(this.startRegionRe.test(i))return this.getCommentRegionBlock(e,i,n);var s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++tf)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)},this.getCommentRegionBlock=function(e,t,n){var r=t.search(/\s*$/),s=e.getLength(),o=n,u=/^\s*(?:\/\*|\/\/|--)#?(end)?region\b/,a=1;while(++no)return new i(o,r,l,t.length)}}.call(o.prototype)}),define("ace/mode/sh",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/sh_highlight_rules","ace/range","ace/mode/folding/cstyle","ace/mode/behaviour/cstyle"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./sh_highlight_rules").ShHighlightRules,o=e("../range").Range,u=e("./folding/cstyle").FoldMode,a=e("./behaviour/cstyle").CstyleBehaviour,f=function(){this.HighlightRules=s,this.foldingRules=new u,this.$behaviour=new a};r.inherits(f,i),function(){this.lineCommentStart="#",this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t),i=this.getTokenizer().getLineTokens(t,e),s=i.tokens;if(s.length&&s[s.length-1].type=="comment")return r;if(e=="start"){var o=t.match(/^.*[\{\(\[:]\s*$/);o&&(r+=n)}return r};var e={pass:1,"return":1,raise:1,"break":1,"continue":1};this.checkOutdent=function(t,n,r){if(r!=="\r\n"&&r!=="\r"&&r!=="\n")return!1;var i=this.getTokenizer().getLineTokens(n.trim(),t).tokens;if(!i)return!1;do var s=i.pop();while(s&&(s.type=="comment"||s.type=="text"&&s.value.match(/^\s+$/)));return s?s.type=="keyword"&&e[s.value]:!1},this.autoOutdent=function(e,t,n){n+=1;var r=this.$getIndent(t.getLine(n)),i=t.getTabString();r.slice(-i.length)==i&&t.remove(new o(n,r.length-i.length,n,r.length))},this.$id="ace/mode/sh"}.call(f.prototype),t.Mode=f}) \ No newline at end of file diff --git a/resources/themes/pterodactyl/admin/servers/new.blade.php b/resources/themes/pterodactyl/admin/servers/new.blade.php index e2071d37d..eaf20445f 100644 --- a/resources/themes/pterodactyl/admin/servers/new.blade.php +++ b/resources/themes/pterodactyl/admin/servers/new.blade.php @@ -212,6 +212,13 @@

Select a service pack to be automatically installed on this server when first created.

+
+
+ + +
+

If the selected Option 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.

+
diff --git a/resources/themes/pterodactyl/admin/servers/view/manage.blade.php b/resources/themes/pterodactyl/admin/servers/view/manage.blade.php index 00043f221..0566a8a1c 100644 --- a/resources/themes/pterodactyl/admin/servers/view/manage.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/manage.blade.php @@ -53,6 +53,22 @@
+
+
+
+

Reinstall Server

+
+
+

This will reinstall the server with the assigned pack and service scripts. Danger! This could overwrite server data.

+
+ +
+
diff --git a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php index 2c1964bec..c83b48cb0 100644 --- a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php @@ -64,48 +64,179 @@

Edit your server's startup command here. The following variables are available by default: @{{SERVER_MEMORY}}, @{{SERVER_IP}}, and @{{SERVER_PORT}}.

+
+ + +
- @foreach($server->option->variables as $variable) -
-
-
-

{{ $variable->name }}

-
-
- -

{{ $variable->description }}

-

- @if($variable->required)Required@elseOptional@endif - @if($variable->user_viewable)Visible@elseHidden@endif - @if($variable->user_editable)Editable@elseLocked@endif +

+
+
+
+
+

Service Configuration

+
+
+
+

+ 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 may be overwritten. If you would like the service scripts to not run, ensure the box is checked at the bottom. +

+

+ This is a destructive operation in many cases. This server will be stopped immediately in order for this action to proceede.

-
- @endforeach +
+
+
+ @foreach($server->option->variables as $variable) +
+
+
+

{{ $variable->name }}

+
+
+ +

{{ $variable->description }}

+

+ @if($variable->required)Required@elseOptional@endif + @if($variable->user_viewable)Visible@elseHidden@endif + @if($variable->user_editable)Editable@elseLocked@endif +

+
+ +
+
+ @endforeach +
+
@endsection @section('footer-scripts') @parent + {!! Theme::js('vendor/lodash/lodash.js') !!} + @endsection diff --git a/resources/themes/pterodactyl/admin/services/options/scripts.blade.php b/resources/themes/pterodactyl/admin/services/options/scripts.blade.php new file mode 100644 index 000000000..998247f70 --- /dev/null +++ b/resources/themes/pterodactyl/admin/services/options/scripts.blade.php @@ -0,0 +1,103 @@ +{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} + +{{-- 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 → Option: {{ $option->name }} → Scripts +@endsection + +@section('content-header') +

{{ $option->name }}Manage install and upgrade scripts for this service option.

+ +@endsection + +@section('content') +
+
+ +
+
+
+
+
+
+
+

Install Script

+
+
+
{{ $option->script_install }}
+
+
+
+
+ + +

Docker container to use when running this script for the server.

+
+
+ + +

The entrypoint command to use for this script.

+
+
+
+ +
+
+
+
+@endsection + +@section('footer-scripts') + @parent + {!! Theme::js('vendor/ace/ace.js') !!} + {!! Theme::js('vendor/ace/ext-modelist.js') !!} + + +@endsection diff --git a/resources/themes/pterodactyl/admin/services/options/variables.blade.php b/resources/themes/pterodactyl/admin/services/options/variables.blade.php index 830bec458..dcc8882ca 100644 --- a/resources/themes/pterodactyl/admin/services/options/variables.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/variables.blade.php @@ -42,6 +42,7 @@
  • Configuration
  • Variables
  • New Variable
  • +
  • Scripts
  • diff --git a/resources/themes/pterodactyl/admin/services/options/view.blade.php b/resources/themes/pterodactyl/admin/services/options/view.blade.php index 6450fdef3..b61f79f9d 100644 --- a/resources/themes/pterodactyl/admin/services/options/view.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/view.blade.php @@ -40,6 +40,7 @@
    diff --git a/routes/admin.php b/routes/admin.php index 0ebe173b0..7b6c459ee 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -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'); diff --git a/routes/daemon.php b/routes/daemon.php index 78edf308f..d5ebcee67 100644 --- a/routes/daemon.php +++ b/routes/daemon.php @@ -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');