diff --git a/app/Actions/Monitoring/GetMetrics.php b/app/Actions/Monitoring/GetMetrics.php new file mode 100644 index 0000000..e393064 --- /dev/null +++ b/app/Actions/Monitoring/GetMetrics.php @@ -0,0 +1,150 @@ +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(); + } + } +} diff --git a/app/Actions/PHP/ChangeDefaultCli.php b/app/Actions/PHP/ChangeDefaultCli.php index ff60476..79a311f 100644 --- a/app/Actions/PHP/ChangeDefaultCli.php +++ b/app/Actions/PHP/ChangeDefaultCli.php @@ -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]); diff --git a/app/Actions/PHP/GetPHPIni.php b/app/Actions/PHP/GetPHPIni.php index ef717d6..d0dd1be 100644 --- a/app/Actions/PHP/GetPHPIni.php +++ b/app/Actions/PHP/GetPHPIni.php @@ -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()] diff --git a/app/Actions/PHP/InstallPHPExtension.php b/app/Actions/PHP/InstallPHPExtension.php index eba6a41..adf3a43 100755 --- a/app/Actions/PHP/InstallPHPExtension.php +++ b/app/Actions/PHP/InstallPHPExtension.php @@ -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; diff --git a/app/Actions/Service/Create.php b/app/Actions/Service/Install.php similarity index 71% rename from app/Actions/Service/Create.php rename to app/Actions/Service/Install.php index bfb75cc..51d332b 100644 --- a/app/Actions/Service/Create.php +++ b/app/Actions/Service/Install.php @@ -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(); diff --git a/app/Actions/Service/Uninstall.php b/app/Actions/Service/Uninstall.php new file mode 100644 index 0000000..6528647 --- /dev/null +++ b/app/Actions/Service/Uninstall.php @@ -0,0 +1,28 @@ + $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'); + } +} diff --git a/app/Http/Controllers/API/AgentController.php b/app/Http/Controllers/API/AgentController.php new file mode 100644 index 0000000..2271d8c --- /dev/null +++ b/app/Http/Controllers/API/AgentController.php @@ -0,0 +1,36 @@ +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(); + } +} diff --git a/app/Http/Controllers/MetricController.php b/app/Http/Controllers/MetricController.php new file mode 100644 index 0000000..0c7f0db --- /dev/null +++ b/app/Http/Controllers/MetricController.php @@ -0,0 +1,27 @@ +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()), + ]); + } +} diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php index 6969126..6e696ba 100644 --- a/app/Http/Controllers/ServiceController.php +++ b/app/Http/Controllers/ServiceController.php @@ -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!'); diff --git a/app/Models/Metric.php b/app/Models/Metric.php new file mode 100644 index 0000000..357507a --- /dev/null +++ b/app/Models/Metric.php @@ -0,0 +1,53 @@ + '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); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 7e63f01..c53cc41 100755 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -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 */ diff --git a/app/Models/Service.php b/app/Models/Service.php index 4516ae2..39d0d8c 100755 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -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); diff --git a/app/SSH/OS/OS.php b/app/SSH/OS/OS.php index 4f3d886..ae07ce9 100644 --- a/app/SSH/OS/OS.php +++ b/app/SSH/OS/OS.php @@ -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' + ); + } } diff --git a/app/SSH/OS/scripts/cleanup.sh b/app/SSH/OS/scripts/cleanup.sh new file mode 100644 index 0000000..1290b67 --- /dev/null +++ b/app/SSH/OS/scripts/cleanup.sh @@ -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." diff --git a/app/SSH/Services/AbstractService.php b/app/SSH/Services/AbstractService.php new file mode 100644 index 0000000..1b58ccc --- /dev/null +++ b/app/SSH/Services/AbstractService.php @@ -0,0 +1,42 @@ +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, diff --git a/app/SSH/Services/Database/scripts/mariadb/install-10.3.sh b/app/SSH/Services/Database/scripts/mariadb/install-10.3.sh index 8a946d0..06ede44 100755 --- a/app/SSH/Services/Database/scripts/mariadb/install-10.3.sh +++ b/app/SSH/Services/Database/scripts/mariadb/install-10.3.sh @@ -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 diff --git a/app/SSH/Services/Database/scripts/mariadb/install-10.4.sh b/app/SSH/Services/Database/scripts/mariadb/install-10.4.sh index ec0dc75..6e1a075 100755 --- a/app/SSH/Services/Database/scripts/mariadb/install-10.4.sh +++ b/app/SSH/Services/Database/scripts/mariadb/install-10.4.sh @@ -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 diff --git a/app/SSH/Services/Database/scripts/mariadb/uninstall.sh b/app/SSH/Services/Database/scripts/mariadb/uninstall.sh new file mode 100644 index 0000000..e0e51b1 --- /dev/null +++ b/app/SSH/Services/Database/scripts/mariadb/uninstall.sh @@ -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 diff --git a/app/SSH/Services/Database/scripts/mysql/install-5.7.sh b/app/SSH/Services/Database/scripts/mysql/install-5.7.sh index 7348d94..29828bd 100755 --- a/app/SSH/Services/Database/scripts/mysql/install-5.7.sh +++ b/app/SSH/Services/Database/scripts/mysql/install-5.7.sh @@ -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 diff --git a/app/SSH/Services/Database/scripts/mysql/install-8.0.sh b/app/SSH/Services/Database/scripts/mysql/install-8.0.sh index 0df07a7..eb6e605 100755 --- a/app/SSH/Services/Database/scripts/mysql/install-8.0.sh +++ b/app/SSH/Services/Database/scripts/mysql/install-8.0.sh @@ -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 diff --git a/app/SSH/Services/Database/scripts/mysql/uninstall.sh b/app/SSH/Services/Database/scripts/mysql/uninstall.sh new file mode 100755 index 0000000..8835b52 --- /dev/null +++ b/app/SSH/Services/Database/scripts/mysql/uninstall.sh @@ -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 diff --git a/app/SSH/Services/Database/scripts/postgresql/uninstall.sh b/app/SSH/Services/Database/scripts/postgresql/uninstall.sh new file mode 100644 index 0000000..a35fe93 --- /dev/null +++ b/app/SSH/Services/Database/scripts/postgresql/uninstall.sh @@ -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 diff --git a/app/SSH/Services/Firewall/AbstractFirewall.php b/app/SSH/Services/Firewall/AbstractFirewall.php index 19b4724..e0358e5 100755 --- a/app/SSH/Services/Firewall/AbstractFirewall.php +++ b/app/SSH/Services/Firewall/AbstractFirewall.php @@ -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; - } } diff --git a/app/SSH/Services/Firewall/Ufw.php b/app/SSH/Services/Firewall/Ufw.php index 471b58a..a5132fc 100755 --- a/app/SSH/Services/Firewall/Ufw.php +++ b/app/SSH/Services/Firewall/Ufw.php @@ -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 diff --git a/app/SSH/Services/PHP/PHP.php b/app/SSH/Services/PHP/PHP.php index 21eb4ab..ac65702 100644 --- a/app/SSH/Services/PHP/PHP.php +++ b/app/SSH/Services/PHP/PHP.php @@ -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 diff --git a/app/SSH/Services/ProcessManager/AbstractProcessManager.php b/app/SSH/Services/ProcessManager/AbstractProcessManager.php index 229ba69..c67b5b8 100644 --- a/app/SSH/Services/ProcessManager/AbstractProcessManager.php +++ b/app/SSH/Services/ProcessManager/AbstractProcessManager.php @@ -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.'); + } + }, + ], + ]; } } diff --git a/app/SSH/Services/ProcessManager/Supervisor.php b/app/SSH/Services/ProcessManager/Supervisor.php index baf0ed4..abc5074 100644 --- a/app/SSH/Services/ProcessManager/Supervisor.php +++ b/app/SSH/Services/ProcessManager/Supervisor.php @@ -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(); } /** diff --git a/app/SSH/Services/ProcessManager/scripts/supervisor/uninstall-supervisor.sh b/app/SSH/Services/ProcessManager/scripts/supervisor/uninstall-supervisor.sh new file mode 100755 index 0000000..e0c48ee --- /dev/null +++ b/app/SSH/Services/ProcessManager/scripts/supervisor/uninstall-supervisor.sh @@ -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 diff --git a/app/SSH/Services/Redis/Redis.php b/app/SSH/Services/Redis/Redis.php index 1a1296b..552f219 100644 --- a/app/SSH/Services/Redis/Redis.php +++ b/app/SSH/Services/Redis/Redis.php @@ -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(); } } diff --git a/app/SSH/Services/Redis/scripts/uninstall.sh b/app/SSH/Services/Redis/scripts/uninstall.sh new file mode 100755 index 0000000..15aa35b --- /dev/null +++ b/app/SSH/Services/Redis/scripts/uninstall.sh @@ -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 diff --git a/app/SSH/Services/ServiceInterface.php b/app/SSH/Services/ServiceInterface.php index 23646d0..b7b7822 100644 --- a/app/SSH/Services/ServiceInterface.php +++ b/app/SSH/Services/ServiceInterface.php @@ -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; } diff --git a/app/SSH/Services/VitoAgent/VitoAgent.php b/app/SSH/Services/VitoAgent/VitoAgent.php new file mode 100644 index 0000000..96a6b84 --- /dev/null +++ b/app/SSH/Services/VitoAgent/VitoAgent.php @@ -0,0 +1,85 @@ + [ + 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(); + } +} diff --git a/app/SSH/Services/VitoAgent/scripts/install.sh b/app/SSH/Services/VitoAgent/scripts/install.sh new file mode 100644 index 0000000..6c7ae32 --- /dev/null +++ b/app/SSH/Services/VitoAgent/scripts/install.sh @@ -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" diff --git a/app/SSH/Services/VitoAgent/scripts/uninstall.sh b/app/SSH/Services/VitoAgent/scripts/uninstall.sh new file mode 100644 index 0000000..03905ad --- /dev/null +++ b/app/SSH/Services/VitoAgent/scripts/uninstall.sh @@ -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" diff --git a/app/SSH/Services/Webserver/AbstractWebserver.php b/app/SSH/Services/Webserver/AbstractWebserver.php index 297b86f..4864f90 100755 --- a/app/SSH/Services/Webserver/AbstractWebserver.php +++ b/app/SSH/Services/Webserver/AbstractWebserver.php @@ -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) - { - } } diff --git a/app/SSH/Services/Webserver/Nginx.php b/app/SSH/Services/Webserver/Nginx.php index 5b2bf82..8e39d3e 100755 --- a/app/SSH/Services/Webserver/Nginx.php +++ b/app/SSH/Services/Webserver/Nginx.php @@ -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 diff --git a/app/SSH/Services/Webserver/scripts/nginx/uninstall-nginx.sh b/app/SSH/Services/Webserver/scripts/nginx/uninstall-nginx.sh new file mode 100755 index 0000000..ec811bf --- /dev/null +++ b/app/SSH/Services/Webserver/scripts/nginx/uninstall-nginx.sh @@ -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 diff --git a/app/ServerTypes/AbstractType.php b/app/ServerTypes/AbstractType.php index bf5792a..2143d76 100755 --- a/app/ServerTypes/AbstractType.php +++ b/app/ServerTypes/AbstractType.php @@ -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'); diff --git a/app/ServerTypes/Regular.php b/app/ServerTypes/Regular.php index 9ba276a..fa5cddb 100755 --- a/app/ServerTypes/Regular.php +++ b/app/ServerTypes/Regular.php @@ -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', diff --git a/app/Support/helpers.php b/app/Support/helpers.php index fd46659..f55450d 100755 --- a/app/Support/helpers.php +++ b/app/Support/helpers.php @@ -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); +} diff --git a/config/core.php b/config/core.php index 02843c8..5a6afec 100755 --- a/config/core.php +++ b/config/core.php @@ -1,35 +1,5 @@ [ - 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' => [ diff --git a/database/factories/MetricFactory.php b/database/factories/MetricFactory.php new file mode 100644 index 0000000..8f2bdb2 --- /dev/null +++ b/database/factories/MetricFactory.php @@ -0,0 +1,22 @@ + 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), + ]; + } +} diff --git a/database/migrations/2024_04_08_212940_create_metrics_table.php b/database/migrations/2024_04_08_212940_create_metrics_table.php new file mode 100644 index 0000000..ab8ab06 --- /dev/null +++ b/database/migrations/2024_04_08_212940_create_metrics_table.php @@ -0,0 +1,37 @@ +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'); + } +}; diff --git a/database/seeders/MetricsSeeder.php b/database/seeders/MetricsSeeder.php new file mode 100644 index 0000000..826f6bc --- /dev/null +++ b/database/seeders/MetricsSeeder.php @@ -0,0 +1,32 @@ +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, + ]); + } + } +} diff --git a/package-lock.json b/package-lock.json index 4434558..63f7ebd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 638b03d..8261e80 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/public/static/images/vito-agent.svg b/public/static/images/vito-agent.svg new file mode 100644 index 0000000..ddfda2f --- /dev/null +++ b/public/static/images/vito-agent.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/js/app.js b/resources/js/app.js index 200621f..85ad3ab 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -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', { diff --git a/resources/views/components/chart.blade.php b/resources/views/components/chart.blade.php new file mode 100644 index 0000000..fb40159 --- /dev/null +++ b/resources/views/components/chart.blade.php @@ -0,0 +1,103 @@ +@props([ + "id", + "type", + "title", + "color", + "sets", + "categories", + "toolbar" => false, +]) + +
+
{{ $title }}
+
+
+ +
diff --git a/resources/views/components/heroicons/o-calendar.blade.php b/resources/views/components/heroicons/o-calendar.blade.php new file mode 100644 index 0000000..b659dc9 --- /dev/null +++ b/resources/views/components/heroicons/o-calendar.blade.php @@ -0,0 +1,14 @@ + + + diff --git a/resources/views/components/heroicons/o-chart-bar.blade.php b/resources/views/components/heroicons/o-chart-bar.blade.php new file mode 100644 index 0000000..6ff0ad2 --- /dev/null +++ b/resources/views/components/heroicons/o-chart-bar.blade.php @@ -0,0 +1,14 @@ + + + diff --git a/resources/views/layouts/sidebar.blade.php b/resources/views/layouts/sidebar.blade.php index 3868e22..dbced91 100644 --- a/resources/views/layouts/sidebar.blade.php +++ b/resources/views/layouts/sidebar.blade.php @@ -37,7 +37,7 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g @endif - @if ($server->database()) + @if ($server->database()?->status == \App\Enums\ServiceStatus::READY)
  • $server])" - :active="request()->routeIs('servers.php')" - > - - - {{ __("PHP") }} - - -
  • - @endif +
  • + + + + {{ __("PHP") }} + + +
  • @if ($server->firewall())
  • @@ -116,6 +114,20 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g
  • + @if ($server->monitoring()) +
  • + + + + {{ __("Metrics") }} + + +
  • + @endif +
  • @endif - @if ($site->hasFeature(SiteFeature::QUEUES)) + @if ($site->hasFeature(SiteFeature::QUEUES) && $site->server->processManager()?->status == \App\Enums\ServiceStatus::READY) + {{ $server->name }} - Metrics + + @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 + +
    + + + + + +
    + +
    + +
    + diff --git a/resources/views/metrics/partials/filter.blade.php b/resources/views/metrics/partials/filter.blade.php new file mode 100644 index 0000000..af9d898 --- /dev/null +++ b/resources/views/metrics/partials/filter.blade.php @@ -0,0 +1,76 @@ +
    + + +
    +
    +
    +
    + +
    +
    + + + 10 Minutes + + + 30 Minutes + + + 1 Hour + + + 12 Hours + + + 1 Day + + + 7 Days + + Custom + +
    + +
    $server, "period" => "custom"]) }}" + > + +
    +
    +
    + +
    + +
    + to +
    +
    + +
    + + +
    +
    + {{ __("Filter") }} +
    +
    diff --git a/resources/views/servers/partials/create-server.blade.php b/resources/views/servers/partials/create-server.blade.php index 630858b..c32b837 100644 --- a/resources/views/servers/partials/create-server.blade.php +++ b/resources/views/servers/partials/create-server.blade.php @@ -239,6 +239,7 @@ class="mt-1 block w-full"
    + @foreach (config("core.php_versions") as $p)
    -
    +
    name . ".svg") }}" - class="h-20 w-20" + class="h-[70px] w-[70px]" alt="" />
    diff --git a/resources/views/services/partials/status.blade.php b/resources/views/services/partials/status.blade.php index fa2812e..6fa62ef 100644 --- a/resources/views/services/partials/status.blade.php +++ b/resources/views/services/partials/status.blade.php @@ -11,7 +11,7 @@ @endif @if ($status == \App\Enums\ServiceStatus::UNINSTALLING) - {{ $status }} + {{ $status }} @endif @if ($status == \App\Enums\ServiceStatus::FAILED) diff --git a/resources/views/services/partials/unit-actions.blade.php b/resources/views/services/partials/unit-actions.blade.php deleted file mode 100644 index b7d03ea..0000000 --- a/resources/views/services/partials/unit-actions.blade.php +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/resources/views/services/partials/unit-actions/disable.blade.php b/resources/views/services/partials/unit-actions/disable.blade.php new file mode 100644 index 0000000..8b71fc6 --- /dev/null +++ b/resources/views/services/partials/unit-actions/disable.blade.php @@ -0,0 +1,7 @@ + + + diff --git a/resources/views/services/partials/unit-actions/enable.blade.php b/resources/views/services/partials/unit-actions/enable.blade.php new file mode 100644 index 0000000..8ae0523 --- /dev/null +++ b/resources/views/services/partials/unit-actions/enable.blade.php @@ -0,0 +1,8 @@ + + + diff --git a/resources/views/services/partials/unit-actions/restart.blade.php b/resources/views/services/partials/unit-actions/restart.blade.php new file mode 100644 index 0000000..25463e2 --- /dev/null +++ b/resources/views/services/partials/unit-actions/restart.blade.php @@ -0,0 +1,7 @@ + + + diff --git a/resources/views/services/partials/unit-actions/start.blade.php b/resources/views/services/partials/unit-actions/start.blade.php new file mode 100644 index 0000000..5a33531 --- /dev/null +++ b/resources/views/services/partials/unit-actions/start.blade.php @@ -0,0 +1,8 @@ + + + diff --git a/resources/views/services/partials/unit-actions/stop.blade.php b/resources/views/services/partials/unit-actions/stop.blade.php new file mode 100644 index 0000000..6c55d3b --- /dev/null +++ b/resources/views/services/partials/unit-actions/stop.blade.php @@ -0,0 +1,8 @@ + + + diff --git a/resources/views/services/partials/unit-actions/uninstall.blade.php b/resources/views/services/partials/unit-actions/uninstall.blade.php new file mode 100644 index 0000000..921f929 --- /dev/null +++ b/resources/views/services/partials/unit-actions/uninstall.blade.php @@ -0,0 +1,33 @@ + + + +@push("modals") + +
    $server, "service" => $service]) }}" + hx-target="#uninstall-{{ $service->id }}-form" + hx-select="#uninstall-{{ $service->id }}-form" + hx-swap="outerHTML" + class="p-6" + > + @csrf + @method("delete") +

    Confirm

    +

    Are you sure that you want to uninstall this service?

    + + @error("service") + + @enderror + +
    + Cancel + Confirm +
    + +
    +@endpush diff --git a/routes/api.php b/routes/api.php index 7e5a574..377de32 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,9 +1,13 @@ 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'); diff --git a/routes/server.php b/routes/server.php index 5789068..c32921e 100644 --- a/routes/server.php +++ b/routes/server.php @@ -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'); diff --git a/scripts/install.sh b/scripts/install.sh index 19612f6..f73d1e9 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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}" diff --git a/tests/Feature/MetricsTest.php b/tests/Feature/MetricsTest.php new file mode 100644 index 0000000..cfb1cc1 --- /dev/null +++ b/tests/Feature/MetricsTest.php @@ -0,0 +1,40 @@ +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])); + } +} diff --git a/tests/Feature/ServicesTest.php b/tests/Feature/ServicesTest.php index c7a8586..20a9509 100644 --- a/tests/Feature/ServicesTest.php +++ b/tests/Feature/ServicesTest.php @@ -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', + ], + ]; + } } diff --git a/tests/Unit/Actions/Service/InstallTest.php b/tests/Unit/Actions/Service/InstallTest.php new file mode 100644 index 0000000..a1efeda --- /dev/null +++ b/tests/Unit/Actions/Service/InstallTest.php @@ -0,0 +1,156 @@ + 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); + } +} diff --git a/tests/Unit/Actions/Service/UninstallTest.php b/tests/Unit/Actions/Service/UninstallTest.php new file mode 100644 index 0000000..1f3173a --- /dev/null +++ b/tests/Unit/Actions/Service/UninstallTest.php @@ -0,0 +1,86 @@ +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()); + } +}