Monitoring & Service Management (#163)

Monitoring & Service Management
This commit is contained in:
Saeed Vaziry 2024-04-13 11:47:56 +02:00 committed by GitHub
parent 87ec0af697
commit 052e28d2e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
95 changed files with 2423 additions and 341 deletions

View File

@ -0,0 +1,150 @@
<?php
namespace App\Actions\Monitoring;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class GetMetrics
{
public function filter(Server $server, array $input): array
{
if (isset($input['from']) && isset($input['to']) && $input['from'] === $input['to']) {
$input['from'] = Carbon::parse($input['from'])->format('Y-m-d').' 00:00:00';
$input['to'] = Carbon::parse($input['to'])->format('Y-m-d').' 23:59:59';
}
$defaultInput = [
'period' => '10m',
];
$input = array_merge($defaultInput, $input);
$this->validate($input);
return $this->metrics(
server: $server,
fromDate: $this->getFromDate($input),
toDate: $this->getToDate($input),
interval: $this->getInterval($input)
);
}
private function metrics(
Server $server,
Carbon $fromDate,
Carbon $toDate,
?Expression $interval = null
): array {
$metrics = DB::table('metrics')
->where('server_id', $server->id)
->whereBetween('created_at', [$fromDate->format('Y-m-d H:i:s'), $toDate->format('Y-m-d H:i:s')])
->select(
[
DB::raw('created_at as date'),
DB::raw('AVG(load) as load'),
DB::raw('AVG(memory_total) as memory_total'),
DB::raw('AVG(memory_used) as memory_used'),
DB::raw('AVG(memory_free) as memory_free'),
DB::raw('AVG(disk_total) as disk_total'),
DB::raw('AVG(disk_used) as disk_used'),
DB::raw('AVG(disk_free) as disk_free'),
$interval,
],
)
->groupByRaw('date_interval')
->orderBy('date_interval')
->get()
->map(function ($item) {
$item->date = Carbon::parse($item->date)->format('Y-m-d H:i');
return $item;
});
return [
'metrics' => $metrics,
];
}
private function getFromDate(array $input): Carbon
{
if ($input['period'] === 'custom') {
return new Carbon($input['from']);
}
return Carbon::parse('-'.convert_time_format($input['period']));
}
private function getToDate(array $input): Carbon
{
if ($input['period'] === 'custom') {
return new Carbon($input['to']);
}
return Carbon::now();
}
private function getInterval(array $input): Expression
{
if ($input['period'] === 'custom') {
$from = new Carbon($input['from']);
$to = new Carbon($input['to']);
$periodInHours = $from->diffInHours($to);
}
if (! isset($periodInHours)) {
$periodInHours = Carbon::parse(
convert_time_format($input['period'])
)->diffInHours();
}
if ($periodInHours <= 1) {
return DB::raw("strftime('%Y-%m-%d %H:%M:00', created_at) as date_interval");
}
if ($periodInHours <= 24) {
return DB::raw("strftime('%Y-%m-%d %H:00:00', created_at) as date_interval");
}
if ($periodInHours > 24) {
return DB::raw("strftime('%Y-%m-%d 00:00:00', created_at) as date_interval");
}
}
private function validate(array $input): void
{
Validator::make($input, [
'period' => [
'required',
Rule::in([
'10m',
'30m',
'1h',
'12h',
'1d',
'7d',
'custom',
]),
],
])->validate();
if ($input['period'] === 'custom') {
Validator::make($input, [
'from' => [
'required',
'date',
'before:to',
],
'to' => [
'required',
'date',
'after:from',
],
])->validate();
}
}
}

View File

@ -4,6 +4,7 @@
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\SSH\Services\PHP\PHP;
use Illuminate\Validation\ValidationException;
class ChangeDefaultCli
@ -12,7 +13,9 @@ public function change(Server $server, array $input): void
{
$this->validate($server, $input);
$service = $server->php($input['version']);
$service->handler()->setDefaultCli();
/** @var PHP $handler */
$handler = $service->handler();
$handler->setDefaultCli();
$server->defaultService('php')->update(['is_default' => 0]);
$service->update(['is_default' => 1]);
$service->update(['status' => ServiceStatus::READY]);

View File

@ -3,6 +3,7 @@
namespace App\Actions\PHP;
use App\Models\Server;
use App\SSH\Services\PHP\PHP;
use Illuminate\Validation\ValidationException;
class GetPHPIni
@ -14,7 +15,10 @@ public function getIni(Server $server, array $input): string
$php = $server->php($input['version']);
try {
return $php->handler()->getPHPIni();
/** @var PHP $handler */
$handler = $php->handler();
return $handler->getPHPIni();
} catch (\Throwable $e) {
throw ValidationException::withMessages(
['ini' => $e->getMessage()]

View File

@ -4,6 +4,7 @@
use App\Models\Server;
use App\Models\Service;
use App\SSH\Services\PHP\PHP;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
@ -23,7 +24,9 @@ public function install(Server $server, array $input): Service
$service->save();
dispatch(function () use ($service, $input) {
$service->handler()->installExtension($input['extension']);
/** @var PHP $handler */
$handler = $service->handler();
$handler->installExtension($input['extension']);
})->catch(function () use ($service, $input) {
$service->refresh();
$typeData = $service->type_data;

View File

@ -8,14 +8,15 @@
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class Create
class Install
{
public function create(Server $server, array $input): Service
public function install(Server $server, array $input): Service
{
$this->validate($server, $input);
$service = new Service([
'name' => $input['type'],
'server_id' => $server->id,
'name' => $input['name'],
'type' => $input['type'],
'version' => $input['version'],
'status' => ServiceStatus::INSTALLING,
@ -27,15 +28,13 @@ public function create(Server $server, array $input): Service
$service->save();
$service->handler()->create();
dispatch(function () use ($service) {
$service->handler()->install();
$service->status = ServiceStatus::READY;
$service->save();
})->catch(function () use ($service) {
$service->handler()->delete();
$service->delete();
$service->status = ServiceStatus::INSTALLATION_FAILED;
$service->save();
})->onConnection('ssh');
return $service;
@ -46,8 +45,11 @@ private function validate(Server $server, array $input): void
Validator::make($input, [
'type' => [
'required',
Rule::in(config('core.add_on_services')),
Rule::unique('services', 'type')->where('server_id', $server->id),
Rule::in(config('core.service_types')),
],
'name' => [
'required',
Rule::in(array_keys(config('core.service_types'))),
],
'version' => 'required',
])->validate();

View File

@ -0,0 +1,28 @@
<?php
namespace App\Actions\Service;
use App\Enums\ServiceStatus;
use App\Models\Service;
use Illuminate\Support\Facades\Validator;
class Uninstall
{
public function uninstall(Service $service): void
{
Validator::make([
'service' => $service->id,
], $service->handler()->deletionRules())->validate();
$service->status = ServiceStatus::UNINSTALLING;
$service->save();
dispatch(function () use ($service) {
$service->handler()->uninstall();
$service->delete();
})->catch(function () use ($service) {
$service->status = ServiceStatus::FAILED;
$service->save();
})->onConnection('ssh');
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AgentController extends Controller
{
public function __invoke(Request $request, Server $server, int $id): JsonResponse
{
$validated = $this->validate($request, [
'load' => 'required|numeric',
'memory_total' => 'required|numeric',
'memory_used' => 'required|numeric',
'memory_free' => 'required|numeric',
'disk_total' => 'required|numeric',
'disk_used' => 'required|numeric',
'disk_free' => 'required|numeric',
]);
/** @var Service $service */
$service = $server->services()->findOrFail($id);
if ($request->header('secret') !== $service->handler()->data()['secret']) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$server->metrics()->create(array_merge($validated, ['server_id' => $server->id]));
return response()->json();
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Monitoring\GetMetrics;
use App\Facades\Toast;
use App\Models\Server;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class MetricController extends Controller
{
public function index(Server $server, Request $request): View|RedirectResponse
{
if (! $server->service('monitoring')) {
Toast::error('You need to install monitoring service first');
return redirect()->route('servers.services', $server);
}
return view('metrics.index', [
'server' => $server,
'data' => app(GetMetrics::class)->filter($server, $request->input()),
]);
}
}

View File

@ -2,7 +2,8 @@
namespace App\Http\Controllers;
use App\Actions\Service\Create;
use App\Actions\Service\Install;
use App\Actions\Service\Uninstall;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server;
@ -68,7 +69,16 @@ public function disable(Server $server, Service $service): RedirectResponse
public function install(Server $server, Request $request): HtmxResponse
{
app(Create::class)->create($server, $request->input());
app(Install::class)->install($server, $request->input());
Toast::success('Service is being uninstalled!');
return htmx()->back();
}
public function uninstall(Server $server, Service $service): HtmxResponse
{
app(Uninstall::class)->uninstall($service);
Toast::success('Service is being uninstalled!');

53
app/Models/Metric.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $server_id
* @property float $load
* @property float $memory_total
* @property float $memory_used
* @property float $memory_free
* @property float $disk_total
* @property float $disk_used
* @property float $disk_free
* @property Server $server
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class Metric extends Model
{
use HasFactory;
protected $fillable = [
'server_id',
'load',
'memory_total',
'memory_used',
'memory_free',
'disk_total',
'disk_used',
'disk_free',
];
protected $casts = [
'server_id' => 'integer',
'load' => 'float',
'memory_total' => 'float',
'memory_used' => 'float',
'memory_free' => 'float',
'disk_total' => 'float',
'disk_used' => 'float',
'disk_free' => 'float',
];
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
}

View File

@ -195,6 +195,11 @@ public function daemons(): HasMany
return $this->queues()->whereNull('site_id');
}
public function metrics(): HasMany
{
return $this->hasMany(Metric::class);
}
public function sshKeys(): BelongsToMany
{
return $this->belongsToMany(SshKey::class, 'server_ssh_keys')
@ -325,6 +330,24 @@ public function php(?string $version = null): ?Service
return $this->service('php', $version);
}
public function memoryDatabase(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('memory_database');
}
return $this->service('memory_database', $version);
}
public function monitoring(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('monitoring');
}
return $this->service('monitoring', $version);
}
public function sshKey(): array
{
/** @var FilesystemAdapter $storageDisk */

View File

@ -4,13 +4,7 @@
use App\Actions\Service\Manage;
use App\Exceptions\ServiceInstallationFailed;
use App\SSH\Services\AddOnServices\AbstractAddOnService;
use App\SSH\Services\Database\Database as DatabaseHandler;
use App\SSH\Services\Firewall\Firewall as FirewallHandler;
use App\SSH\Services\PHP\PHP as PHPHandler;
use App\SSH\Services\ProcessManager\ProcessManager as ProcessManagerHandler;
use App\SSH\Services\Redis\Redis as RedisHandler;
use App\SSH\Services\Webserver\Webserver as WebserverHandler;
use App\SSH\Services\ServiceInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
@ -65,8 +59,8 @@ public function server(): BelongsTo
return $this->belongsTo(Server::class);
}
public function handler(
): PHPHandler|WebserverHandler|DatabaseHandler|FirewallHandler|ProcessManagerHandler|RedisHandler|AbstractAddOnService {
public function handler(): ServiceInterface
{
$handler = config('core.service_handlers')[$this->name];
return new $handler($this);

View File

@ -148,4 +148,12 @@ public function unzip(string $path): string
'unzip '.$path
);
}
public function cleanup(): void
{
$this->server->ssh()->exec(
$this->getScript('cleanup.sh'),
'cleanup'
);
}
}

View File

@ -0,0 +1,19 @@
# Update package lists
sudo DEBIAN_FRONTEND=noninteractive apt-get update -y
# Remove unnecessary dependencies
sudo DEBIAN_FRONTEND=noninteractive apt-get autoremove --purge -y
# Clear package cache
sudo DEBIAN_FRONTEND=noninteractive apt-get clean -y
# Remove old configuration files
sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y $(dpkg -l | grep '^rc' | awk '{print $2}')
# Clear temporary files
sudo rm -rf /tmp/*
# Clear journal logs
sudo DEBIAN_FRONTEND=noninteractive journalctl --vacuum-time=1d
echo "Cleanup completed."

View File

@ -0,0 +1,42 @@
<?php
namespace App\SSH\Services;
use App\Models\Service;
abstract class AbstractService implements ServiceInterface
{
public function __construct(protected Service $service)
{
}
public function creationRules(array $input): array
{
return [];
}
public function creationData(array $input): array
{
return [];
}
public function deletionRules(): array
{
return [];
}
public function data(): array
{
return [];
}
public function install(): void
{
//
}
public function uninstall(): void
{
//
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\SSH\Services\AddOnServices;
use App\SSH\Services\ServiceInterface;
abstract class AbstractAddOnService implements ServiceInterface
{
abstract public function creationRules(array $input): array;
abstract public function creationData(array $input): array;
abstract public function create(): void;
abstract public function delete(): void;
abstract public function data(): array;
}

View File

@ -2,40 +2,78 @@
namespace App\SSH\Services\Database;
use App\Enums\BackupStatus;
use App\Models\BackupFile;
use App\Models\Server;
use App\Models\Service;
use App\SSH\HasScripts;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
use Closure;
abstract class AbstractDatabase implements Database, ServiceInterface
abstract class AbstractDatabase extends AbstractService implements Database
{
use HasScripts;
protected Service $service;
protected Server $server;
abstract protected function getScriptsDir(): string;
public function __construct(Service $service)
public function creationRules(array $input): array
{
$this->service = $service;
$this->server = $service->server;
return [
'type' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
$databaseExists = $this->service->server->database();
if ($databaseExists) {
$fail('You already have a database service on the server.');
}
},
],
];
}
public function install(): void
{
$version = $this->service->version;
$command = $this->getScript($this->service->name.'/install-'.$version.'.sh');
$this->server->ssh()->exec($command, 'install-'.$this->service->name.'-'.$version);
$status = $this->server->systemd()->status($this->service->unit);
$this->service->server->ssh()->exec($command, 'install-'.$this->service->name.'-'.$version);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
$this->service->server->os()->cleanup();
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail) {
$hasDatabase = $this->service->server->databases()->exists();
if ($hasDatabase) {
$fail('You have database(s) on the server.');
}
$hasDatabaseUser = $this->service->server->databaseUsers()->exists();
if ($hasDatabaseUser) {
$fail('You have database user(s) on the server.');
}
$hasRunningBackup = $this->service->server->backups()
->where('status', BackupStatus::RUNNING)
->exists();
if ($hasRunningBackup) {
$fail('You have database backup(s) on the server.');
}
},
],
];
}
public function uninstall(): void
{
$version = $this->service->version;
$command = $this->getScript($this->service->name.'/uninstall.sh');
$this->service->server->ssh()->exec($command, 'uninstall-'.$this->service->name.'-'.$version);
$this->service->server->os()->cleanup();
}
public function create(string $name): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/create.sh', [
'name' => $name,
]),
@ -45,7 +83,7 @@ public function create(string $name): void
public function delete(string $name): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/delete.sh', [
'name' => $name,
]),
@ -55,7 +93,7 @@ public function delete(string $name): void
public function createUser(string $username, string $password, string $host): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/create-user.sh', [
'username' => $username,
'password' => $password,
@ -67,7 +105,7 @@ public function createUser(string $username, string $password, string $host): vo
public function deleteUser(string $username, string $host): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/delete-user.sh', [
'username' => $username,
'host' => $host,
@ -78,7 +116,7 @@ public function deleteUser(string $username, string $host): void
public function link(string $username, string $host, array $databases): void
{
$ssh = $this->server->ssh();
$ssh = $this->service->server->ssh();
foreach ($databases as $database) {
$ssh->exec(
@ -94,7 +132,7 @@ public function link(string $username, string $host, array $databases): void
public function unlink(string $username, string $host): void
{
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/unlink.sh', [
'username' => $username,
'host' => $host,
@ -106,7 +144,7 @@ public function unlink(string $username, string $host): void
public function runBackup(BackupFile $backupFile): void
{
// backup
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/backup.sh', [
'file' => $backupFile->name,
'database' => $backupFile->backup->database->name,
@ -115,13 +153,13 @@ public function runBackup(BackupFile $backupFile): void
);
// upload to storage
$upload = $backupFile->backup->storage->provider()->ssh($this->server)->upload(
$upload = $backupFile->backup->storage->provider()->ssh($this->service->server)->upload(
$backupFile->path(),
$backupFile->storagePath(),
);
// cleanup
$this->server->ssh()->exec('rm '.$backupFile->name.'.zip');
$this->service->server->ssh()->exec('rm '.$backupFile->name.'.zip');
$backupFile->size = $upload['size'];
$backupFile->save();
@ -130,12 +168,12 @@ public function runBackup(BackupFile $backupFile): void
public function restoreBackup(BackupFile $backupFile, string $database): void
{
// download
$backupFile->backup->storage->provider()->ssh($this->server)->download(
$backupFile->backup->storage->provider()->ssh($this->service->server)->download(
$backupFile->storagePath(),
$backupFile->name.'.zip',
);
$this->server->ssh()->exec(
$this->service->server->ssh()->exec(
$this->getScript($this->getScriptsDir().'/restore.sh', [
'database' => $database,
'file' => $backupFile->name,

View File

@ -9,4 +9,6 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install mariadb-server mariadb-backup -y
sudo systemctl unmask mysql.service
sudo service mysql start

View File

@ -9,4 +9,6 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install mariadb-server mariadb-backup -y
sudo systemctl unmask mysql.service
sudo service mysql start

View File

@ -0,0 +1,9 @@
sudo service mysql stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove mariadb-server mariadb-backup -y
sudo rm -rf /etc/mysql
sudo rm -rf /var/lib/mysql
sudo rm -rf /var/log/mysql
sudo rm -rf /var/run/mysqld
sudo rm -rf /var/run/mysqld/mysqld.sock

View File

@ -1,5 +1,7 @@
sudo DEBIAN_FRONTEND=noninteractive apt-get install mysql-server -y
sudo systemctl unmask mysql.service
sudo service mysql enable
sudo service mysql start

View File

@ -6,6 +6,8 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install mysql-server -y
sudo systemctl unmask mysql.service
sudo service mysql enable
sudo service mysql start

View File

@ -0,0 +1,9 @@
sudo service mysql stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove mysql-server -y
sudo rm -rf /etc/mysql
sudo rm -rf /var/lib/mysql
sudo rm -rf /var/log/mysql
sudo rm -rf /var/run/mysqld
sudo rm -rf /var/run/mysqld/mysqld.sock

View File

@ -0,0 +1,11 @@
sudo service postgresql stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove postgresql-* -y
sudo rm -rf /etc/postgresql
sudo rm -rf /var/lib/postgresql
sudo rm -rf /var/log/postgresql
sudo rm -rf /var/run/postgresql
sudo rm -rf /var/run/postgresql/postmaster.pid
sudo rm -rf /var/run/postgresql/.s.PGSQL.5432
sudo rm -rf /var/run/postgresql/.s.PGSQL.5432.lock

View File

@ -2,15 +2,8 @@
namespace App\SSH\Services\Firewall;
use App\Models\Service;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
abstract class AbstractFirewall implements Firewall, ServiceInterface
abstract class AbstractFirewall extends AbstractService implements Firewall
{
protected Service $service;
public function __construct(Service $service)
{
$this->service = $service;
}
}

View File

@ -14,6 +14,12 @@ public function install(): void
$this->getScript('ufw/install-ufw.sh'),
'install-ufw'
);
$this->service->server->os()->cleanup();
}
public function uninstall(): void
{
//
}
public function addRule(string $type, string $protocol, int $port, string $source, ?string $mask): void

View File

@ -3,20 +3,43 @@
namespace App\SSH\Services\PHP;
use App\Exceptions\SSHCommandError;
use App\Models\Service;
use App\SSH\HasScripts;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
use Closure;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class PHP implements ServiceInterface
class PHP extends AbstractService
{
use HasScripts;
protected Service $service;
public function __construct(Service $service)
public function creationRules(array $input): array
{
$this->service = $service;
return [
'version' => [
'required',
Rule::in(config('core.php_versions')),
Rule::unique('services', 'version')
->where('type', 'php')
->where('server_id', $this->service->server_id),
],
];
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail) {
$hasSite = $this->service->server->sites()
->where('php_version', $this->service->version)
->exists();
if ($hasSite) {
$fail('Some sites are using this PHP version.');
}
},
],
];
}
public function install(): void
@ -29,6 +52,7 @@ public function install(): void
]),
'install-php-'.$this->service->version
);
$this->service->server->os()->cleanup();
}
public function uninstall(): void
@ -39,6 +63,7 @@ public function uninstall(): void
]),
'uninstall-php-'.$this->service->version
);
$this->service->server->os()->cleanup();
}
public function setDefaultCli(): void

View File

@ -2,15 +2,37 @@
namespace App\SSH\Services\ProcessManager;
use App\Models\Service;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
use Closure;
abstract class AbstractProcessManager implements ProcessManager, ServiceInterface
abstract class AbstractProcessManager extends AbstractService implements ProcessManager
{
protected Service $service;
public function __construct(Service $service)
public function creationRules(array $input): array
{
$this->service = $service;
return [
'type' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
$processManagerExists = $this->service->server->processManager();
if ($processManagerExists) {
$fail('You already have a process manager service on the server.');
}
},
],
];
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail) {
$hasQueue = $this->service->server->queues()->exists();
if ($hasQueue) {
$fail('You have queue(s) on the server.');
}
},
],
];
}
}

View File

@ -15,6 +15,18 @@ public function install(): void
$this->getScript('supervisor/install-supervisor.sh'),
'install-supervisor'
);
$this->service->server->os()->cleanup();
}
public function uninstall(): void
{
$this->service->server->ssh()->exec(
$this->getScript('supervisor/uninstall-supervisor.sh'),
'uninstall-supervisor'
);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
$this->service->server->os()->cleanup();
}
/**

View File

@ -0,0 +1,8 @@
sudo service supervisor stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove supervisor -y
sudo rm -rf /etc/supervisor
sudo rm -rf /var/log/supervisor
sudo rm -rf /var/run/supervisor
sudo rm -rf /var/run/supervisor/supervisor.sock

View File

@ -2,16 +2,27 @@
namespace App\SSH\Services\Redis;
use App\Models\Service;
use App\SSH\HasScripts;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
use Closure;
class Redis implements ServiceInterface
class Redis extends AbstractService
{
use HasScripts;
public function __construct(protected Service $service)
public function creationRules(array $input): array
{
return [
'type' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
$redisExists = $this->service->server->memoryDatabase();
if ($redisExists) {
$fail('You already have a Redis service on the server.');
}
},
],
];
}
public function install(): void
@ -20,5 +31,17 @@ public function install(): void
$this->getScript('install.sh'),
'install-redis'
);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
$this->service->server->os()->cleanup();
}
public function uninstall(): void
{
$this->service->server->ssh()->exec(
$this->getScript('uninstall.sh'),
'uninstall-redis'
);
$this->service->server->os()->cleanup();
}
}

View File

@ -0,0 +1,15 @@
sudo service redis stop
sudo DEBIAN_FRONTEND=noninteractive apt-get remove redis-server -y
sudo rm -rf /etc/redis
sudo rm -rf /var/lib/redis
sudo rm -rf /var/log/redis
sudo rm -rf /var/run/redis
sudo rm -rf /var/run/redis/redis-server.pid
sudo rm -rf /var/run/redis/redis-server.sock
sudo rm -rf /var/run/redis/redis-server.sock
sudo DEBIAN_FRONTEND=noninteractive sudo apt-get autoremove -y
sudo DEBIAN_FRONTEND=noninteractive sudo apt-get autoclean -y

View File

@ -4,5 +4,15 @@
interface ServiceInterface
{
public function creationRules(array $input): array;
public function creationData(array $input): array;
public function deletionRules(): array;
public function data(): array;
public function install(): void;
public function uninstall(): void;
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\SSH\Services\VitoAgent;
use App\Models\Metric;
use App\SSH\HasScripts;
use App\SSH\Services\AbstractService;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\Rule;
use Ramsey\Uuid\Uuid;
class VitoAgent extends AbstractService
{
use HasScripts;
const TAGS_URL = 'https://api.github.com/repos/vitodeploy/agent/tags';
const DOWNLOAD_URL = 'https://github.com/vitodeploy/agent/releases/download/%s';
public function creationRules(array $input): array
{
return [
'type' => [
Rule::unique('services', 'type')->where('server_id', $this->service->server_id),
],
'version' => [
'required',
Rule::in(['latest']),
],
];
}
public function creationData(array $input): array
{
return [
'url' => '',
'secret' => Uuid::uuid4()->toString(),
];
}
public function data(): array
{
return [
'url' => $this->service->type_data['url'] ?? null,
'secret' => $this->service->type_data['secret'] ?? null,
];
}
public function install(): void
{
$tags = Http::get(self::TAGS_URL)->json();
if (empty($tags)) {
throw new \Exception('Failed to fetch tags');
}
$this->service->version = $tags[0]['name'];
$this->service->save();
$downloadUrl = sprintf(self::DOWNLOAD_URL, $this->service->version);
$data = $this->data();
$data['url'] = route('api.servers.agent', [$this->service->server, $this->service->id]);
$this->service->type_data = $data;
$this->service->save();
$this->service->refresh();
$this->service->server->ssh()->exec(
$this->getScript('install.sh', [
'download_url' => $downloadUrl,
'config_url' => $this->data()['url'],
'config_secret' => $this->data()['secret'],
]),
'install-vito-agent'
);
$status = $this->service->server->systemd()->status($this->service->unit);
$this->service->validateInstall($status);
}
public function uninstall(): void
{
$this->service->server->ssh()->exec(
$this->getScript('uninstall.sh'),
'uninstall-vito-agent'
);
Metric::where('server_id', $this->service->server_id)->delete();
}
}

View File

@ -0,0 +1,53 @@
arch=$(uname -m)
if [ "$arch" == "x86_64" ]; then
executable="vitoagent-linux-amd64"
elif [ "$arch" == "i686" ]; then
executable="vitoagent-linux-amd"
elif [ "$arch" == "armv7l" ]; then
executable="vitoagent-linux-arm"
elif [ "$arch" == "aarch64" ]; then
executable="vitoagent-linux-arm64"
else
executable="vitoagent-linux-amd64"
fi
wget __download_url__/$executable
chmod +x ./$executable
sudo mv ./$executable /usr/local/bin/vito-agent
# create service
export VITO_AGENT_SERVICE="
[Unit]
Description=Vito Agent
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/vito-agent
Restart=on-failure
[Install]
WantedBy=multi-user.target
"
echo "${VITO_AGENT_SERVICE}" | sudo tee /etc/systemd/system/vito-agent.service
sudo mkdir -p /etc/vito-agent
export VITO_AGENT_CONFIG="
{
\"url\": \"__config_url__\",
\"secret\": \"__config_secret__\"
}
"
echo "${VITO_AGENT_CONFIG}" | sudo tee /etc/vito-agent/config.json
sudo systemctl daemon-reload
sudo systemctl enable vito-agent
sudo systemctl start vito-agent
echo "Vito Agent installed successfully"

View File

@ -0,0 +1,13 @@
sudo service vito-agent stop
sudo systemctl disable vito-agent
sudo rm -f /usr/local/bin/vito-agent
sudo rm -f /etc/systemd/system/vito-agent.service
sudo rm -rf /etc/vito-agent
sudo systemctl daemon-reload
echo "Vito Agent uninstalled successfully"

View File

@ -2,12 +2,8 @@
namespace App\SSH\Services\Webserver;
use App\Models\Service;
use App\SSH\Services\ServiceInterface;
use App\SSH\Services\AbstractService;
abstract class AbstractWebserver implements ServiceInterface, Webserver
abstract class AbstractWebserver extends AbstractService implements Webserver
{
public function __construct(protected Service $service)
{
}
}

View File

@ -6,6 +6,7 @@
use App\Models\Site;
use App\Models\Ssl;
use App\SSH\HasScripts;
use Closure;
use Illuminate\Support\Str;
use Throwable;
@ -23,6 +24,31 @@ public function install(): void
]),
'install-nginx'
);
$this->service->server->os()->cleanup();
}
public function deletionRules(): array
{
return [
'service' => [
function (string $attribute, mixed $value, Closure $fail) {
$hasSite = $this->service->server->sites()
->exists();
if ($hasSite) {
$fail('Cannot uninstall webserver while you have websites using it.');
}
},
],
];
}
public function uninstall(): void
{
$this->service->server->ssh()->exec(
$this->getScript('nginx/uninstall-nginx.sh'),
'uninstall-nginx'
);
$this->service->server->os()->cleanup();
}
public function createVHost(Site $site): void

View File

@ -0,0 +1,12 @@
sudo service nginx stop
sudo DEBIAN_FRONTEND=noninteractive apt-get purge nginx nginx-common nginx-full -y
sudo rm -rf /etc/nginx
sudo rm -rf /var/log/nginx
sudo rm -rf /var/lib/nginx
sudo rm -rf /var/cache/nginx
sudo rm -rf /usr/share/nginx
sudo rm -rf /etc/systemd/system/nginx.service
sudo systemctl daemon-reload

View File

@ -4,6 +4,7 @@
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\SSH\Services\PHP\PHP;
abstract class AbstractType implements ServerType
{
@ -31,7 +32,9 @@ public function install(): void
$service->update(['status' => ServiceStatus::READY]);
if ($service->type == 'php') {
$this->progress($currentProgress, 'installing-composer');
$service->handler()->installComposer();
/** @var PHP $handler */
$handler = $service->handler();
$handler->installComposer();
}
}
$this->progress(100, 'finishing');

View File

@ -13,7 +13,7 @@ public function createRules(array $input): array
],
'php' => [
'required',
'in:'.implode(',', config('core.php_versions')),
'in:none,'.implode(',', config('core.php_versions')),
],
'database' => [
'required',

View File

@ -34,3 +34,12 @@ function vito_version(): string
{
return exec('git describe --tags');
}
function convert_time_format($string): string
{
$string = preg_replace('/(\d+)m/', '$1 minutes', $string);
$string = preg_replace('/(\d+)s/', '$1 seconds', $string);
$string = preg_replace('/(\d+)d/', '$1 days', $string);
return preg_replace('/(\d+)h/', '$1 hours', $string);
}

View File

@ -1,35 +1,5 @@
<?php
use App\Enums\OperatingSystem;
use App\Enums\StorageProvider;
use App\NotificationChannels\Discord;
use App\NotificationChannels\Email;
use App\NotificationChannels\Slack;
use App\NotificationChannels\Telegram;
use App\ServerProviders\AWS;
use App\ServerProviders\DigitalOcean;
use App\ServerProviders\Hetzner;
use App\ServerProviders\Linode;
use App\ServerProviders\Vultr;
use App\SiteTypes\Laravel;
use App\SiteTypes\PHPBlank;
use App\SiteTypes\PHPMyAdmin;
use App\SiteTypes\PHPSite;
use App\SiteTypes\Wordpress;
use App\SourceControlProviders\Bitbucket;
use App\SourceControlProviders\Github;
use App\SourceControlProviders\Gitlab;
use App\SSH\Services\Database\Mariadb;
use App\SSH\Services\Database\Mysql;
use App\SSH\Services\Database\Postgresql;
use App\SSH\Services\Firewall\Ufw;
use App\SSH\Services\PHP\PHP;
use App\SSH\Services\ProcessManager\Supervisor;
use App\SSH\Services\Redis\Redis;
use App\SSH\Services\Webserver\Nginx;
use App\StorageProviders\Dropbox;
use App\StorageProviders\FTP;
return [
/*
* SSH
@ -44,12 +14,12 @@
* General
*/
'operating_systems' => [
OperatingSystem::UBUNTU20,
OperatingSystem::UBUNTU22,
\App\Enums\OperatingSystem::UBUNTU20,
\App\Enums\OperatingSystem::UBUNTU22,
],
'webservers' => ['none', 'nginx'],
'php_versions' => [
'none',
// 'none',
// '5.6',
'7.0',
'7.1',
@ -126,117 +96,102 @@
],
'server_providers_class' => [
\App\Enums\ServerProvider::CUSTOM => \App\ServerProviders\Custom::class,
\App\Enums\ServerProvider::AWS => AWS::class,
\App\Enums\ServerProvider::LINODE => Linode::class,
\App\Enums\ServerProvider::DIGITALOCEAN => DigitalOcean::class,
\App\Enums\ServerProvider::VULTR => Vultr::class,
\App\Enums\ServerProvider::HETZNER => Hetzner::class,
\App\Enums\ServerProvider::AWS => \App\ServerProviders\AWS::class,
\App\Enums\ServerProvider::LINODE => \App\ServerProviders\Linode::class,
\App\Enums\ServerProvider::DIGITALOCEAN => \App\ServerProviders\DigitalOcean::class,
\App\Enums\ServerProvider::VULTR => \App\ServerProviders\Vultr::class,
\App\Enums\ServerProvider::HETZNER => \App\ServerProviders\Hetzner::class,
],
'server_providers_default_user' => [
'custom' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
'aws' => [
'ubuntu_18' => 'ubuntu',
'ubuntu_20' => 'ubuntu',
'ubuntu_22' => 'ubuntu',
\App\Enums\OperatingSystem::UBUNTU20 => 'ubuntu',
\App\Enums\OperatingSystem::UBUNTU22 => 'ubuntu',
],
'linode' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
'digitalocean' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
'vultr' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
'hetzner' => [
'ubuntu_18' => 'root',
'ubuntu_20' => 'root',
'ubuntu_22' => 'root',
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
],
],
/*
* Service
*/
'service_handlers' => [
'nginx' => Nginx::class,
'mysql' => Mysql::class,
'mariadb' => Mariadb::class,
'postgresql' => Postgresql::class,
'redis' => Redis::class,
'php' => PHP::class,
'ufw' => Ufw::class,
'supervisor' => Supervisor::class,
'service_types' => [
'nginx' => 'webserver',
'mysql' => 'database',
'mariadb' => 'database',
'postgresql' => 'database',
'redis' => 'memory_database',
'php' => 'php',
'ufw' => 'firewall',
'supervisor' => 'process_manager',
'vito-agent' => 'monitoring',
],
'add_on_services' => [
// add-on services
'service_handlers' => [
'nginx' => \App\SSH\Services\Webserver\Nginx::class,
'mysql' => \App\SSH\Services\Database\Mysql::class,
'mariadb' => \App\SSH\Services\Database\Mariadb::class,
'postgresql' => \App\SSH\Services\Database\Postgresql::class,
'redis' => \App\SSH\Services\Redis\Redis::class,
'php' => \App\SSH\Services\PHP\PHP::class,
'ufw' => \App\SSH\Services\Firewall\Ufw::class,
'supervisor' => \App\SSH\Services\ProcessManager\Supervisor::class,
'vito-agent' => \App\SSH\Services\VitoAgent\VitoAgent::class,
],
'service_units' => [
'nginx' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'nginx',
],
'ubuntu_20' => [
'latest' => 'nginx',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'nginx',
],
],
'mysql' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'5.7' => 'mysql',
'8.0' => 'mysql',
],
'ubuntu_20' => [
'5.7' => 'mysql',
'8.0' => 'mysql',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'5.7' => 'mysql',
'8.0' => 'mysql',
],
],
'mariadb' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'10.3' => 'mariadb',
'10.4' => 'mariadb',
],
'ubuntu_20' => [
'10.3' => 'mariadb',
'10.4' => 'mariadb',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'10.3' => 'mariadb',
'10.4' => 'mariadb',
],
],
'postgresql' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'12' => 'postgresql',
'13' => 'postgresql',
'14' => 'postgresql',
'15' => 'postgresql',
'16' => 'postgresql',
],
'ubuntu_20' => [
'12' => 'postgresql',
'13' => 'postgresql',
'14' => 'postgresql',
'15' => 'postgresql',
'16' => 'postgresql',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'12' => 'postgresql',
'13' => 'postgresql',
'14' => 'postgresql',
@ -245,19 +200,7 @@
],
],
'php' => [
'ubuntu_18' => [
'5.6' => 'php5.6-fpm',
'7.0' => 'php7.0-fpm',
'7.1' => 'php7.1-fpm',
'7.2' => 'php7.2-fpm',
'7.3' => 'php7.3-fpm',
'7.4' => 'php7.4-fpm',
'8.0' => 'php8.0-fpm',
'8.1' => 'php8.1-fpm',
'8.2' => 'php8.2-fpm',
'8.3' => 'php8.3-fpm',
],
'ubuntu_20' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'5.6' => 'php5.6-fpm',
'7.0' => 'php7.0-fpm',
'7.1' => 'php7.1-fpm',
@ -268,7 +211,7 @@
'8.1' => 'php8.1-fpm',
'8.3' => 'php8.3-fpm',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'5.6' => 'php5.6-fpm',
'7.0' => 'php7.0-fpm',
'7.1' => 'php7.1-fpm',
@ -282,36 +225,35 @@
],
],
'redis' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'redis',
],
'ubuntu_20' => [
'latest' => 'redis',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'redis',
],
],
'supervisor' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'supervisor',
],
'ubuntu_20' => [
'latest' => 'supervisor',
],
'ubuntu_22' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'supervisor',
],
],
'ufw' => [
'ubuntu_18' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'ufw',
],
'ubuntu_20' => [
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'ufw',
],
'ubuntu_22' => [
'latest' => 'ufw',
],
'vito-agent' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
'latest' => 'vito-agent',
],
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'vito-agent',
],
],
],
@ -327,11 +269,11 @@
\App\Enums\SiteType::PHPMYADMIN,
],
'site_types_class' => [
\App\Enums\SiteType::PHP => PHPSite::class,
\App\Enums\SiteType::PHP_BLANK => PHPBlank::class,
\App\Enums\SiteType::LARAVEL => Laravel::class,
\App\Enums\SiteType::WORDPRESS => Wordpress::class,
\App\Enums\SiteType::PHPMYADMIN => PHPMyAdmin::class,
\App\Enums\SiteType::PHP => \App\SiteTypes\PHPSite::class,
\App\Enums\SiteType::PHP_BLANK => \App\SiteTypes\PHPBlank::class,
\App\Enums\SiteType::LARAVEL => \App\SiteTypes\Laravel::class,
\App\Enums\SiteType::WORDPRESS => \App\SiteTypes\Wordpress::class,
\App\Enums\SiteType::PHPMYADMIN => \App\SiteTypes\PHPMyAdmin::class,
],
/*
@ -343,9 +285,9 @@
'bitbucket',
],
'source_control_providers_class' => [
'github' => Github::class,
'gitlab' => Gitlab::class,
'bitbucket' => Bitbucket::class,
'github' => \App\SourceControlProviders\Github::class,
'gitlab' => \App\SourceControlProviders\Gitlab::class,
'bitbucket' => \App\SourceControlProviders\Bitbucket::class,
],
/*
@ -403,22 +345,22 @@
\App\Enums\NotificationChannel::TELEGRAM,
],
'notification_channels_providers_class' => [
\App\Enums\NotificationChannel::SLACK => Slack::class,
\App\Enums\NotificationChannel::DISCORD => Discord::class,
\App\Enums\NotificationChannel::EMAIL => Email::class,
\App\Enums\NotificationChannel::TELEGRAM => Telegram::class,
\App\Enums\NotificationChannel::SLACK => \App\NotificationChannels\Slack::class,
\App\Enums\NotificationChannel::DISCORD => \App\NotificationChannels\Discord::class,
\App\Enums\NotificationChannel::EMAIL => \App\NotificationChannels\Email::class,
\App\Enums\NotificationChannel::TELEGRAM => \App\NotificationChannels\Telegram::class,
],
/*
* storage providers
*/
'storage_providers' => [
StorageProvider::DROPBOX,
StorageProvider::FTP,
\App\Enums\StorageProvider::DROPBOX,
\App\Enums\StorageProvider::FTP,
],
'storage_providers_class' => [
'dropbox' => Dropbox::class,
'ftp' => FTP::class,
'dropbox' => \App\StorageProviders\Dropbox::class,
'ftp' => \App\StorageProviders\Ftp::class,
],
'ssl_types' => [

View File

@ -0,0 +1,22 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class MetricFactory extends Factory
{
public function definition(): array
{
return [
'server_id' => 1,
'load' => $this->faker->randomFloat(2, 0, 100),
'memory_total' => $this->faker->randomFloat(0, 0, 100),
'memory_used' => $this->faker->randomFloat(0, 0, 100),
'memory_free' => $this->faker->randomFloat(0, 0, 100),
'disk_total' => $this->faker->randomFloat(0, 0, 100),
'disk_used' => $this->faker->randomFloat(0, 0, 100),
'disk_free' => $this->faker->randomFloat(0, 0, 100),
];
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('metrics', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('server_id');
$table->decimal('load', 5, 2);
$table->decimal('memory_total', 15, 0);
$table->decimal('memory_used', 15, 0);
$table->decimal('memory_free', 15, 0);
$table->decimal('disk_total', 15, 0);
$table->decimal('disk_used', 15, 0);
$table->decimal('disk_free', 15, 0);
$table->timestamps();
$table->index(['server_id', 'created_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('metrics');
}
};

View File

@ -0,0 +1,32 @@
<?php
namespace Database\Seeders;
use App\Models\Metric;
use App\Models\Service;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Illuminate\Database\Seeder;
class MetricsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Metric::query()->delete();
$monitoring = Service::query()
->where('type', 'monitoring')
->firstOrFail();
$range = CarbonPeriod::create(Carbon::now()->subDays(7), '1 minute', Carbon::now());
foreach ($range as $date) {
Metric::factory()->create([
'server_id' => $monitoring->server_id,
'created_at' => $date,
]);
}
}
}

225
package-lock.json generated
View File

@ -8,8 +8,10 @@
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.9",
"alpinejs": "^3.4.2",
"apexcharts": "^3.44.2",
"autoprefixer": "^10.4.2",
"flowbite": "^2.3.0",
"flowbite-datepicker": "^1.2.6",
"htmx.org": "^1.9.10",
"laravel-echo": "^1.15.0",
"laravel-vite-plugin": "^0.7.2",
@ -529,6 +531,12 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
"dev": true
},
"node_modules/@yr/monotone-cubic-spline": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
"dev": true
},
"node_modules/alpinejs": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.12.0.tgz",
@ -557,6 +565,21 @@
"node": ">= 8"
}
},
"node_modules/apexcharts": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.48.0.tgz",
"integrity": "sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==",
"dev": true,
"dependencies": {
"@yr/monotone-cubic-spline": "^1.0.3",
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@ -885,6 +908,15 @@
"mini-svg-data-uri": "^1.4.3"
}
},
"node_modules/flowbite-datepicker": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.2.6.tgz",
"integrity": "sha512-UbU/xXs9HFiwWfL4M1vpwIo8EpS0NUQSOvYnp0Z9u3N118nU7lPFGoUOq7su9d0aOJy9FssXzx1SZwN8MXhE1g==",
"dev": true,
"dependencies": {
"flowbite": "^2.0.0"
}
},
"node_modules/fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@ -1680,6 +1712,97 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"dev": true,
"dependencies": {
"svg.js": "^2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
"dev": true,
"dependencies": {
"svg.js": ">=2.3.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
"dev": true,
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==",
"dev": true
},
"node_modules/svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"dev": true,
"dependencies": {
"svg.js": "^2.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"dev": true,
"dependencies": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js/node_modules/svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"dev": true,
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"dev": true,
"dependencies": {
"svg.js": "^2.6.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/tailwindcss": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz",
@ -2177,6 +2300,12 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
"dev": true
},
"@yr/monotone-cubic-spline": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
"dev": true
},
"alpinejs": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.12.0.tgz",
@ -2202,6 +2331,21 @@
"picomatch": "^2.0.4"
}
},
"apexcharts": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.48.0.tgz",
"integrity": "sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==",
"dev": true,
"requires": {
"@yr/monotone-cubic-spline": "^1.0.3",
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
}
},
"arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@ -2434,6 +2578,15 @@
"mini-svg-data-uri": "^1.4.3"
}
},
"flowbite-datepicker": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.2.6.tgz",
"integrity": "sha512-UbU/xXs9HFiwWfL4M1vpwIo8EpS0NUQSOvYnp0Z9u3N118nU7lPFGoUOq7su9d0aOJy9FssXzx1SZwN8MXhE1g==",
"dev": true,
"requires": {
"flowbite": "^2.0.0"
}
},
"fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@ -2911,6 +3064,78 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"dev": true,
"requires": {
"svg.js": "^2.0.1"
}
},
"svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
"dev": true,
"requires": {
"svg.js": ">=2.3.x"
}
},
"svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
"dev": true,
"requires": {
"svg.js": "^2.2.5"
}
},
"svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==",
"dev": true
},
"svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"dev": true,
"requires": {
"svg.js": "^2.4.0"
}
},
"svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"dev": true,
"requires": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"dependencies": {
"svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"dev": true,
"requires": {
"svg.js": "^2.2.5"
}
}
}
},
"svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"dev": true,
"requires": {
"svg.js": "^2.6.5"
}
},
"tailwindcss": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz",

View File

@ -22,6 +22,8 @@
"tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7",
"toastr": "^2.1.4",
"vite": "^4.5.3"
"vite": "^4.5.3",
"apexcharts": "^3.44.2",
"flowbite-datepicker": "^1.2.6"
}
}

View File

@ -0,0 +1,22 @@
<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="800" height="800" rx="50" fill="#5046E5" />
<g filter="url(#filter0_d_1_35)">
<path
d="M400 601.2C392.4 601.2 386.2 599.4 381.4 595.8C376.6 592.2 372.8 586.8 370 579.6L205 202.2C202.2 195.4 201.4 189.8 202.6 185.4C203.8 180.6 206.4 176.8 210.4 174C214.8 171.2 219.8 169.8 225.4 169.8C233 169.8 238.6 171.6 242.2 175.2C246.2 178.4 249.4 183.2 251.8 189.6L410.2 557.4H391L548.8 189C551.6 183 555 178.4 559 175.2C563 171.6 568.6 169.8 575.8 169.8C581.4 169.8 586 171.2 589.6 174C593.6 176.8 596 180.6 596.8 185.4C598 190.2 597.2 195.8 594.4 202.2L429.4 579.6C426.6 586.8 422.8 592.2 418 595.8C413.6 599.4 407.6 601.2 400 601.2Z"
fill="white" />
</g>
<defs>
<filter id="filter0_d_1_35" x="196.8" y="169.8" width="405.8" height="439.4" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha" />
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_35" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_35" result="shape" />
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,9 +1,13 @@
import 'flowbite';
import 'flowbite/dist/datepicker.js';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
import ApexCharts from 'apexcharts';
window.ApexCharts = ApexCharts;
import htmx from "htmx.org";
window.htmx = htmx;
window.htmx.defineExtension('disable-element', {

View File

@ -0,0 +1,103 @@
@props([
"id",
"type",
"title",
"color",
"sets",
"categories",
"toolbar" => false,
])
<x-simple-card {{ $attributes }}>
<div class="relative">
<div class="absolute left-4 top-4">{{ $title }}</div>
</div>
<div id="{{ $id }}" class="pt-4"></div>
<script>
window.addEventListener('load', function () {
let options = {
series: [
@foreach ($sets as $set)
{
name: '{{ $set["name"] }}',
data: @json($set["data"]),
color: '{{ $set["color"] }}'
},
@endforeach
],
chart: {
height: '100%',
maxWidth: '100%',
type: '{{ $type }}',
fontFamily: 'Inter, sans-serif',
dropShadow: {
enabled: false
},
toolbar: {
show: @js($toolbar)
}
},
tooltip: {
enabled: true,
x: {
show: true
}
},
legend: {
show: true
},
@if ($type == 'area')
fill: {
type: 'gradient',
gradient: {
opacityFrom: 0.55,
opacityTo: 0,
shade: '{{ $color }}',
gradientToColors: ['{{ $color }}']
}
},
@endif
dataLabels: {
enabled: false
},
stroke: {
width: 2,
curve: 'smooth'
},
grid: {
show: false,
strokeDashArray: 4,
padding: {
left: 2,
right: 2,
top: 0
}
},
xaxis: {
categories: @json($categories),
labels: {
show: false
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
show: false,
labels: {
formatter: function (value) {
return parseInt(value);
}
}
}
};
if (document.getElementById('{{ $id }}') && typeof ApexCharts !== 'undefined') {
const chart = new ApexCharts(document.getElementById('{{ $id }}'), options);
chart.render();
}
});
</script>
</x-simple-card>

View File

@ -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 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"
/>
</svg>

After

Width:  |  Height:  |  Size: 482 B

View File

@ -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="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@ -37,7 +37,7 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g
</li>
@endif
@if ($server->database())
@if ($server->database()?->status == \App\Enums\ServiceStatus::READY)
<li>
<x-sidebar-link
:href="route('servers.databases', ['server' => $server])"
@ -52,19 +52,17 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g
</li>
@endif
@if ($server->php())
<li>
<x-sidebar-link
:href="route('servers.php', ['server' => $server])"
:active="request()->routeIs('servers.php')"
>
<x-heroicon name="o-code-bracket" class="h-6 w-6" />
<span class="ml-2">
{{ __("PHP") }}
</span>
</x-sidebar-link>
</li>
@endif
<li>
<x-sidebar-link
:href="route('servers.php', ['server' => $server])"
:active="request()->routeIs('servers.php')"
>
<x-heroicon name="o-code-bracket" class="h-6 w-6" />
<span class="ml-2">
{{ __("PHP") }}
</span>
</x-sidebar-link>
</li>
@if ($server->firewall())
<li>
@ -116,6 +114,20 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g
</x-sidebar-link>
</li>
@if ($server->monitoring())
<li>
<x-sidebar-link
:href="route('servers.metrics', ['server' => $server])"
:active="request()->routeIs('servers.metrics')"
>
<x-heroicon name="o-chart-bar" class="h-6 w-6" />
<span class="ml-2">
{{ __("Metrics") }}
</span>
</x-sidebar-link>
</li>
@endif
<li>
<x-sidebar-link
:href="route('servers.console', ['server' => $server])"

View File

@ -28,7 +28,7 @@ class="mr-1"
</x-tab-item>
@endif
@if ($site->hasFeature(SiteFeature::QUEUES))
@if ($site->hasFeature(SiteFeature::QUEUES) && $site->server->processManager()?->status == \App\Enums\ServiceStatus::READY)
<x-tab-item
class="mr-1"
:href="route('servers.sites.queues', ['server' => $site->server, 'site' => $site])"

View File

@ -0,0 +1,68 @@
<x-server-layout :server="$server">
<x-slot name="pageTitle">{{ $server->name }} - Metrics</x-slot>
@include("metrics.partials.filter")
@php
$cpuSets = [
"name" => "CPU Load",
"data" => $data["metrics"]->pluck("load")->toArray(),
"color" => "#ff9900",
];
$memorySets = [
"name" => "Memory Usage",
"data" => $data["metrics"]->pluck("memory_used")->toArray(),
"color" => "#3366cc",
];
$diskSets = [
"name" => "Disk Usage",
"data" => $data["metrics"]->pluck("disk_used")->toArray(),
"color" => "#109618",
];
@endphp
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
<x-chart
id="cpu-load"
type="area"
title="CPU Load"
:sets="[$cpuSets]"
:categories="$data['metrics']->pluck('date')->toArray()"
color="#ff9900"
class="h-[200px] !p-0"
/>
<x-chart
id="memory-usage"
type="area"
title="Memory"
:sets="[$memorySets]"
:categories="$data['metrics']->pluck('date')->toArray()"
color="#3366cc"
class="h-[200px] !p-0"
/>
<x-chart
id="disk-usage"
type="area"
title="Disk"
:sets="[$diskSets]"
:categories="$data['metrics']->pluck('date')->toArray()"
color="#109618"
class="h-[200px] !p-0"
/>
</div>
<div class="mt-10">
<x-chart
id="resource-usage"
type="line"
title="Resource Usage"
:sets="[$cpuSets, $memorySets, $diskSets]"
:categories="$data['metrics']->pluck('date')->toArray()"
color="#109618"
:toolbar="true"
class="h-[400px] !px-0 !pt-0"
/>
</div>
</x-server-layout>

View File

@ -0,0 +1,76 @@
<div class="flex items-center" x-data="{ period: '{{ request()->query("period") ?? "10m" }}' }">
<x-dropdown align="left" class="ml-2">
<x-slot name="trigger">
<div data-tooltip="Change Period">
<div
class="flex w-full items-center rounded-md border border-gray-300 bg-white p-2.5 pr-10 text-sm capitalize text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
>
<div x-text="period"></div>
</div>
<button type="button" class="absolute inset-y-0 right-0 flex items-center pr-2">
<x-heroicon name="o-chevron-down" class="h-4 w-4 text-gray-400" />
</button>
</div>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('servers.metrics', ['server' => $server, 'period' => '10m'])">
10 Minutes
</x-dropdown-link>
<x-dropdown-link :href="route('servers.metrics', ['server' => $server, 'period' => '30m'])">
30 Minutes
</x-dropdown-link>
<x-dropdown-link :href="route('servers.metrics', ['server' => $server, 'period' => '1h'])">
1 Hour
</x-dropdown-link>
<x-dropdown-link :href="route('servers.metrics', ['server' => $server, 'period' => '12h'])">
12 Hours
</x-dropdown-link>
<x-dropdown-link :href="route('servers.metrics', ['server' => $server, 'period' => '1d'])">
1 Day
</x-dropdown-link>
<x-dropdown-link :href="route('servers.metrics', ['server' => $server, 'period' => '7d'])">
7 Days
</x-dropdown-link>
<x-dropdown-link x-on:click="period = 'custom'" class="cursor-pointer">Custom</x-dropdown-link>
</x-slot>
</x-dropdown>
<form
x-show="period === 'custom'"
class="flex items-center"
action="{{ route("servers.metrics", ["server" => $server, "period" => "custom"]) }}"
>
<input type="hidden" name="period" value="custom" />
<div date-rangepicker datepicker-format="yyyy-mm-dd" class="ml-2 flex items-center">
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3">
<x-heroicon name="o-calendar" class="h-4 w-4 text-gray-500 dark:text-gray-400" />
</div>
<input
name="from"
type="text"
class="block w-full rounded-md border border-gray-300 bg-white p-2.5 ps-10 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="{{ __("From Date") }}"
value="{{ request()->query("from") }}"
autocomplete="off"
/>
</div>
<span class="mx-2 text-gray-500">to</span>
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3">
<x-heroicon name="o-calendar" class="h-4 w-4 text-gray-500 dark:text-gray-400" />
</div>
<input
name="to"
type="text"
class="block w-full rounded-md border border-gray-300 bg-white p-2.5 ps-10 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
placeholder="{{ __("To Date") }}"
value="{{ request()->query("to") }}"
autocomplete="off"
/>
<x-input-error class="absolute left-0 top-10 ml-1 mt-1" :messages="$errors->get('to')" />
</div>
</div>
<x-primary-button class="ml-2 h-[42px]">{{ __("Filter") }}</x-primary-button>
</form>
</div>

View File

@ -239,6 +239,7 @@ class="mt-1 block w-full"
<div x-show="['{{ ServerType::REGULAR }}'].includes(type)">
<x-input-label for="php" value="PHP" />
<x-select-input id="php" name="php" class="mt-1 w-full">
<option value="none" @if('none' == old('php', '8.2')) selected @endif>none</option>
@foreach (config("core.php_versions") as $p)
<option value="{{ $p }}" @if($p == old('php', '8.2')) selected @endif>
{{ $p }}

View File

@ -3,5 +3,7 @@
@include("services.partials.services-list")
{{-- @include("services.partials.add-ons") --}}
@include("services.partials.available-services")
@stack("modals")
</x-server-layout>

View File

@ -1 +1,6 @@
@include("services.partials.unit-actions")
@include("services.partials.unit-actions.restart")
@include("services.partials.unit-actions.start")
@include("services.partials.unit-actions.stop")
@include("services.partials.unit-actions.enable")
@include("services.partials.unit-actions.disable")
@include("services.partials.unit-actions.uninstall")

View File

@ -1 +1,6 @@
@include("services.partials.unit-actions")
@include("services.partials.unit-actions.restart")
@include("services.partials.unit-actions.start")
@include("services.partials.unit-actions.stop")
@include("services.partials.unit-actions.enable")
@include("services.partials.unit-actions.disable")
@include("services.partials.unit-actions.uninstall")

View File

@ -1 +1,6 @@
@include("services.partials.unit-actions")
@include("services.partials.unit-actions.restart")
@include("services.partials.unit-actions.start")
@include("services.partials.unit-actions.stop")
@include("services.partials.unit-actions.enable")
@include("services.partials.unit-actions.disable")
@include("services.partials.unit-actions.uninstall")

View File

@ -1 +1,6 @@
@include("services.partials.unit-actions")
@include("services.partials.unit-actions.restart")
@include("services.partials.unit-actions.start")
@include("services.partials.unit-actions.stop")
@include("services.partials.unit-actions.enable")
@include("services.partials.unit-actions.disable")
@include("services.partials.unit-actions.uninstall")

View File

@ -1 +1,6 @@
@include("services.partials.unit-actions")
@include("services.partials.unit-actions.restart")
@include("services.partials.unit-actions.start")
@include("services.partials.unit-actions.stop")
@include("services.partials.unit-actions.enable")
@include("services.partials.unit-actions.disable")
@include("services.partials.unit-actions.uninstall")

View File

@ -1 +1,6 @@
@include("services.partials.unit-actions")
@include("services.partials.unit-actions.restart")
@include("services.partials.unit-actions.start")
@include("services.partials.unit-actions.stop")
@include("services.partials.unit-actions.enable")
@include("services.partials.unit-actions.disable")
@include("services.partials.unit-actions.uninstall")

View File

@ -1 +1,6 @@
@include("services.partials.unit-actions")
@include("services.partials.unit-actions.restart")
@include("services.partials.unit-actions.start")
@include("services.partials.unit-actions.stop")
@include("services.partials.unit-actions.enable")
@include("services.partials.unit-actions.disable")
@include("services.partials.unit-actions.uninstall")

View File

@ -1 +1,5 @@
@include("services.partials.unit-actions")
@include("services.partials.unit-actions.restart")
@include("services.partials.unit-actions.start")
@include("services.partials.unit-actions.stop")
@include("services.partials.unit-actions.enable")
@include("services.partials.unit-actions.disable")

View File

@ -0,0 +1,6 @@
@include("services.partials.unit-actions.restart")
@include("services.partials.unit-actions.start")
@include("services.partials.unit-actions.stop")
@include("services.partials.unit-actions.enable")
@include("services.partials.unit-actions.disable")
@include("services.partials.unit-actions.uninstall")

View File

@ -1,33 +0,0 @@
<div>
<x-card-header>
<x-slot name="title">Supported Services</x-slot>
<x-slot name="description">Here you can find the supported services to install</x-slot>
<x-slot name="aside"></x-slot>
</x-card-header>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@foreach (config("core.add_on_services") as $addOn)
<div
class="relative flex h-auto flex-col items-center justify-between space-y-3 rounded-b-md rounded-t-md border border-gray-200 bg-white text-center dark:border-gray-700 dark:bg-gray-800"
>
<div class="space-y-3 p-5">
<div class="flex items-center justify-center">
<img src="{{ asset("static/images/" . $addOn . ".svg") }}" class="h-20 w-20" alt="" />
</div>
<div class="flex flex-grow flex-col items-start justify-center">
<div class="flex items-center">
<div class="flex items-center text-lg">
{{ $addOn }}
</div>
</div>
</div>
</div>
<div
class="flex w-full items-center justify-between rounded-b-md border-t border-t-gray-200 bg-gray-50 p-2 dark:border-t-gray-600 dark:bg-gray-700"
>
@include("services.partials.add-on-installers." . $addOn)
</div>
</div>
@endforeach
</div>
</div>

View File

@ -0,0 +1,33 @@
<div>
<x-card-header>
<x-slot name="title">Supported Services</x-slot>
<x-slot name="description">Here you can find the supported services to install</x-slot>
<x-slot name="aside"></x-slot>
</x-card-header>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@foreach (config("core.service_handlers") as $key => $addOn)
@if (! $server->services()->where("name", $key)->exists())
<div
class="relative flex h-auto flex-col items-center justify-between space-y-3 rounded-b-md rounded-t-md border border-gray-200 bg-white text-center dark:border-gray-700 dark:bg-gray-800"
>
<div class="space-y-3 p-5">
<div class="flex items-center justify-center">
<img src="{{ asset("static/images/" . $key . ".svg") }}" class="h-20 w-20" alt="" />
</div>
<div class="flex flex-grow flex-col items-center justify-center">
<div class="flex items-center justify-center text-center text-lg">
{{ $key }}
</div>
</div>
</div>
<div
class="flex w-full items-center justify-between rounded-b-md border-t border-t-gray-200 bg-gray-50 p-2 dark:border-t-gray-600 dark:bg-gray-700"
>
@include("services.partials.installers." . $key)
</div>
</div>
@endif
@endforeach
</div>
</div>

View File

@ -0,0 +1,48 @@
<x-secondary-button class="!w-full" x-on:click="$dispatch('open-modal', 'install-mariadb')">Install</x-secondary-button>
@push("modals")
<x-modal name="install-mariadb">
<form
id="install-mariadb-form"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-mariadb-form"
class="p-6"
>
@csrf
<input type="hidden" name="name" value="mariadb" />
<input type="hidden" name="type" value="database" />
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("Install mariadb") }}
</h2>
<div class="mt-6">
<x-input-label for="version" value="Version" />
<x-select-input id="version" name="version" class="mt-1 w-full">
@foreach (collect(config("core.databases_name"))->filter(fn ($value) => $value == "mariadb") as $db => $value)
<option value="{{ config("core.databases_version")[$db] }}">
{{ config("core.databases_name")[$db] }} {{ config("core.databases_version")[$db] }}
</option>
@endforeach
</x-select-input>
@error("version")
<x-input-error class="mt-2" :messages="$message" />
@enderror
@error("type")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}
</x-secondary-button>
<x-primary-button id="btn-install-mariadb" hx-disable class="ml-3">
{{ __("Install") }}
</x-primary-button>
</div>
</form>
</x-modal>
@endpush

View File

@ -0,0 +1,48 @@
<x-secondary-button class="!w-full" x-on:click="$dispatch('open-modal', 'install-mysql')">Install</x-secondary-button>
@push("modals")
<x-modal name="install-mysql">
<form
id="install-mysql-form"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-mysql-form"
class="p-6"
>
@csrf
<input type="hidden" name="name" value="mysql" />
<input type="hidden" name="type" value="database" />
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("Install Mysql") }}
</h2>
<div class="mt-6">
<x-input-label for="version" value="Version" />
<x-select-input id="version" name="version" class="mt-1 w-full">
@foreach (collect(config("core.databases_name"))->filter(fn ($value) => $value == "mysql") as $db => $value)
<option value="{{ config("core.databases_version")[$db] }}">
{{ config("core.databases_name")[$db] }} {{ config("core.databases_version")[$db] }}
</option>
@endforeach
</x-select-input>
@error("version")
<x-input-error class="mt-2" :messages="$message" />
@enderror
@error("type")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}
</x-secondary-button>
<x-primary-button id="btn-install-mysql" hx-disable class="ml-3">
{{ __("Install") }}
</x-primary-button>
</div>
</form>
</x-modal>
@endpush

View File

@ -0,0 +1,13 @@
<form
id="install-nginx"
class="w-full"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-nginx"
>
@csrf
<input type="hidden" name="name" value="nginx" />
<input type="hidden" name="type" value="webserver" />
<input type="hidden" name="version" value="latest" />
<x-secondary-button class="!w-full" hx-disable>Install</x-secondary-button>
</form>

View File

@ -0,0 +1,43 @@
<x-secondary-button class="!w-full" x-on:click="$dispatch('open-modal', 'install-php')">Install</x-secondary-button>
@push("modals")
<x-modal name="install-php">
<form
id="install-php-form"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-php-form"
class="p-6"
>
@csrf
<input type="hidden" name="type" value="php" />
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("Install PHP") }}
</h2>
<div class="mt-6">
<x-input-label for="version" value="Version" />
<x-select-input id="version" name="version" class="mt-1 w-full">
@foreach (config("core.php_versions") as $p)
<option value="{{ $p }}">
{{ $p }}
</option>
@endforeach
</x-select-input>
@error("version")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}
</x-secondary-button>
<x-primary-button id="btn-install-php" hx-disable class="ml-3">
{{ __("Install") }}
</x-primary-button>
</div>
</form>
</x-modal>
@endpush

View File

@ -0,0 +1,50 @@
<x-secondary-button class="!w-full" x-on:click="$dispatch('open-modal', 'install-postgresql')">
Install
</x-secondary-button>
@push("modals")
<x-modal name="install-postgresql">
<form
id="install-postgresql-form"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-postgresql-form"
class="p-6"
>
@csrf
<input type="hidden" name="name" value="postgresql" />
<input type="hidden" name="type" value="database" />
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("Install postgresql") }}
</h2>
<div class="mt-6">
<x-input-label for="version" value="Version" />
<x-select-input id="version" name="version" class="mt-1 w-full">
@foreach (collect(config("core.databases_name"))->filter(fn ($value) => $value == "postgresql") as $db => $value)
<option value="{{ config("core.databases_version")[$db] }}">
{{ config("core.databases_name")[$db] }} {{ config("core.databases_version")[$db] }}
</option>
@endforeach
</x-select-input>
@error("version")
<x-input-error class="mt-2" :messages="$message" />
@enderror
@error("type")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}
</x-secondary-button>
<x-primary-button id="btn-install-postgresql" hx-disable class="ml-3">
{{ __("Install") }}
</x-primary-button>
</div>
</form>
</x-modal>
@endpush

View File

@ -0,0 +1,13 @@
<form
id="install-redis"
class="w-full"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-redis"
>
@csrf
<input type="hidden" name="name" value="redis" />
<input type="hidden" name="type" value="memory_database" />
<input type="hidden" name="version" value="latest" />
<x-secondary-button class="!w-full" hx-disable>Install</x-secondary-button>
</form>

View File

@ -0,0 +1,13 @@
<form
id="install-supervisor"
class="w-full"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-supervisor"
>
@csrf
<input type="hidden" name="name" value="supervisor" />
<input type="hidden" name="type" value="process_manager" />
<input type="hidden" name="version" value="latest" />
<x-secondary-button class="!w-full" hx-disable>Install</x-secondary-button>
</form>

View File

@ -0,0 +1,13 @@
<form
id="install-ufw"
class="w-full"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-ufw"
>
@csrf
<input type="hidden" name="name" value="ufw" />
<input type="hidden" name="type" value="firewall" />
<input type="hidden" name="version" value="latest" />
<x-secondary-button class="!w-full" hx-disable>Install</x-secondary-button>
</form>

View File

@ -0,0 +1,39 @@
<x-secondary-button class="!w-full" x-on:click="$dispatch('open-modal', 'install-vito-agent')">
Install
</x-secondary-button>
@push("modals")
<x-modal name="install-vito-agent">
<form
id="install-vito-agent-form"
hx-post="{{ route("servers.services.install", ["server" => $server]) }}"
hx-swap="outerHTML"
hx-select="#install-vito-agent-form"
class="p-6"
>
@csrf
<input type="hidden" name="name" value="vito-agent" />
<input type="hidden" name="type" value="monitoring" />
<input type="hidden" name="version" value="latest" />
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("Install Vito Agent") }}
</h2>
<div class="mt-6">
<x-alert-warning>
Vito Agent is only works if you are running your Vito instance on a cloud not local!
</x-alert-warning>
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}
</x-secondary-button>
<x-primary-button id="btn-vito-agent" hx-disable class="ml-3">
{{ __("Install") }}
</x-primary-button>
</div>
</form>
</x-modal>
@endpush

View File

@ -15,10 +15,10 @@ class="relative flex h-auto flex-col items-center justify-between space-y-3 roun
@include("services.partials.status", ["status" => $service->status])
</div>
<div class="space-y-3 p-5">
<div class="flex items-center justify-center">
<div class="mt-5 flex items-center justify-center">
<img
src="{{ asset("static/images/" . $service->name . ".svg") }}"
class="h-20 w-20"
class="h-[70px] w-[70px]"
alt=""
/>
</div>

View File

@ -11,7 +11,7 @@
@endif
@if ($status == \App\Enums\ServiceStatus::UNINSTALLING)
<x-status status="danger">{{ $status }}</x-status>
<x-status status="warning">{{ $status }}</x-status>
@endif
@if ($status == \App\Enums\ServiceStatus::FAILED)

View File

@ -1,42 +0,0 @@
<x-icon-button
data-tooltip="Restart Service"
class="cursor-pointer"
href="{{ route('servers.services.restart', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-arrow-path" class="h-5 w-5" />
</x-icon-button>
<x-icon-button
:disabled="$service->status != \App\Enums\ServiceStatus::STOPPED"
data-tooltip="Start Service"
class="cursor-pointer"
href="{{ route('servers.services.start', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-play" class="h-5 w-5 text-green-400" />
</x-icon-button>
<x-icon-button
data-tooltip="Stop Service"
:disabled="$service->status != \App\Enums\ServiceStatus::READY"
class="cursor-pointer"
href="{{ route('servers.services.stop', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-stop" class="h-5 w-5 text-red-400" />
</x-icon-button>
<x-icon-button
:disabled="$service->status != \App\Enums\ServiceStatus::DISABLED"
data-tooltip="Enable Service"
class="cursor-pointer"
href="{{ route('servers.services.enable', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-check" class="h-5 w-5" />
</x-icon-button>
<x-icon-button
data-tooltip="Disable Service"
class="cursor-pointer"
href="{{ route('servers.services.disable', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-no-symbol" class="h-5 w-5" />
</x-icon-button>

View File

@ -0,0 +1,7 @@
<x-icon-button
data-tooltip="Disable Service"
class="cursor-pointer"
href="{{ route('servers.services.disable', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-no-symbol" class="h-5 w-5" />
</x-icon-button>

View File

@ -0,0 +1,8 @@
<x-icon-button
:disabled="$service->status != \App\Enums\ServiceStatus::DISABLED"
data-tooltip="Enable Service"
class="cursor-pointer"
href="{{ route('servers.services.enable', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-check" class="h-5 w-5" />
</x-icon-button>

View File

@ -0,0 +1,7 @@
<x-icon-button
data-tooltip="Restart Service"
class="cursor-pointer"
href="{{ route('servers.services.restart', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-arrow-path" class="h-5 w-5" />
</x-icon-button>

View File

@ -0,0 +1,8 @@
<x-icon-button
:disabled="$service->status != \App\Enums\ServiceStatus::STOPPED"
data-tooltip="Start Service"
class="cursor-pointer"
href="{{ route('servers.services.start', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-play" class="h-5 w-5 text-green-400" />
</x-icon-button>

View File

@ -0,0 +1,8 @@
<x-icon-button
data-tooltip="Stop Service"
:disabled="$service->status != \App\Enums\ServiceStatus::READY"
class="cursor-pointer"
href="{{ route('servers.services.stop', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-stop" class="h-5 w-5 text-red-400" />
</x-icon-button>

View File

@ -0,0 +1,33 @@
<x-icon-button
data-tooltip="Uninstall Service"
class="cursor-pointer"
x-on:click="$dispatch('open-modal', 'uninstall-{{ $service->id }}')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>
@push("modals")
<x-modal name="uninstall-{{ $service->id }}">
<form
id="uninstall-{{ $service->id }}-form"
hx-post="{{ route("servers.services.uninstall", ["server" => $server, "service" => $service]) }}"
hx-target="#uninstall-{{ $service->id }}-form"
hx-select="#uninstall-{{ $service->id }}-form"
hx-swap="outerHTML"
class="p-6"
>
@csrf
@method("delete")
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">Confirm</h2>
<p>Are you sure that you want to uninstall this service?</p>
@error("service")
<x-input-error class="mt-2" :messages="$message" />
@enderror
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">Cancel</x-secondary-button>
<x-danger-button class="ml-3" hx-disable>Confirm</x-danger-button>
</div>
</form>
</x-modal>
@endpush

View File

@ -1,9 +1,13 @@
<?php
// git hook
use App\Http\Controllers\API\AgentController;
use App\Http\Controllers\API\GitHookController;
use App\Http\Controllers\API\HealthController;
use Illuminate\Support\Facades\Route;
Route::get('health', HealthController::class)->name('api.health');
Route::any('git-hooks', GitHookController::class)->name('api.git-hooks');
Route::post('servers/{server}/agent/{id}', AgentController::class)->name('api.servers.agent');

View File

@ -7,6 +7,7 @@
use App\Http\Controllers\DatabaseController;
use App\Http\Controllers\DatabaseUserController;
use App\Http\Controllers\FirewallController;
use App\Http\Controllers\MetricController;
use App\Http\Controllers\PHPController;
use App\Http\Controllers\QueueController;
use App\Http\Controllers\ServerController;
@ -127,6 +128,10 @@
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::post('/{server}/services/install', [ServiceController::class, 'install'])->name('servers.services.install');
Route::delete('/{server}/services/{service}/uninstall', [ServiceController::class, 'uninstall'])->name('servers.services.uninstall');
// metrics
Route::get('/{server}/metrics', [MetricController::class, 'index'])->name('servers.metrics');
// console
Route::get('/{server}/console', [ConsoleController::class, 'index'])->name('servers.console');

View File

@ -192,7 +192,6 @@ touch /home/${V_USERNAME}/.logs/workers/worker.log
echo "${V_WORKER_CONFIG}" | tee /etc/supervisor/conf.d/worker.conf
supervisorctl reread
supervisorctl update
supervisorctl start worker:*
# setup cronjobs
echo "* * * * * cd /home/${V_USERNAME}/vito && php artisan schedule:run >> /dev/null 2>&1" | sudo -u ${V_USERNAME} crontab -
@ -203,6 +202,9 @@ chown -R ${V_USERNAME}:${V_USERNAME} /home/${V_USERNAME}
# cache
php artisan config:cache
# start worker
supervisorctl start worker:*
# print info
echo "🎉 Congratulations!"
echo "✅ SSH User: ${V_USERNAME}"

View File

@ -0,0 +1,40 @@
<?php
namespace Tests\Feature;
use App\Enums\ServiceStatus;
use App\Models\Service;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class MetricsTest extends TestCase
{
use RefreshDatabase;
public function test_visit_metrics(): void
{
$this->actingAs($this->user);
Service::factory()->create([
'server_id' => $this->server->id,
'name' => 'vito-agent',
'type' => 'monitoring',
'version' => 'latest',
'status' => ServiceStatus::READY,
]);
$this->get(route('servers.metrics', ['server' => $this->server]))
->assertSee('CPU Load')
->assertSee('Memory Usage')
->assertSee('Disk Usage')
->assertSee('Resource Usage');
}
public function test_cannot_visit_metrics(): void
{
$this->actingAs($this->user);
$this->get(route('servers.metrics', ['server' => $this->server]))
->assertRedirect(route('servers.services', ['server' => $this->server]));
}
}

View File

@ -4,7 +4,10 @@
use App\Enums\ServiceStatus;
use App\Facades\SSH;
use App\Models\Server;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class ServicesTest extends TestCase
@ -22,6 +25,7 @@ public function test_see_services_list(): void
->assertSee('php')
->assertSee('supervisor')
->assertSee('redis')
->assertSee('vito-agent')
->assertSee('ufw');
}
@ -235,6 +239,45 @@ public function test_failed_to_disable_service(string $name): void
$this->assertEquals(ServiceStatus::FAILED, $service->status);
}
/**
* @dataProvider installData
*/
public function test_install_service(string $name, string $type, string $version): void
{
Http::fake([
'https://api.github.com/repos/vito/vito-agent/releases/latest' => Http::response([
'tag_name' => '0.1.0',
]),
]);
SSH::fake('Active: active');
$this->actingAs($this->user);
$server = Server::factory()->create([
'user_id' => $this->user->id,
'project_id' => $this->user->current_project_id,
]);
$keys = $server->sshKey();
if (! File::exists($keys['public_key_path']) || ! File::exists($keys['private_key_path'])) {
$server->provider()->generateKeyPair();
}
$this->post(route('servers.services.install', [
'server' => $server,
]), [
'name' => $name,
'type' => $type,
'version' => $version,
])->assertSessionDoesntHaveErrors();
$this->assertDatabaseHas('services', [
'server_id' => $server->id,
'name' => $name,
'type' => $type,
'status' => ServiceStatus::READY,
]);
}
public static function data(): array
{
return [
@ -247,4 +290,50 @@ public static function data(): array
['mysql'],
];
}
public static function installData(): array
{
return [
[
'nginx',
'webserver',
'latest',
],
[
'php',
'php',
'7.4',
],
[
'supervisor',
'process_manager',
'latest',
],
[
'redis',
'memory_database',
'latest',
],
[
'mysql',
'database',
'8.0',
],
[
'mariadb',
'database',
'10.4',
],
[
'postgresql',
'database',
'16',
],
[
'vito-agent',
'monitoring',
'latest',
],
];
}
}

View File

@ -0,0 +1,156 @@
<?php
namespace Tests\Unit\Actions\Service;
use App\Actions\Service\Install;
use App\Enums\ServiceStatus;
use App\Facades\SSH;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;
class InstallTest extends TestCase
{
use RefreshDatabase;
public function test_install_vito_agent(): void
{
SSH::fake('Active: active');
Http::fake([
'https://api.github.com/repos/vitodeploy/agent/tags' => Http::response([['name' => '0.1.0']]),
]);
$service = app(Install::class)->install($this->server, [
'type' => 'monitoring',
'name' => 'vito-agent',
'version' => 'latest',
]);
$this->assertDatabaseHas('services', [
'server_id' => $this->server->id,
'name' => 'vito-agent',
'type' => 'monitoring',
'version' => '0.1.0',
'status' => ServiceStatus::READY,
]);
$this->assertNotNull($service->type_data);
}
public function test_install_vito_agent_failed(): void
{
$this->expectExceptionMessage('Failed to fetch tags');
SSH::fake('Active: inactive');
Http::fake([
'https://api.github.com/repos/vitodeploy/agent/tags' => Http::response([]),
]);
app(Install::class)->install($this->server, [
'type' => 'monitoring',
'name' => 'vito-agent',
'version' => 'latest',
]);
}
public function test_install_nginx(): void
{
$this->server->webserver()->delete();
SSH::fake('Active: active');
$service = app(Install::class)->install($this->server, [
'type' => 'webserver',
'name' => 'nginx',
'version' => 'latest',
]);
$this->assertDatabaseHas('services', [
'server_id' => $this->server->id,
'name' => 'nginx',
'type' => 'webserver',
'version' => 'latest',
'status' => ServiceStatus::READY,
]);
$this->assertNotNull($service->type_data);
}
public function test_install_mysql(): void
{
$this->server->database()->delete();
SSH::fake('Active: active');
$service = app(Install::class)->install($this->server, [
'type' => 'database',
'name' => 'mysql',
'version' => '8.0',
]);
$this->assertDatabaseHas('services', [
'server_id' => $this->server->id,
'name' => 'mysql',
'type' => 'database',
'version' => '8.0',
'status' => ServiceStatus::READY,
]);
$this->assertNotNull($service->type_data);
}
public function test_install_mysql_failed(): void
{
$this->expectException(ValidationException::class);
app(Install::class)->install($this->server, [
'type' => 'database',
'name' => 'mysql',
'version' => '8.0',
]);
}
public function test_install_supervisor(): void
{
$this->server->processManager()->delete();
SSH::fake('Active: active');
$service = app(Install::class)->install($this->server, [
'type' => 'process_manager',
'name' => 'supervisor',
'version' => 'latest',
]);
$this->assertDatabaseHas('services', [
'server_id' => $this->server->id,
'name' => 'supervisor',
'type' => 'process_manager',
'version' => 'latest',
'status' => ServiceStatus::READY,
]);
$this->assertNotNull($service->type_data);
}
public function test_install_redis(): void
{
$this->server->memoryDatabase()->delete();
SSH::fake('Active: active');
$service = app(Install::class)->install($this->server, [
'type' => 'memory_database',
'name' => 'redis',
'version' => 'latest',
]);
$this->assertDatabaseHas('services', [
'server_id' => $this->server->id,
'name' => 'redis',
'type' => 'memory_database',
'version' => 'latest',
'status' => ServiceStatus::READY,
]);
$this->assertNotNull($service->type_data);
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace Tests\Unit\Actions\Service;
use App\Actions\Service\Uninstall;
use App\Enums\ServiceStatus;
use App\Facades\SSH;
use App\Models\Database;
use App\Models\Queue;
use App\Models\Service;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;
class UninstallTest extends TestCase
{
use RefreshDatabase;
public function test_uninstall_vito_agent(): void
{
SSH::fake();
Service::factory()->create([
'server_id' => $this->server->id,
'name' => 'vito-agent',
'type' => 'monitoring',
'version' => 'latest',
'status' => ServiceStatus::READY,
]);
app(Uninstall::class)->uninstall($this->server->monitoring());
$this->assertDatabaseMissing('services', [
'server_id' => $this->server->id,
'name' => 'vito-agent',
'type' => 'monitoring',
'version' => 'latest',
'status' => ServiceStatus::READY,
]);
}
/**
* Cannot uninstall nginx because some sites using it
*/
public function test_cannot_uninstall_nginx(): void
{
SSH::fake();
$this->expectException(ValidationException::class);
app(Uninstall::class)->uninstall($this->server->webserver());
}
/**
* Cannot uninstall mysql because some databases exist
*/
public function test_cannot_uninstall_mysql(): void
{
SSH::fake();
Database::factory()->create([
'server_id' => $this->server->id,
]);
$this->expectException(ValidationException::class);
app(Uninstall::class)->uninstall($this->server->database());
}
/**
* Cannot uninstall supervisor because some queues exist
*/
public function test_cannot_uninstall_supervisor(): void
{
SSH::fake();
Queue::factory()->create([
'server_id' => $this->server->id,
'site_id' => $this->site->id,
]);
$this->expectException(ValidationException::class);
app(Uninstall::class)->uninstall($this->server->processManager());
}
}