mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-19 18:01:37 +00:00
headless console
This commit is contained in:
parent
f68d6c7ca2
commit
33594f2dba
@ -12,7 +12,7 @@
|
|||||||
* @method static init(Server $server, string $asUser = null)
|
* @method static init(Server $server, string $asUser = null)
|
||||||
* @method static setLog(string $logType, int $siteId = null)
|
* @method static setLog(string $logType, int $siteId = null)
|
||||||
* @method static connect()
|
* @method static connect()
|
||||||
* @method static string exec(string $command, string $log = '', int $siteId = null)
|
* @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false)
|
||||||
* @method static string assertExecuted(array|string $commands)
|
* @method static string assertExecuted(array|string $commands)
|
||||||
* @method static string assertExecutedContains(string $command)
|
* @method static string assertExecutedContains(string $command)
|
||||||
* @method static disconnect()
|
* @method static disconnect()
|
||||||
|
@ -96,7 +96,7 @@ public function connect(bool $sftp = false): void
|
|||||||
* @throws SSHCommandError
|
* @throws SSHCommandError
|
||||||
* @throws SSHConnectionError
|
* @throws SSHConnectionError
|
||||||
*/
|
*/
|
||||||
public function exec(string|array $commands, string $log = '', ?int $siteId = null): string
|
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
|
||||||
{
|
{
|
||||||
if ($log) {
|
if ($log) {
|
||||||
$this->setLog($log, $siteId);
|
$this->setLog($log, $siteId);
|
||||||
@ -112,18 +112,34 @@ public function exec(string|array $commands, string $log = '', ?int $siteId = nu
|
|||||||
throw new SSHConnectionError($e->getMessage());
|
throw new SSHConnectionError($e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! is_array($commands)) {
|
|
||||||
$commands = [$commands];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = '';
|
if ($this->asUser) {
|
||||||
foreach ($commands as $command) {
|
$command = 'sudo su - '.$this->asUser.' -c '.'"'.addslashes($command).'"';
|
||||||
$result .= $this->executeCommand($command);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
$this->connection->setTimeout(0);
|
||||||
|
if ($stream) {
|
||||||
|
$this->connection->exec($command, function ($output) {
|
||||||
|
$this->log?->write($output);
|
||||||
|
echo $output;
|
||||||
|
ob_flush();
|
||||||
|
flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
$output = $this->connection->exec($command);
|
||||||
|
|
||||||
|
$this->log?->write($output);
|
||||||
|
|
||||||
|
if (Str::contains($output, 'VITO_SSH_ERROR')) {
|
||||||
|
throw new Exception('SSH command failed with an error');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
|
throw $e;
|
||||||
throw new SSHCommandError($e->getMessage());
|
throw new SSHCommandError($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,28 +157,6 @@ public function upload(string $local, string $remote): void
|
|||||||
$this->connection->put($remote, $local, SFTP::SOURCE_LOCAL_FILE);
|
$this->connection->put($remote, $local, SFTP::SOURCE_LOCAL_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
protected function executeCommand(string $command): string
|
|
||||||
{
|
|
||||||
if ($this->asUser) {
|
|
||||||
$command = 'sudo su - '.$this->asUser.' -c '.'"'.addslashes($command).'"';
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->connection->setTimeout(0);
|
|
||||||
|
|
||||||
$output = $this->connection->exec($command);
|
|
||||||
|
|
||||||
$this->log?->write($output);
|
|
||||||
|
|
||||||
if (Str::contains($output, 'VITO_SSH_ERROR')) {
|
|
||||||
throw new Exception('SSH command failed with an error');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
|
43
app/Http/Controllers/ConsoleController.php
Normal file
43
app/Http/Controllers/ConsoleController.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ConsoleController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Server $server): View
|
||||||
|
{
|
||||||
|
return view('console.index', [
|
||||||
|
'server' => $server,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(Server $server, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'user' => [
|
||||||
|
'required',
|
||||||
|
Rule::in(['root', $server->ssh_user]),
|
||||||
|
],
|
||||||
|
'command' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->stream(
|
||||||
|
function () use ($server, $request) {
|
||||||
|
$ssh = $server->ssh($request->user);
|
||||||
|
$log = 'console-'.time();
|
||||||
|
$ssh->exec(command: $request->command, log: $log, stream: true);
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
[
|
||||||
|
'Cache-Control' => 'no-cache',
|
||||||
|
'X-Accel-Buffering' => 'no',
|
||||||
|
'Content-Type' => 'text/event-stream',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -7,13 +7,14 @@
|
|||||||
use App\Facades\Toast;
|
use App\Facades\Toast;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
class HandleSSHErrors
|
class HandleSSHErrors
|
||||||
{
|
{
|
||||||
public function handle(Request $request, Closure $next)
|
public function handle(Request $request, Closure $next)
|
||||||
{
|
{
|
||||||
$res = $next($request);
|
$res = $next($request);
|
||||||
if ($res->exception) {
|
if ($res instanceof Response && $res->exception) {
|
||||||
if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
|
if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
|
||||||
Toast::error($res->exception->getMessage());
|
Toast::error($res->exception->getMessage());
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ public function connect(bool $sftp = false): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exec(string|array $commands, string $log = '', ?int $siteId = null): string
|
public function exec(string|array $commands, string $log = '', ?int $siteId = null, ?bool $stream = false): string
|
||||||
{
|
{
|
||||||
if ($log) {
|
if ($log) {
|
||||||
$this->setLog($log, $siteId);
|
$this->setLog($log, $siteId);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div
|
<div
|
||||||
class="h-[500px] w-full overflow-auto whitespace-pre-line rounded-md border border-gray-200 bg-gray-900 p-5 text-gray-50 dark:border-gray-700"
|
{{ $attributes->merge(["class" => "h-[500px] w-full overflow-auto whitespace-pre-line rounded-md border border-gray-200 bg-black p-5 text-gray-50 dark:border-gray-700"]) }}
|
||||||
>
|
>
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
{{ $attributes }}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 406 B |
76
resources/views/console/index.blade.php
Normal file
76
resources/views/console/index.blade.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<x-server-layout :server="$server">
|
||||||
|
<x-slot name="pageTitle">{{ $server->name }} - Console</x-slot>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-data="{
|
||||||
|
user: '{{ $server->ssh_user }}',
|
||||||
|
command: '',
|
||||||
|
output: '',
|
||||||
|
runUrl: '{{ route("servers.console.run", ["server" => $server]) }}',
|
||||||
|
async run() {
|
||||||
|
this.output = 'Running...\n'
|
||||||
|
const fetchOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user: this.user,
|
||||||
|
command: this.command,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(this.runUrl, fetchOptions)
|
||||||
|
const reader = response.body.getReader()
|
||||||
|
const decoder = new TextDecoder('utf-8')
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
|
||||||
|
const textChunk = decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
|
this.output += textChunk
|
||||||
|
|
||||||
|
document.getElementById('console-output').scrollTop =
|
||||||
|
document.getElementById('console-output').scrollHeight
|
||||||
|
}
|
||||||
|
this.output += '\nDone!'
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<x-card-header>
|
||||||
|
<x-slot name="title">Headless Console</x-slot>
|
||||||
|
<x-slot name="description">
|
||||||
|
Here you can run ssh commands on your server and see the result right away.
|
||||||
|
<br />
|
||||||
|
<b>Note that</b>
|
||||||
|
this is a headless console, it doesn't keep the current path. it will always run from the home path of
|
||||||
|
the selected user.
|
||||||
|
</x-slot>
|
||||||
|
</x-card-header>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<x-console-view id="console-output">
|
||||||
|
<div class="w-full" x-text="output"></div>
|
||||||
|
</x-console-view>
|
||||||
|
<form onsubmit="return false" id="console-form" class="flex items-center justify-between">
|
||||||
|
<x-select-input x-model="user" id="user" name="user" class="flex-none" data-tooltip="User">
|
||||||
|
<option value="{{ $server->ssh_user }}">{{ $server->ssh_user }}</option>
|
||||||
|
<option value="root">root</option>
|
||||||
|
</x-select-input>
|
||||||
|
<x-text-input
|
||||||
|
id="command"
|
||||||
|
name="command"
|
||||||
|
x-model="command"
|
||||||
|
type="text"
|
||||||
|
placeholder="Type your command here..."
|
||||||
|
class="mx-1 flex-grow"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<x-primary-button id="btn-run" x-on:click="run" class="h-[40px]">Run</x-primary-button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-server-layout>
|
@ -116,6 +116,18 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g
|
|||||||
</x-sidebar-link>
|
</x-sidebar-link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<x-sidebar-link
|
||||||
|
:href="route('servers.console', ['server' => $server])"
|
||||||
|
:active="request()->routeIs('servers.console')"
|
||||||
|
>
|
||||||
|
<x-heroicon name="o-command-line" class="h-6 w-6" />
|
||||||
|
<span class="ml-2">
|
||||||
|
{{ __("Console") }}
|
||||||
|
</span>
|
||||||
|
</x-sidebar-link>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<x-sidebar-link
|
<x-sidebar-link
|
||||||
:href="route('servers.settings', ['server' => $server])"
|
:href="route('servers.settings', ['server' => $server])"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\ApplicationController;
|
use App\Http\Controllers\ApplicationController;
|
||||||
|
use App\Http\Controllers\ConsoleController;
|
||||||
use App\Http\Controllers\CronjobController;
|
use App\Http\Controllers\CronjobController;
|
||||||
use App\Http\Controllers\DatabaseBackupController;
|
use App\Http\Controllers\DatabaseBackupController;
|
||||||
use App\Http\Controllers\DatabaseController;
|
use App\Http\Controllers\DatabaseController;
|
||||||
@ -123,6 +124,10 @@
|
|||||||
Route::get('/{server}/services/{service}/restart', [ServiceController::class, 'restart'])->name('servers.services.restart');
|
Route::get('/{server}/services/{service}/restart', [ServiceController::class, 'restart'])->name('servers.services.restart');
|
||||||
Route::get('/{server}/services/{service}/enable', [ServiceController::class, 'enable'])->name('servers.services.enable');
|
Route::get('/{server}/services/{service}/enable', [ServiceController::class, 'enable'])->name('servers.services.enable');
|
||||||
Route::get('/{server}/services/{service}/disable', [ServiceController::class, 'disable'])->name('servers.services.disable');
|
Route::get('/{server}/services/{service}/disable', [ServiceController::class, 'disable'])->name('servers.services.disable');
|
||||||
|
|
||||||
|
// console
|
||||||
|
Route::get('/{server}/console', [ConsoleController::class, 'index'])->name('servers.console');
|
||||||
|
Route::post('/{server}/console', [ConsoleController::class, 'run'])->name('servers.console.run');
|
||||||
});
|
});
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
|
Loading…
x
Reference in New Issue
Block a user