From 2f4f9170324cca1217c5c84cd4e5af9af85ff39c Mon Sep 17 00:00:00 2001 From: JonatanRek Date: Fri, 16 Aug 2024 18:20:45 +0200 Subject: [PATCH] Proxxgers + ZBX hosts Sync --- app/Http/Controllers/HostController.php | 25 ++++ .../Controllers/MaintenanceController.php | 2 + app/Jobs/ScheduleNextMaintenance.php | 8 +- app/Livewire/Host/DataTable.php | 5 + app/Models/Host.php | 1 + app/Models/MaintenanceHistory.php | 7 + app/Observers/MaintenanceHistoryObserver.php | 18 +++ app/Services/ZabbixService.php | 130 ++++++++++++++++++ ...trol_hash_to_maintenance_history_table.php | 25 ++++ ..._154630_add_display_name_to_host_table.php | 25 ++++ resources/views/hosts/index.blade.php | 13 +- routes/web.php | 6 + storage/app/.gitignore | 3 - storage/app/public/.gitignore | 2 - storage/app/public/images/logo.png | Bin 0 -> 7084 bytes 15 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 app/Observers/MaintenanceHistoryObserver.php create mode 100644 app/Services/ZabbixService.php create mode 100644 database/migrations/2024_08_16_150626_add_control_hash_to_maintenance_history_table.php create mode 100644 database/migrations/2024_08_16_154630_add_display_name_to_host_table.php delete mode 100644 storage/app/.gitignore delete mode 100644 storage/app/public/.gitignore create mode 100644 storage/app/public/images/logo.png diff --git a/app/Http/Controllers/HostController.php b/app/Http/Controllers/HostController.php index 3bff2ef..4a640a9 100644 --- a/app/Http/Controllers/HostController.php +++ b/app/Http/Controllers/HostController.php @@ -2,10 +2,35 @@ namespace App\Http\Controllers; +use App\Models\Host; +use App\Services\ZabbixService; +use Carbon\Carbon; + class HostController extends BaseController { public function index() { return view('hosts.index'); } + + public function sync() + { + $zabbix = new ZabbixService("https://zabbix.itego.cz"); + $zabbix->connect("spaninger", "*"); + + foreach ( $zabbix->maintenances([ "selectTimeperiods" => "extend" ]) as $maintennace ) { + if(!Carbon::createFromTimestamp($maintennace['active_till'])->isPast()){ + dd(Carbon::createFromTimestamp($maintennace['active_till'])); + } + } + + foreach ($zabbix->hosts() as $host) { + Host::updateOrCreate([ + "hostname" => $host['host'], + ], [ + "display_name" => $host['name'], + ]); + } + return redirect()->back(); + } } diff --git a/app/Http/Controllers/MaintenanceController.php b/app/Http/Controllers/MaintenanceController.php index 6dd41da..304b9a8 100644 --- a/app/Http/Controllers/MaintenanceController.php +++ b/app/Http/Controllers/MaintenanceController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Models\MaintenanceHistory; +use App\Services\ZabbixService; use Carbon\Carbon; use Illuminate\Http\Request; @@ -31,6 +32,7 @@ class MaintenanceController extends BaseController public function plannedDetailPut(Request $request, MaintenanceHistory $maintenance_history) { + if (!empty($maintenance_history->finished_at)) { abort(404); } diff --git a/app/Jobs/ScheduleNextMaintenance.php b/app/Jobs/ScheduleNextMaintenance.php index abf36b5..7f9b220 100644 --- a/app/Jobs/ScheduleNextMaintenance.php +++ b/app/Jobs/ScheduleNextMaintenance.php @@ -34,8 +34,14 @@ class ScheduleNextMaintenance implements ShouldQueue $maintenances = Maintenance::all(); foreach ($maintenances as $maintenance) { $cron = new CronCronExpression($maintenance->schedule); + $nextRunTime = Carbon::createFromTimestamp($cron->getNext()); + + if(MaintenanceHistory::where('hash', md5($maintenance->id . $nextRunTime))->first() === null){ + continue; + }; + $maintenancePlanned = $maintenance->history()->create([ - 'start_at' => Carbon::createFromTimestamp($cron->getNext()), + 'start_at' => $nextRunTime, 'guestor_id' => $maintenance->guestor_id, ]); $maintenancePlanned->refresh(); diff --git a/app/Livewire/Host/DataTable.php b/app/Livewire/Host/DataTable.php index 21cfdca..ce9020d 100644 --- a/app/Livewire/Host/DataTable.php +++ b/app/Livewire/Host/DataTable.php @@ -2,6 +2,7 @@ namespace App\Livewire\Host; use App\Models\Host; +use App\Services\ZabbixService; use SteelAnts\DataTable\Livewire\DataTableComponent; use Illuminate\Database\Eloquent\Builder; @@ -12,6 +13,9 @@ class DataTable extends DataTableComponent 'closeModal' => '$refresh', ]; + public bool $searchable = true; + public array $searchableColumns = ['display_name', 'hostname']; + public function query(): Builder { return Host::query(); @@ -20,6 +24,7 @@ class DataTable extends DataTableComponent public function headers(): array { return [ + 'display_name' => 'display_name', 'hostname' => 'hostname', ]; } diff --git a/app/Models/Host.php b/app/Models/Host.php index 0cc252c..ec3ea44 100644 --- a/app/Models/Host.php +++ b/app/Models/Host.php @@ -11,6 +11,7 @@ class Host extends Model protected $fillable = [ 'hostname', + 'display_name', ]; public function tasks() diff --git a/app/Models/MaintenanceHistory.php b/app/Models/MaintenanceHistory.php index 1f3e5bc..a756349 100644 --- a/app/Models/MaintenanceHistory.php +++ b/app/Models/MaintenanceHistory.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Observers\MaintenanceHistoryObserver; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -16,6 +17,12 @@ class MaintenanceHistory extends Model 'guestor_id', ]; + + protected static function booted() + { + MaintenanceHistory::observe(MaintenanceHistoryObserver::class); + } + public function maintenance() { return $this->BelongsTo(Maintenance::class); diff --git a/app/Observers/MaintenanceHistoryObserver.php b/app/Observers/MaintenanceHistoryObserver.php new file mode 100644 index 0000000..6d7a8a7 --- /dev/null +++ b/app/Observers/MaintenanceHistoryObserver.php @@ -0,0 +1,18 @@ +hash = md5($maintenanceHistory->maintenance_id . $maintenanceHistory->start_at); + } + + public function updating(MaintenanceHistory $maintenanceHistory): void + { + $maintenanceHistory->hash = md5($maintenanceHistory->maintenance_id . $maintenanceHistory->start_at); + } +} diff --git a/app/Services/ZabbixService.php b/app/Services/ZabbixService.php new file mode 100644 index 0000000..10ca353 --- /dev/null +++ b/app/Services/ZabbixService.php @@ -0,0 +1,130 @@ +url = $url; + } + + public function connect( $username, $password) + { + $response = Http::withoutVerifying()->post($this->url . "/api_jsonrpc.php", [ + "jsonrpc" => "2.0", + "method" => "user.login", + "params" => [ + "username" => $username, + "password" => $password, + ], + "id" => $this->id, + "auth" => null, + ]); + + if (!$response->successful()) { + throw new Exception("Unable To Authenticated", 1); + } + + $responseObject = $response->json(); + if (isset($responseObject['error'])) { + throw new Exception($responseObject['error']['data'], $responseObject['error']['code']); + } + + $this->token = $response['result']; + $this->id++; + } + + public function hosts($params = []) + { + if (empty($this->token)) { + throw new Exception("you need to connect first", 1); + } + + $response = Http::withoutVerifying()->post($this->url . "/api_jsonrpc.php", [ + "jsonrpc" => "2.0", + "method" => "host.get", + "params" => $params, + "id" => $this->id, + "auth" => $this->token, + ]); + + if (!$response->successful()) { + throw new Exception("Unable To Request", 1); + } + + $responseObject = $response->json(); + if (isset($responseObject['error'])) { + throw new Exception($responseObject['error']['data'], $responseObject['error']['code']); + } + + $this->id++; + return collect($responseObject["result"]); + } + + public function hostGroups($params = []) + { + if (empty($this->token)) { + throw new Exception("you need to connect first", 1); + } + + $response = Http::withoutVerifying()->post($this->url . "/api_jsonrpc.php", [ + "jsonrpc" => "2.0", + "method" => "hostgroup.get", + "params" => $params, + "id" => $this->id, + "auth" => $this->token, + ]); + + if (!$response->successful()) { + throw new Exception("Unable To Request", 1); + } + + $responseObject = $response->json(); + if (isset($responseObject['error'])) { + throw new Exception($responseObject['error']['data'], $responseObject['error']['code']); + } + + $this->id++; + return collect($responseObject["result"]); + } + + public function maintenances($params = []){ + return $this->request( [ + "jsonrpc" => "2.0", + "method" => "maintenance.get", + "params" => $params, + "id" => $this->id, + "auth" => $this->token, + ]); + } + + + /*Helpers*/ + + private function request($body = []){ + if (empty($this->token)) { + throw new Exception("you need to connect first", 1); + } + + $response = Http::withoutVerifying()->post($this->url . "/api_jsonrpc.php", $body); + + if (!$response->successful()) { + throw new Exception("Unable To Request", 1); + } + + $responseObject = $response->json(); + if (isset($responseObject['error'])) { + throw new Exception($responseObject['error']['data'], $responseObject['error']['code']); + } + + $this->id++; + return collect($responseObject["result"]); + } +} diff --git a/database/migrations/2024_08_16_150626_add_control_hash_to_maintenance_history_table.php b/database/migrations/2024_08_16_150626_add_control_hash_to_maintenance_history_table.php new file mode 100644 index 0000000..32ba382 --- /dev/null +++ b/database/migrations/2024_08_16_150626_add_control_hash_to_maintenance_history_table.php @@ -0,0 +1,25 @@ +string('hash'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + } +}; diff --git a/database/migrations/2024_08_16_154630_add_display_name_to_host_table.php b/database/migrations/2024_08_16_154630_add_display_name_to_host_table.php new file mode 100644 index 0000000..d835ae0 --- /dev/null +++ b/database/migrations/2024_08_16_154630_add_display_name_to_host_table.php @@ -0,0 +1,25 @@ +string('display_name')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + } +}; diff --git a/resources/views/hosts/index.blade.php b/resources/views/hosts/index.blade.php index 99697d7..2bbc847 100644 --- a/resources/views/hosts/index.blade.php +++ b/resources/views/hosts/index.blade.php @@ -3,9 +3,16 @@ @livewire('host.data-table', [], key('data-table')) diff --git a/routes/web.php b/routes/web.php index 880473b..b1b376c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,9 @@ use Illuminate\Support\Facades\Route; Route::auth(); +Route::get('/', function () { + return redirect()->route('login'); +}); Route::get('/maintenance/planned', [App\Http\Controllers\MaintenanceController::class, 'planned'])->name('maintenance.planned'); Route::get('/maintenance/planned/{maintenance_history}', [App\Http\Controllers\MaintenanceController::class, 'plannedDetail'])->name('maintenance.planned.detail'); @@ -13,6 +16,9 @@ Route::get('/maintenance/history/{maintenance_history}', [App\Http\Controllers\M Route::get('/host', [App\Http\Controllers\HostController::class, 'index'])->name('host'); +Route::get('/host/sync', [App\Http\Controllers\HostController::class, 'sync'])->name('host.sync'); + + Route::get('/maintenance', [App\Http\Controllers\MaintenanceController::class, 'index'])->name('maintenance'); Route::get('/tasks', [App\Http\Controllers\TaskController::class, 'index'])->name('tasks'); diff --git a/storage/app/.gitignore b/storage/app/.gitignore deleted file mode 100644 index 8f4803c..0000000 --- a/storage/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -* -!public/ -!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/storage/app/public/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/storage/app/public/images/logo.png b/storage/app/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6149f0deef795940d24cfecd35e3d62660c619a9 GIT binary patch literal 7084 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 literal 0 HcmV?d00001