diff --git a/app/Http/Controllers/admin/vm/VirtualMachineController.php b/app/Http/Controllers/admin/vm/VirtualMachineController.php index 3f095fa..a2ad24b 100644 --- a/app/Http/Controllers/admin/vm/VirtualMachineController.php +++ b/app/Http/Controllers/admin/vm/VirtualMachineController.php @@ -1,38 +1,148 @@ -virtualMachineRepository->filter( + [ + 'page' => $request->get('page') ?? 1 + ] + ); + $title = 'List all Virtual Machine'; + return view('administrator.pages.package.vm.index', compact('vms', 'title')); } public function create() { - return view('administrator.pages.package.vm.add'); + $title = 'Add Virtual Machine'; + return view('administrator.pages.package.vm.add', compact('title')); } public function store(Request $request) { - return view('administrator.pages.package.vm.index'); + $request->validate([ + 'vm_id' => 'required|unique:virtual_machines,vm_id', + 'name' => 'required', + ]); + + DB::beginTransaction(); + try { + $data = $request->all(); + if (empty($data['uuid'])) { + $data['uuid'] = Str::uuid(); + } + $this->virtualMachineRepository->create($data); + DB::commit(); + } catch (Exception $error) { + DB::rollBack(); + return back()->withErrors($error->getMessage()); + } + return redirect()->route('package.vm.index')->with('success', 'Add new VM successfully!'); } public function edit($id) { - return view('administrator.pages.package.vm.edit'); + $vm = $this->virtualMachineRepository->find($id); + if (!$vm) { + return back()->withErrors('Invalid request.'); + } + $title = 'Edit Virtual Machine'; + return view('administrator.pages.package.vm.edit', compact('title', 'vm')); } public function update(Request $request, $id) { - return view('administrator.pages.package.vm.index'); + $vm = $this->virtualMachineRepository->find($id); + if (!$vm) { + return back()->withErrors('Invalid request.'); + } + + $request->validate([ + 'name' => 'required', + 'vm_id' => 'required|unique:virtual_machines,vm_id,' . $id, + ]); + + DB::beginTransaction(); + try { + $this->virtualMachineRepository->update($id, $request->all()); + DB::commit(); + } catch (Exception $error) { + DB::rollBack(); + return back()->withErrors($error->getMessage()); + } + return redirect()->route('package.vm.index')->with('success', 'Update VM successfully!'); } public function destroy($id) { - return view('administrator.pages.package.vm.index'); + $vm = $this->virtualMachineRepository->find($id); + if (!$vm) { + return back()->withErrors('Invalid request.'); + } + + DB::beginTransaction(); + try { + $this->virtualMachineRepository->delete($id); + DB::commit(); + } catch (Exception $error) { + DB::rollBack(); + return back()->withErrors($error->getMessage()); + } + return redirect()->route('package.vm.index')->with('success', 'Deleted VM successfully!'); + } + + public function sync(SyncVirtualMachine $syncVM) + { + dispatch($syncVM)->delay(now()->addSeconds(5)); + return back()->with('success', 'The process is being established, please wait'); + } + + public function datatable(Request $request) + { + $query = $this->virtualMachineRepository->filter( + $request->all(), + ['datacenter', 'host'], + [], + ['*'], + true + ); + + return DataTables::eloquent($query) + ->addColumn( + 'action', + function ($d) { + return ' +
+ ' . csrf_field() . ' + ' . method_field('delete') . ' + +
'; + } + ) + ->editColumn('updated_at', function ($d) { + return $d->updated_at ? $d->updated_at->format('H:i:s d/m/Y') : ''; + }) + ->rawColumns(['action']) + ->make(true); } } \ No newline at end of file diff --git a/app/Http/Services/vCenter.php b/app/Http/Services/vCenter.php index 486da78..f4c7327 100644 --- a/app/Http/Services/vCenter.php +++ b/app/Http/Services/vCenter.php @@ -77,6 +77,29 @@ class vCenter return "Login_Again"; } + public function getAllVirtualMachine($datacenter = '') + { + $check = $this->checkApiExits(); + if ($check) { + $params = []; + if (!empty($datacenter)) { + $params['filter.datacenters'] = $datacenter; + } + + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->token + ]) + ->get("https://$this->apiUri/rest/vcenter/vm", $params)->json(); + + if (!isset($response['type'])) { + return $response['value']; + } + return false; + } + return "Login_Again"; + } + public function checkApiExits(): bool { diff --git a/app/Jobs/VirtualMachine/Sync/SyncVirtualMachine.php b/app/Jobs/VirtualMachine/Sync/SyncVirtualMachine.php new file mode 100644 index 0000000..8436991 --- /dev/null +++ b/app/Jobs/VirtualMachine/Sync/SyncVirtualMachine.php @@ -0,0 +1,146 @@ +vCenter = new ServiceVCenter; + $this->telegram = new Telegram(); + + try { + $data = vCenter::select('id', 'host', 'user', 'password', 'token')->get(); + + $vCenterCount = 0; + $vmTotalCount = 0; + + foreach ($data as $item) { + Log::info("Processing vCenter: {$item->host}"); + $this->vCenter->apiUri = $item->host; + + // Always login to get a fresh token + $token = $this->vCenter->loginServer($item); + if ($token) { + $item->token = $token; + $item->save(); + $this->vCenter->token = $token; + } else { + $this->vCenter->token = null; + Log::error("Login failed for {$item->host}"); + } + + if (!$this->vCenter->token) { + continue; + } + + $datacenters = ModuleDatacenter::where('vcenter_id', $item->id)->get(); + if ($datacenters->count() > 0) { + foreach ($datacenters as $dc) { + $vms = $this->vCenter->getAllVirtualMachine($dc->datacenter_id); + $vmCount = $this->processVMs($vms, $item, $dc->id); // Extract method for reuse + $vmTotalCount += $vmCount; + } + } else { + // Fallback: Sync all VMs without datacenter context + Log::info("No datacenters found, fetching all VMs for {$item->host}"); + $vms = $this->vCenter->getAllVirtualMachine(); + $vmCount = $this->processVMs($vms, $item, null); + $vmTotalCount += $vmCount; + } + + $vCenterCount++; // Increment vCenter synced count + } + + // Notify success via Telegram + $this->telegram->SendJobRunInfo('SyncVirtualMachine', 'Success', [ + 'message' => 'Sync VMs completed successfully', + 'synced_vcenter_count' => $vCenterCount, + 'synced_vm_count' => $vmTotalCount, + ]); + + } catch (Throwable $e) { + Log::error("SyncVirtualMachine Job Failed: " . $e->getMessage()); + // Gửi log lỗi qua Telegram + if (isset($this->telegram)) { + $this->telegram->SendJobRunInfo('SyncVirtualMachine', 'Failed', [ + 'error' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ]); + } + } + } + + private function processVMs($vms, $vCenterItem, $datacenterId) + { + // If token expired, retry login (handled inside loop in previous logic, but simpler here if we just handle the array check) + if ($vms === 'Login_Again') { + Log::info("Token expired, retrying login for {$vCenterItem->host}"); + $token = $this->vCenter->loginServer($vCenterItem); + if ($token) { + $vCenterItem->token = $token; + $vCenterItem->save(); + $this->vCenter->token = $token; + // Retry fetch + $vms = $this->vCenter->getAllVirtualMachine($datacenterId ? ModuleDatacenter::find($datacenterId)->datacenter_id : ''); + } + } + + if (!is_array($vms)) { + Log::warning("Failed to fetch VMs or empty response. Response: " . json_encode($vms)); + return 0; + } + + Log::info("Fetched " . count($vms) . " VMs"); + $count = 0; + foreach ($vms as $vmData) { + $vmObj = VirtualMachines::firstOrNew(['vm_id' => $vmData['vm']]); + + if (!$vmObj->exists) { + $vmObj->uuid = Str::uuid(); + } + + $vmObj->name = $vmData['name']; + $vmObj->power_state = $vmData['power_state']; + $vmObj->cpu = $vmData['cpu_count'] ?? 0; + $vmObj->ram_mb = $vmData['memory_size_MiB'] ?? 0; + $vmObj->datacenter_id = $datacenterId; // Assign datacenter if known + $vmObj->last_synced_at = now(); + + $vmObj->save(); + $count++; + } + return $count; + } +} diff --git a/app/Models/VirtualMachines.php b/app/Models/VirtualMachines.php index b1d958c..7bf74b7 100644 --- a/app/Models/VirtualMachines.php +++ b/app/Models/VirtualMachines.php @@ -8,4 +8,29 @@ use Illuminate\Database\Eloquent\Model; class VirtualMachines extends Model { use HasFactory; + + protected $fillable = [ + 'uuid', + 'vm_id', + 'name', + 'power_state', + 'folder', + 'is_template', + 'cpu', + 'ram_mb', + 'is_system', + 'last_synced_at', + 'datacenter_id', + 'host_id' + ]; + + public function datacenter() + { + return $this->belongsTo(ModuleDatacenter::class, 'datacenter_id'); + } + + public function host() + { + return $this->belongsTo(ModuleHost::class, 'host_id'); + } } diff --git a/app/Providers/RepositoryProvider.php b/app/Providers/RepositoryProvider.php index 03aba5d..fd1bceb 100644 --- a/app/Providers/RepositoryProvider.php +++ b/app/Providers/RepositoryProvider.php @@ -94,6 +94,8 @@ use App\Repositories\vCenter\vCenterCachedRepository; use App\Repositories\vCenter\vCenterRepositoryInterface; use App\Repositories\VPS\VPSCachedRepository; use App\Repositories\VPS\VPSRepositoryInterface; +use App\Repositories\Module\VirtualMachine\VirtualMachineRepository; +use App\Repositories\Module\VirtualMachine\VirtualMachineRepositoryInterface; class RepositoryProvider extends ServiceProvider { @@ -335,6 +337,12 @@ class RepositoryProvider extends ServiceProvider IPPoolRepositoryInterface::class, IPPoolCachedRepository::class ); + + //Module VirtualMachine + $this->app->bind( + VirtualMachineRepositoryInterface::class, + VirtualMachineRepository::class + ); } /** diff --git a/app/Repositories/Module/VirtualMachine/VirtualMachineRepository.php b/app/Repositories/Module/VirtualMachine/VirtualMachineRepository.php new file mode 100644 index 0000000..cc10ec2 --- /dev/null +++ b/app/Repositories/Module/VirtualMachine/VirtualMachineRepository.php @@ -0,0 +1,62 @@ +model + ->select($select) + ->with($relationships); + + if (!empty($data['datacenter_id'])) { + $query->where('datacenter_id', $data['datacenter_id']); + } + + if (!empty($data['host_id'])) { + $query->where('host_id', $data['host_id']); + } + + if (!empty($data['name'])) { + $query->where('name', 'like', '%' . $data['name'] . '%'); + } + + if (!empty($withCount)) { + $query->withCount($withCount); + } + + if ($returnQuery) { + return $query; + } + + if (!empty($data['paginate']) || !empty($data['page'])) { + return $query->paginate( + $data['per_page'] ?? 15 + ); + } + + return $query->get(); + } + + public function findBy(string $key = '', string|int|null $value = '') + { + if (empty($key) || empty($value)) { + return false; + } + return $this->model->where($key, $value)->first(); + } +} diff --git a/app/Repositories/Module/VirtualMachine/VirtualMachineRepositoryInterface.php b/app/Repositories/Module/VirtualMachine/VirtualMachineRepositoryInterface.php new file mode 100644 index 0000000..c8c5a63 --- /dev/null +++ b/app/Repositories/Module/VirtualMachine/VirtualMachineRepositoryInterface.php @@ -0,0 +1,15 @@ +boolean('is_system')->default(false); // vCLS, system VM $table->timestamp('last_synced_at')->nullable(); $table->foreignId('datacenter_id')->constrained('module_datacenter')->nullable(); - $table->foreignId('host_id')->constrained('module_host')->nullable(); $table->timestamps(); }); } diff --git a/resources/views/administrator/pages/block/Sync/VM.blade.php b/resources/views/administrator/pages/block/Sync/VM.blade.php new file mode 100644 index 0000000..7357adf --- /dev/null +++ b/resources/views/administrator/pages/block/Sync/VM.blade.php @@ -0,0 +1,5 @@ +Add + VM +@include("administrator.pages.block.Sync.All") +Sync + VM \ No newline at end of file diff --git a/resources/views/administrator/pages/package/vm/index.blade.php b/resources/views/administrator/pages/package/vm/index.blade.php index 522f569..f25ff05 100644 --- a/resources/views/administrator/pages/package/vm/index.blade.php +++ b/resources/views/administrator/pages/package/vm/index.blade.php @@ -1,79 +1,39 @@ -@php use App\Http\Helpers\Helper; @endphp @extends('administrator.layout.master') +@push('plugin-styles') + @foreach (config('assets.datatable.css') as $datatable) + + @endforeach +@endpush + +@section('breadcrumb') + @php + $breadcrumbItems = [ + ['label' => 'Modules'], + ['label' => 'Package vCenter'], + ['label' => 'Virtual Machine', 'active' => true], + ]; + @endphp + +@endsection @section('content') @if(auth()->guard('admins')->user()->can("manager package module"))
- @include("administrator.pages.block.Sync.Host") + @include("administrator.pages.block.Sync.VM")
- - - - - - - - - - - - - - @if(count($hosts) > 0) - @foreach($hosts->all() as $item) - - - - - - - - - - @endforeach - @endif - -
- - Host NameDatacenter NamevCenter HostLast Updated
- - - {{ $item->name }} - - @isset($item->datacenter->name) - {{$item->datacenter->name}} - @endisset - - @isset($item->datacenter->vCenter->host) - {{$item->datacenter->vCenter->host}} - @endisset - - @time($item->updated_at) - - - - - - @if(auth()->guard('admins')->user()->can("delete module vcenter")) -
- @method('delete') - @csrf - -
- @endif -
+
@else @@ -83,3 +43,8 @@ @endif @endsection +@push('plugin-scripts') + @foreach (config('assets.datatable.js') as $datatable) + + @endforeach +@endpush \ No newline at end of file diff --git a/routes/admin.php b/routes/admin.php index 39fb116..dd3fec7 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -331,6 +331,8 @@ Route::prefix('administrator')->group(function () { 'controller' => VirtualMachineController::class ], function () { Route::get('list-all-vm', 'index')->name('index'); + Route::get('datatable', 'datatable')->name('datatable'); + Route::get('sync', 'sync')->name('sync'); Route::get('create', 'create')->name('create'); Route::post('create', 'store')->name('store'); Route::get('edit/{id}', 'edit')->name('edit');