From bb73d6c5a2911e8667d0ef08208b3618c1cdcd28 Mon Sep 17 00:00:00 2001 From: pbao9 Date: Wed, 15 Apr 2026 23:38:38 +0700 Subject: [PATCH] Refactor vCenter snapshot handling and VM operations - Introduced a new method `resolveVCenterFromProduct` in `SnapshotService` to streamline vCenter resolution from product data. - Updated `CreateSnapshot`, `RemoveSnapshot`, and `RevertSnapshot` jobs to utilize the new vCenter resolution method. - Enhanced the `VmController` to support UUID-based service identification for VM operations. - Added new actions for changing VM IP and RDP port, encapsulating the logic for these operations. - Improved error handling and logging for VM operations, including detailed responses for API consumers. - Removed unnecessary relationships in model queries to optimize performance. --- app/Actions/VM/ChangeIpVMAction.php | 65 ++++ app/Actions/VM/ChangePortVMAction.php | 48 +++ app/Actions/VM/PowerOffVMAction.php | 37 +++ app/Actions/VM/PowerOnVMAction.php | 37 +++ app/Http/.DS_Store | Bin 6148 -> 6148 bytes .../Controllers/Api/Service/VmController.php | 291 ++++++++++++++++ app/Http/Services/vCenter.php | 313 +++++++++++++++--- app/Http/Services/vCenter/SnapshotService.php | 50 ++- .../Snapshot/CreateSnapshot.php | 25 +- .../Snapshot/RemoveSnapshot.php | 16 +- .../Snapshot/RevertSnapshot.php | 16 +- app/Models/ModuleVmware.php | 9 +- app/Models/Service.php | 6 + routes/client.php | 194 ++++++++--- 14 files changed, 987 insertions(+), 120 deletions(-) create mode 100644 app/Actions/VM/ChangeIpVMAction.php create mode 100644 app/Actions/VM/ChangePortVMAction.php create mode 100644 app/Actions/VM/PowerOffVMAction.php create mode 100644 app/Actions/VM/PowerOnVMAction.php create mode 100644 app/Http/Controllers/Api/Service/VmController.php diff --git a/app/Actions/VM/ChangeIpVMAction.php b/app/Actions/VM/ChangeIpVMAction.php new file mode 100644 index 0000000..7686d43 --- /dev/null +++ b/app/Actions/VM/ChangeIpVMAction.php @@ -0,0 +1,65 @@ +vCenter + ?? $vm->ipAddress?->host?->datacenter?->vCenter; + + if (!$vcenter) { + return 'vCenter_Not_Found'; + } + + if (!$this->vcenter->loginServer($vcenter)) { + return 'Login_Again'; + } + + // Use provided guest credentials, fallback to VM stored credentials + $user = $guestUser ?: (string) $vm->user; + $pass = $guestPassword ?: (string) $vm->getRawOriginal('password'); + + return $this->vcenter->changeIpVM( + (string) $vm->name, // -VMName (Get-VM -Name) + (string) $vm->vmware_id, // VMware ID (Windows -Id pattern) + $ipAddress, + $gateway, + $osType, + $user, // -GuestUser + $pass, // -GuestPassword (plaintext) + $dns, + (string) $vm->hostname + ); + } catch (\Throwable $e) { + return 'Error: ' . $e->getMessage(); + } + } +} diff --git a/app/Actions/VM/ChangePortVMAction.php b/app/Actions/VM/ChangePortVMAction.php new file mode 100644 index 0000000..6daa1c8 --- /dev/null +++ b/app/Actions/VM/ChangePortVMAction.php @@ -0,0 +1,48 @@ +apiUri / username / password to build + * the VMware.PowerCLI connection string. + * + * @param ModuleVmware $vm + * @param string $newPort + * @return array|string + */ + public function execute(ModuleVmware $vm, string $newPort): array|string + { + try { + $vcenter = $vm->vCenter + ?? $vm->ipAddress?->host?->datacenter?->vCenter; + + if (!$vcenter) { + return ['error' => true, 'data' => 'Không tìm thấy thông tin vCenter cho VM này.']; + } + + if (!$this->vcenter->loginServer($vcenter)) { + return 'Login_Again'; + } + + return $this->vcenter->changeRDPPortWindows( + (string) $vm->name, + (string) $vm->user, + (string) $vm->getRawOriginal('password'), + $newPort + ); + } catch (\Throwable $e) { + return ['error' => true, 'data' => $e->getMessage()]; + } + } +} diff --git a/app/Actions/VM/PowerOffVMAction.php b/app/Actions/VM/PowerOffVMAction.php new file mode 100644 index 0000000..0a19879 --- /dev/null +++ b/app/Actions/VM/PowerOffVMAction.php @@ -0,0 +1,37 @@ +vCenter + ?? $vm->ipAddress?->host?->datacenter?->vCenter; + + if (!$vcenter) { + return ['error' => true, 'data' => 'Không tìm thấy thông tin vCenter cho VM này.']; + } + + if (!$this->vcenter->loginServer($vcenter)) { + return 'Login_Again'; + } + + return $this->vcenter->PowerOffVirtualMachine($vm->vmware_id); + } catch (\Throwable $e) { + return ['error' => true, 'data' => $e->getMessage()]; + } + } +} diff --git a/app/Actions/VM/PowerOnVMAction.php b/app/Actions/VM/PowerOnVMAction.php new file mode 100644 index 0000000..75cce82 --- /dev/null +++ b/app/Actions/VM/PowerOnVMAction.php @@ -0,0 +1,37 @@ +vCenter + ?? $vm->ipAddress?->host?->datacenter?->vCenter; + + if (!$vcenter) { + return ['error' => true, 'data' => 'Không tìm thấy thông tin vCenter cho VM này.']; + } + + if (!$this->vcenter->loginServer($vcenter)) { + return 'Login_Again'; + } + + return $this->vcenter->StartVirtualMachine($vm, $vm->vmware_id); + } catch (\Throwable $e) { + return ['error' => true, 'data' => $e->getMessage()]; + } + } +} diff --git a/app/Http/.DS_Store b/app/Http/.DS_Store index 6338d46d2325cc882b30c4592faf7f1b1639ca1d..ade8750933da8456134bb5452890179af3a5f0ed 100644 GIT binary patch delta 19 acmZoMXffEZij~ROVDdT^`^~ReC4~S)xCVa! delta 19 acmZoMXffEZij~R4Wb!%|`^~ReC4~S)>js1X diff --git a/app/Http/Controllers/Api/Service/VmController.php b/app/Http/Controllers/Api/Service/VmController.php new file mode 100644 index 0000000..eb6b6b1 --- /dev/null +++ b/app/Http/Controllers/Api/Service/VmController.php @@ -0,0 +1,291 @@ + } + */ + public function powerOn(Request $request, PowerOnVMAction $action): JsonResponse + { + $validated = $request->validate(['product' => ['required', 'integer']]); + + $vm = $this->resolveVm((int) $validated['product'], $request); + if ($vm instanceof JsonResponse) { + return $vm; + } + + $result = $action->execute($vm); + + return $this->vmResponse($result, 'Đã gửi lệnh bật VM.'); + } + + // ------------------------------------------------------------------------- + // Power OFF + // ------------------------------------------------------------------------- + + /** + * POST /v1/system/vm-power-off + * + * Body: { "product": } + */ + public function powerOff(Request $request, PowerOffVMAction $action): JsonResponse + { + $validated = $request->validate(['product' => ['required', 'integer']]); + + $vm = $this->resolveVm((int) $validated['product'], $request); + if ($vm instanceof JsonResponse) { + return $vm; + } + + $result = $action->execute($vm); + + return $this->vmResponse($result, 'Đã gửi lệnh tắt VM.'); + } + + // ------------------------------------------------------------------------- + // Change IP + // ------------------------------------------------------------------------- + + /** + * POST /v1/system/vm-change-ip + * + * Body: + * server_name string Service.server_name (e.g. "xasdf.com.vnx") + * ip_address string New IP to assign + * gateway string (optional) Gateway — falls back to existing IP record + */ + public function changeIp(Request $request, ChangeIpVMAction $action): JsonResponse + { + $validated = $request->validate([ + 'server_name' => ['required', 'string', 'max:255'], + 'ip_address' => ['required', 'ip'], + 'gateway' => ['nullable', 'ip'], + 'dns' => ['nullable', 'ip'], + 'guest_user' => ['required', 'string', 'max:255'], + 'guest_password' => ['required', 'string', 'max:255'], + ]); + + $customer = $request->user(); + + // Look up Service by server_name + owner + // VM chain: Service → orderItem → order → vmWare (same as ServiceClientDetailResource) + $service = Service::with([ + 'orderItem.order.vmWare.vCenter', + 'orderItem.order.vmWare.ipAddress.host.datacenter.vCenter', + 'orderItem.os.osGroup', + ]) + ->where('server_name', $validated['server_name']) + ->where('customer_id', $customer->id) + ->first(); + + if (!$service) { + return response()->json(['success' => false, 'message' => 'Không tìm thấy dịch vụ.'], 404); + } + + $vm = $service->orderItem?->order?->vmWare?->first(); + + if (!$vm) { + return response()->json(['success' => false, 'message' => 'Không tìm thấy VM cho dịch vụ này.'], 404); + } + + // Auto-detect OS type from the service's order item OS group + $osGroup = strtolower($service->orderItem?->os?->osGroup?->name ?? ''); + $osType = (str_contains($osGroup, 'linux') || str_contains($osGroup, 'ubuntu') || str_contains($osGroup, 'centos')) + ? 'linux' + : 'windows'; + + $gateway = $validated['gateway'] ?? ''; + + $dns = $validated['dns'] ?? '8.8.8.8'; + + $raw = $action->execute( + $vm, + $validated['ip_address'], + $gateway, + $osType, + $dns, + $validated['guest_user'], + $validated['guest_password'] + ); + + // Always log the raw PowerShell output for debugging + Log::channel('Custom')->info('[changeIp] raw output', [ + 'server_name' => $validated['server_name'], + 'ip_address' => $validated['ip_address'], + 'gateway' => $gateway, + 'os_type' => $osType, + 'vm_name' => $vm->name, + 'vmware_id' => $vm->vmware_id, + 'raw' => $raw, + ]); + + $telegram = new Telegram(); + + if ($raw === 'Login_Again' || $raw === 'vCenter_Not_Found') { + $errMsg = $raw === 'vCenter_Not_Found' + ? 'Không tìm thấy vCenter cho VM này.' + : 'Không thể đăng nhập vào vCenter.'; + + $telegram->SendErrorMessage("❌ Đổi IP thất bại [{$osType}]\n" + . "🖥 VM: {$vm->name} ({$vm->vmware_id})\n" + . "📍 IP mới: {$validated['ip_address']}\n" + . "🔗 GW: {$gateway}\n" + . "🚫 Lỗi: {$errMsg}"); + + return response()->json(['success' => false, 'message' => $errMsg], 400); + } + + if (str_starts_with((string) $raw, 'Error:') || str_contains((string) $raw, '[STDERR]')) { + $telegram->SendErrorMessage("❌ Đổi IP thất bại [{$osType}]\n" + . "🖥 VM: {$vm->name} ({$vm->vmware_id})\n" + . "📍 IP mới: {$validated['ip_address']}\n" + . "🔗 GW: {$gateway}\n" + . "📋 Output:\n" . mb_substr((string) $raw, 0, 800)); + + return response()->json(['success' => false, 'message' => 'Lệnh đổi IP thất bại. Admin đã được thông báo.', 'data' => ['raw' => $raw]], 400); + } + + // Empty raw = script chạy không có output → coi là thành công (PowerShell script không echo) + // Nhưng vẫn gửi cảnh báo để theo dõi + if (trim((string) $raw) === '') { + $telegram->SendMessage("⚠️ Đổi IP [{$osType}] - Script chạy nhưng không có output\n" + . "🖥 VM: {$vm->name} ({$vm->vmware_id})\n" + . "📍 IP mới: {$validated['ip_address']}\n" + . "🔗 GW: {$gateway}\n" + . "ℹ️ Kiểm tra lại VM để xác nhận IP đã thay đổi chưa."); + } + + return response()->json([ + 'success' => true, + 'message' => 'Đã gửi lệnh đổi IP.', + 'data' => ['raw' => $raw], + ]); + } + + // ------------------------------------------------------------------------- + // Change Port (RDP) + // ------------------------------------------------------------------------- + + /** + * POST /v1/system/vm-change-port + * + * Body: + * product int ModuleVmware.id + * new_port int New RDP port (1024–65535) + */ + public function changePort(Request $request, ChangePortVMAction $action): JsonResponse + { + $validated = $request->validate([ + 'product' => ['required', 'integer'], + 'new_port' => ['required', 'integer', 'min:1024', 'max:65535'], + ]); + + $vm = $this->resolveVm((int) $validated['product'], $request); + if ($vm instanceof JsonResponse) { + return $vm; + } + + $result = $action->execute($vm, (string) $validated['new_port']); + + // changeRDPPortWindows returns array on success/failure or string on login fail + if ($result === 'Login_Again' || $result === 'Login_Fail') { + return response()->json([ + 'success' => false, + 'message' => 'Không thể đăng nhập vào vCenter.', + ], 400); + } + + if (is_array($result)) { + $ok = isset($result['success']) + ? (bool) $result['success'] + : !($result['error'] ?? true); + + return response()->json([ + 'success' => $ok, + 'message' => $ok ? 'Đổi cổng RDP thành công.' : ($result['message'] ?? 'Đổi cổng thất bại.'), + 'data' => $result, + ], $ok ? 200 : 400); + } + + return response()->json([ + 'success' => false, + 'message' => 'Phản hồi không hợp lệ từ vCenter.', + 'data' => ['raw' => $result], + ], 400); + } + + // ------------------------------------------------------------------------- + // Shared helpers + // ------------------------------------------------------------------------- + + /** + * Load a ModuleVmware by ID, verify the authenticated user owns it. + * Returns JsonResponse on error, ModuleVmware on success. + */ + private function resolveVm(int $id, Request $request): ModuleVmware|JsonResponse + { + $vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter']) + ->find($id); + + if (!$vm) { + return response()->json(['success' => false, 'message' => 'Không tìm thấy VM.'], 404); + } + + if ((int) $vm->customer_id !== (int) $request->user()->id) { + return response()->json(['success' => false, 'message' => 'Bạn không có quyền thao tác VM này.'], 403); + } + + return $vm; + } + + /** + * Normalise the array|string return value from Power ON/OFF actions. + */ + private function vmResponse(array|string $result, string $successMessage): JsonResponse + { + if ($result === 'Login_Again') { + return response()->json([ + 'success' => false, + 'message' => 'Không thể đăng nhập vào vCenter.', + ], 400); + } + + if (is_string($result)) { + return response()->json(['success' => false, 'message' => $result], 400); + } + + if ($result['error'] ?? true) { + return response()->json([ + 'success' => false, + 'message' => $result['data'] ?? 'Thao tác thất bại.', + 'data' => $result, + ], 400); + } + + return response()->json([ + 'success' => true, + 'message' => $successMessage, + 'data' => $result['data'] ?? 'success', + ]); + } +} diff --git a/app/Http/Services/vCenter.php b/app/Http/Services/vCenter.php index 94571e0..6830e11 100644 --- a/app/Http/Services/vCenter.php +++ b/app/Http/Services/vCenter.php @@ -118,6 +118,49 @@ class vCenter return true; } + /** + * Lấy thông tin vCenter từ product (ModuleVmware) qua nhiều cách: + * 1. Direct relation qua vcenter_id + * 2. Qua ipAddress → host → datacenter → vCenter + * 3. Qua Service relation (nếu Service có vcenter_id) + * + * @param \App\Models\ModuleVmware $product + * @return \App\Models\vCenter|null + */ + public function resolveVCenterFromProduct(\App\Models\ModuleVmware $product): ?\App\Models\vCenter + { + // Cách 1: Ưu tiên quan hệ trực tiếp trên ModuleVmware + if ($product->vCenter) { + Log::channel('Custom')->debug('resolveVCenter: Found via direct vCenter relation', ['vcenter_id' => $product->vCenter->id]); + return $product->vCenter; + } + + // Cách 2: Fallback qua ipAddress chain + if ($product->ipAddress?->host?->datacenter?->vCenter) { + Log::channel('Custom')->debug('resolveVCenter: Found via ipAddress chain'); + return $product->ipAddress->host->datacenter->vCenter; + } + + // Cách 3: Qua Service relation (nếu Service có vcenter_id) + // Lấy service đầu tiên liên kết với VM này + $service = $product->services()->first(); + if ($service) { + // Load vmware relation với vCenter + $service->load('vmware.vCenter'); + if ($service->vmware?->vCenter) { + Log::channel('Custom')->debug('resolveVCenter: Found via Service→Vmware→vCenter'); + return $service->vmware->vCenter; + } + } + + Log::channel('Custom')->warning('resolveVCenter: Could not find vCenter for product', [ + 'product_id' => $product->id, + 'vmware_id' => $product->vmware_id, + ]); + + return null; + } + public function getAllHost($datacenter = '') { $check = $this->checkApiExits(); @@ -294,27 +337,28 @@ class vCenter if (!$auth) { return "Login_Fail"; } + try { - $psCommand = <<apiUri}' -Credential \$cred; - - $cmd; - - Disconnect-VIServer -Confirm:\$false - " - PS; + // Build PowerShell command inline + $psCommand = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "' . + 'Import-Module VMware.PowerCLI; ' . + 'Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false -Scope Session; ' . + '$cred = Import-Clixml -Path \'C:\secure\vcenter.cred\'; ' . + 'Connect-VIServer -Server \'' . $this->apiUri . '\' -Credential $cred; ' . + $cmd . '; ' . + 'Disconnect-VIServer -Confirm:$false" 2>&1'; + + Log::channel('Custom')->info('PowerShell Command: ' . substr($psCommand, 0, 200) . '...'); $out = $auth->exec($psCommand); $err = $auth->getStdError(); + + Log::channel('Custom')->info('PowerShell Output length: ' . strlen($out)); + return $out . ($err ? "\n[STDERR]\n" . $err : ''); } catch (\Exception $e) { + Log::channel('Custom')->error('Command Exception: ' . $e->getMessage()); return $e->getMessage(); } } @@ -573,6 +617,69 @@ PS; return $data; } + /** + * Change IP address of a VM via existing PowerShell scripts. + * + * Linux → C:\CLISave\changeIPLinux.ps1 + * Params: -VMName -IP -Gateway -DNS -GuestUser -GuestPassword + * Uses: Get-VM -Name $VMName + Invoke-VMScript (requires guest creds) + * Returns JSON → parsed via CommandChangePort($cmd, true) + * + * Windows → C:\CLISave\changeWindows1line.ps1 (legacy inline variable pattern) + * + * loginServer() MUST be called before this so $this->apiUri / username / + * password are set for the Connect-VIServer call inside CommandChangePort. + */ + public function changeIpVM( + string $vmName, // VM display name used with Get-VM -Name + string $vmwareId, // VMware ID (used for Windows -Id pattern) + string $ipAddress, + string $gateway, + string $osType, + string $guestUser = '', + string $guestPassword = '', + string $dns = '8.8.8.8', + string $hostname = '' + ): string { + $q = fn(string $v) => str_replace("'", "''", $v); + + $isLinux = str_contains(strtolower($osType), 'linux') + || str_contains(strtolower($osType), 'ubuntu') + || str_contains(strtolower($osType), 'centos') + || str_contains(strtolower($osType), 'rocky'); + + if ($isLinux) { + // changeIPLinux.ps1 param(VMName, IP, Gateway, DNS, GuestUser, GuestPassword) + $psFile = "C:\\CLISave\\changeIPLinux.ps1"; + $Command = "& '{$psFile}'" + . " -VMName '" . $q($vmName) . "'" + . " -IP '" . $q($ipAddress) . "'" + . " -Gateway '" . $q($gateway) . "'" + . " -DNS '" . $q($dns) . "'" + . " -GuestUser '" . $q($guestUser) . "'" + . " -GuestPassword '" . $q($guestPassword) . "'"; + + Log::channel('Custom')->info('[changeIpVM:linux] ' . $Command); + + // Script returns JSON → use CommandChangePort with expectJson=true + return (string) $this->CommandChangePort($Command, true); + } + + // Windows: legacy inline variable pattern → changeWindows1line.ps1 + $VirtualMachine = "VirtualMachine-$vmwareId"; + $hn = strtolower($hostname ?: $vmName); + $Command = "\$vm = get-vm -Id '{$VirtualMachine}';" + . " \$hostname = '{$q($hn)}';" + . " \$ipadd = '{$q($ipAddress)}';" + . " \$gw = '{$q($gateway)}';" + . " \$password = \"'{$q($guestPassword)}'\"; " + . " cd /; C:\\CLISave\\changeWindows1line.ps1;"; + + Log::channel('Custom')->info('[changeIpVM:windows] ' . $Command); + + return (string) $this->Command($Command); + } + public function loadDatastore($data = [], $host_id = '') { Log::info('[vCenter] loadDatastore()', [ @@ -1672,9 +1779,22 @@ PS; public function CreateSnapShot($product, ?string $snapshotName = null): string|array { try { - $this->username = $product->ipaddress->host->datacenter->vCenter->user; - $this->password = SignRSA::DecryptFunc($product->ipaddress->host->datacenter->vCenter->password); - $this->apiUri = $product->ipaddress->host->datacenter->vCenter->host; + // Sử dụng helper method để resolve vCenter từ product + $vcenter = $this->resolveVCenterFromProduct($product); + + if (!$vcenter) { + return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; + } + + $this->username = $vcenter->user; + $this->password = SignRSA::DecryptFunc($vcenter->password); + $this->apiUri = $vcenter->host; + + // Login to get token + $loginResult = $this->loginServer($vcenter); + if (!$loginResult) { + return "Login_Again"; + } } catch (\Exception $e) { return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()]; } @@ -1692,18 +1812,34 @@ PS; Log::channel('Custom')->info("\r\n"); - $Command = "\$vm_name = '$vmware_name';\$name = '$Snapshot'; cd /; C:\CLISave\createsnapshot.ps1;"; + $Command = "& 'C:\CLISave\createsnapshot.ps1' -vm_name '$vmware_name' -name '$Snapshot'"; Log::channel('Custom')->info("$Command"); - $this->Command($Command); + $cmdResult = $this->Command($Command); + Log::channel('Custom')->info("Command Result: " . $cmdResult); Log::channel('Custom')->info("\r\n"); Log::channel('Custom')->info("After run Command |" . Carbon::now()); Log::channel('Custom')->info("==================================================================="); + // Check if command failed + if (strpos($cmdResult, 'Login_Fail') !== false) { + return [ + 'error' => true, + 'message' => 'SSH Login to PowerShell server failed' + ]; + } + + if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) { + return [ + 'error' => true, + 'message' => 'PowerShell command failed: ' . $cmdResult + ]; + } + // Clear snapshot cache for this VM $this->clearSnapshotCache($product); @@ -1739,9 +1875,16 @@ PS; $startTime = microtime(true); try { - $this->username = $product->ipaddress->host->datacenter->vCenter->user; - $this->password = SignRSA::DecryptFunc($product->ipaddress->host->datacenter->vCenter->password); - $this->apiUri = $product->ipaddress->host->datacenter->vCenter->host; + // Sử dụng helper method để resolve vCenter từ product + $vcenter = $this->resolveVCenterFromProduct($product); + + if (!$vcenter) { + return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; + } + + $this->username = $vcenter->user; + $this->password = SignRSA::DecryptFunc($vcenter->password); + $this->apiUri = $vcenter->host; } catch (\Exception $e) { return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()]; } @@ -1919,28 +2062,52 @@ PS; private function callSoap($soapBody, $cookie = null) { $envelope = ' - + ' . $soapBody . ' '; - $headers = [ - 'Content-Type' => 'text/xml; charset=utf-8', - 'SOAPAction' => 'urn:vim25/2005' + $httpHeaders = [ + 'Content-Type: text/xml; charset=utf-8', + 'SOAPAction: "urn:vim25/6.0"' ]; if ($cookie) { - $headers['Cookie'] = $cookie; + $httpHeaders[] = "Cookie: {$cookie}"; } - $response = Http::withoutVerifying() - ->withHeaders($headers) - ->post("https://{$this->apiUri}/sdk", $envelope); + // Use cURL instead of Laravel Http to avoid HTTP/2 issues with vCenter + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "https://{$this->apiUri}/sdk"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $envelope); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders); - $responseCookie = $response->header('Set-Cookie'); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $headers = substr($response, 0, $headerSize); + $body = substr($response, $headerSize); + curl_close($ch); + + // Parse Set-Cookie header + $responseCookie = null; + if (preg_match('/Set-Cookie:\s*([^\r\n]+)/i', $headers, $matches)) { + $responseCookie = trim($matches[1]); + } + + Log::channel('Custom')->debug('SOAP call', [ + 'http_code' => $httpCode, + 'has_cookie' => !empty($responseCookie), + 'body_preview' => substr($body, 0, 200) + ]); return [ - 'body' => $response->body(), - 'cookie' => $responseCookie ? $responseCookie : $cookie + 'body' => $body, + 'cookie' => $responseCookie ?: $cookie ]; } @@ -1948,8 +2115,22 @@ PS; public function RemoveSnapShot($product, $snap = ''): string|array { - $this->username = $product->IpAddress->Host->Datacenter->vCenter->user; - $this->password = SignRSA::DecryptFunc($product->IpAddress->Host->Datacenter->vCenter->password); + // Sử dụng helper method để resolve vCenter từ product + $vcenter = $this->resolveVCenterFromProduct($product); + + if (!$vcenter) { + return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; + } + + $this->username = $vcenter->user; + $this->password = SignRSA::DecryptFunc($vcenter->password); + + // Login to get token + $loginResult = $this->loginServer($vcenter); + if (!$loginResult) { + return "Login_Again"; + } + $vmware_name = (string) $product->name; $check = $this->checkApiExits(); if ($check) { @@ -1961,11 +2142,28 @@ PS; Log::channel('Custom')->info("\r\n"); - $Command = "\$vm_name = '$vmware_name';\$name = '$snap'; cd /; C:\CLISave\deletesnap.ps1;"; + $Command = "& 'C:\CLISave\deletesnap.ps1' -vm_name '$vmware_name' -name '$snap'"; Log::channel('Custom')->info("$Command"); - $this->Command($Command); + $cmdResult = $this->Command($Command); + + Log::channel('Custom')->info("Command Result: " . $cmdResult); + + // Check if command failed + if (strpos($cmdResult, 'Login_Fail') !== false) { + return [ + 'error' => true, + 'message' => 'SSH Login to PowerShell server failed' + ]; + } + + if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) { + return [ + 'error' => true, + 'message' => 'PowerShell command failed: ' . $cmdResult + ]; + } Log::channel('Custom')->info("\r\n"); @@ -1996,9 +2194,23 @@ PS; public function RevertSnapShot($product, $snap = ''): string|array { - $this->username = $product->IpAddress->Host->Datacenter->vCenter->user; - $this->password = SignRSA::DecryptFunc($product->IpAddress->Host->Datacenter->vCenter->password); - $this->apiUri = $product->IpAddress->Host->Datacenter->vCenter->host; + // Sử dụng helper method để resolve vCenter từ product + $vcenter = $this->resolveVCenterFromProduct($product); + + if (!$vcenter) { + return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; + } + + $this->username = $vcenter->user; + $this->password = SignRSA::DecryptFunc($vcenter->password); + $this->apiUri = $vcenter->host; + + // Login to get token + $loginResult = $this->loginServer($vcenter); + if (!$loginResult) { + return "Login_Again"; + } + $vmware_name = (string) $product->name; $check = $this->checkApiExits(); if ($check) { @@ -2010,11 +2222,28 @@ PS; Log::channel('Custom')->info("\r\n"); - $Command = "\$vm_name = '$vmware_name';\$name = '$snap'; cd /; C:\CLISave\backSnap.ps1;"; + $Command = "& 'C:\CLISave\backSnap.ps1' -vm_name '$vmware_name' -name '$snap'"; Log::channel('Custom')->info("$Command"); - $this->Command($Command); + $cmdResult = $this->Command($Command); + + Log::channel('Custom')->info("Command Result: " . $cmdResult); + + // Check if command failed + if (strpos($cmdResult, 'Login_Fail') !== false) { + return [ + 'error' => true, + 'message' => 'SSH Login to PowerShell server failed' + ]; + } + + if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) { + return [ + 'error' => true, + 'message' => 'PowerShell command failed: ' . $cmdResult + ]; + } Log::channel('Custom')->info("\r\n"); diff --git a/app/Http/Services/vCenter/SnapshotService.php b/app/Http/Services/vCenter/SnapshotService.php index 1c857db..b00ad75 100644 --- a/app/Http/Services/vCenter/SnapshotService.php +++ b/app/Http/Services/vCenter/SnapshotService.php @@ -17,19 +17,54 @@ class SnapshotService $this->powerShellService = $powerShellService; } + /** + * Resolve vCenter từ product (ModuleVmware) qua nhiều cách: + * 1. Direct relation qua vcenter_id + * 2. Qua ipAddress → host → datacenter → vCenter + * 3. Qua Service relation + */ + protected function resolveVCenterFromProduct($product): ?\App\Models\vCenter + { + // Cách 1: Ưu tiên quan hệ trực tiếp trên ModuleVmware + if ($product->vCenter) { + return $product->vCenter; + } + + // Cách 2: Fallback qua ipAddress chain + if ($product->ipaddress?->host?->datacenter?->vCenter) { + return $product->ipaddress->host->datacenter->vCenter; + } + + // Cách 3: Qua Service relation + $service = $product->services()->first(); + if ($service) { + $service->load('vmware.vCenter'); + if ($service->vmware?->vCenter) { + return $service->vmware->vCenter; + } + } + + return null; + } + public function CreateSnapShot($product): string|array { try { // Note: credentials setup logic from original vCenter.php seemed redundant with Client logic, - // but CreateSnapShot was overwriting $this->username/password. + // but CreateSnapShot was overwriting $this->username/password. // Here we assume Client is used, or we need to update Client. // Client stores token. If we need username/password for something else, Client has them now via getters. // But CreateSnapshot used $product->ipaddress...->vCenter->user/password. - + // Re-login to ensure we have the right credentials for the vCenter associated with the product - $vcenterModel = $product->ipaddress->host->datacenter->vCenter; + $vcenterModel = $this->resolveVCenterFromProduct($product); + + if (!$vcenterModel) { + return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; + } + $this->client->loginServer($vcenterModel); - + } catch (\Exception $e) { return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()]; } @@ -86,7 +121,12 @@ class SnapshotService // and add the private helpers it depends on. try { - $vcenterModel = $product->ipaddress->host->datacenter->vCenter; + $vcenterModel = $this->resolveVCenterFromProduct($product); + + if (!$vcenterModel) { + return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; + } + $this->client->loginServer($vcenterModel); } catch (\Exception $e) { return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()]; diff --git a/app/Jobs/VirtualMachine/Snapshot/CreateSnapshot.php b/app/Jobs/VirtualMachine/Snapshot/CreateSnapshot.php index 85fa1d7..3e41290 100644 --- a/app/Jobs/VirtualMachine/Snapshot/CreateSnapshot.php +++ b/app/Jobs/VirtualMachine/Snapshot/CreateSnapshot.php @@ -25,25 +25,36 @@ class CreateSnapshot implements ShouldQueue public function handle(ServiceVCenter $serviceVCenter) { - $snapshot = ModuleSnapshot::with('vm.ipAddress.host.datacenter.vCenter') + $snapshot = ModuleSnapshot::with('vm') ->find($this->snapshotId); if (!$snapshot) { + Log::error('Snapshot not found', ['snapshot_id' => $this->snapshotId]); return; } $snapshot->update(['status' => 'processing']); $product = $snapshot->vm; - $vcenter = $product->ipAddress->host->datacenter->vCenter; + + // Resolve vCenter using helper method + $vcenter = $serviceVCenter->resolveVCenterFromProduct($product); + + if (!$vcenter) { + $snapshot->update([ + 'status' => 'failed', + 'error_message' => 'Không tìm thấy thông tin vCenter', + ]); + throw new \Exception('Không tìm thấy thông tin vCenter cho VM'); + } try { // Config vCenter $serviceVCenter->token = $vcenter->token; $serviceVCenter->apiUri = $vcenter->host; - // Call vCenter - $result = $serviceVCenter->CreateSnapShot($product); + // Call vCenter with snapshot name from DB + $result = $serviceVCenter->CreateSnapShot($product, $snapshot->name); if ($result === 'Login_Again') { $token = $serviceVCenter->loginServer($vcenter); @@ -53,7 +64,7 @@ class CreateSnapshot implements ShouldQueue $vcenter->update(['token' => $token]); $serviceVCenter->token = $token; - $result = $serviceVCenter->CreateSnapShot($product); + $result = $serviceVCenter->CreateSnapShot($product, $snapshot->name); } if (is_array($result) && ($result['error'] ?? false)) { @@ -62,10 +73,8 @@ class CreateSnapshot implements ShouldQueue // Update DB SUCCESS $snapshot->update([ - 'name' => $result['name'] ?? $snapshot->name, 'status' => 'success', - 'snapshot_ref' => $result['snapshot_id'] ?? null, - 'vcenter_task_id' => $result['task_id'] ?? null, + 'snapshot_ref' => $result['data'] ?? $snapshot->name, ]); // Increment quota CHỈ khi success diff --git a/app/Jobs/VirtualMachine/Snapshot/RemoveSnapshot.php b/app/Jobs/VirtualMachine/Snapshot/RemoveSnapshot.php index 5f368ba..fae1fe4 100644 --- a/app/Jobs/VirtualMachine/Snapshot/RemoveSnapshot.php +++ b/app/Jobs/VirtualMachine/Snapshot/RemoveSnapshot.php @@ -45,21 +45,21 @@ class RemoveSnapshot implements ShouldQueue public function handle(ServiceVCenter $serviceVCenter) { try { - // Load product and snapshot with necessary relationships - $product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')->find($this->productId); + // Load product and snapshot + $product = ModuleVmware::find($this->productId); $snapshot = ModuleSnapshot::find($this->snapshotId); if (!$product || !$snapshot) { throw new \Exception("Không tìm thấy thông tin VPS hoặc snapshot"); } - // Kiểm tra cấu hình vCenter - if (!$product->ipAddress?->host?->datacenter?->vCenter) { + // Resolve vCenter using helper method + $vcenter = $serviceVCenter->resolveVCenterFromProduct($product); + + if (!$vcenter) { throw new \Exception("VPS không có thông tin vCenter được cấu hình"); } - $vcenter = $product->ipAddress->host->datacenter->vCenter; - // Set vCenter connection info $serviceVCenter->token = $vcenter->token; $serviceVCenter->apiUri = $vcenter->host; @@ -72,7 +72,7 @@ class RemoveSnapshot implements ShouldQueue ]); // Attempt to remove snapshot - $result = $serviceVCenter->RemoveSnapShot($product, $snapshot->name); + $result = $serviceVCenter->RemoveSnapShot($product, $snapshot->snapshot_ref ?? $snapshot->name); // Handle re-login if needed if ($result === 'Login_Again') { @@ -81,7 +81,7 @@ class RemoveSnapshot implements ShouldQueue if ($token) { $vcenter->update(['token' => $token]); $serviceVCenter->token = $token; - $result = $serviceVCenter->RemoveSnapShot($product, $snapshot->name); + $result = $serviceVCenter->RemoveSnapShot($product, $snapshot->snapshot_ref ?? $snapshot->name); } else { throw new \Exception("Không thể đăng nhập lại vào vCenter"); } diff --git a/app/Jobs/VirtualMachine/Snapshot/RevertSnapshot.php b/app/Jobs/VirtualMachine/Snapshot/RevertSnapshot.php index 79d46b6..dff4e67 100644 --- a/app/Jobs/VirtualMachine/Snapshot/RevertSnapshot.php +++ b/app/Jobs/VirtualMachine/Snapshot/RevertSnapshot.php @@ -44,21 +44,21 @@ class RevertSnapshot implements ShouldQueue public function handle(ServiceVCenter $serviceVCenter) { try { - // Load product and snapshot with necessary relationships - $product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')->find($this->productId); + // Load product and snapshot + $product = ModuleVmware::find($this->productId); $snapshot = ModuleSnapshot::find($this->snapshotId); if (!$product || !$snapshot) { throw new \Exception("Không tìm thấy thông tin VPS hoặc snapshot"); } - // Kiểm tra cấu hình vCenter - if (!$product->ipAddress?->host?->datacenter?->vCenter) { + // Resolve vCenter using helper method + $vcenter = $serviceVCenter->resolveVCenterFromProduct($product); + + if (!$vcenter) { throw new \Exception("VPS không có thông tin vCenter được cấu hình"); } - $vcenter = $product->ipAddress->host->datacenter->vCenter; - // Set vCenter connection info $serviceVCenter->token = $vcenter->token; $serviceVCenter->apiUri = $vcenter->host; @@ -71,7 +71,7 @@ class RevertSnapshot implements ShouldQueue ]); // Attempt to revert snapshot - $result = $serviceVCenter->RevertSnapShot($product, $snapshot->name); + $result = $serviceVCenter->RevertSnapShot($product, $snapshot->snapshot_ref ?? $snapshot->name); // Handle re-login if needed if ($result === 'Login_Again') { @@ -80,7 +80,7 @@ class RevertSnapshot implements ShouldQueue if ($token) { $vcenter->update(['token' => $token]); $serviceVCenter->token = $token; - $result = $serviceVCenter->RevertSnapShot($product, $snapshot->name); + $result = $serviceVCenter->RevertSnapShot($product, $snapshot->snapshot_ref ?? $snapshot->name); } else { throw new \Exception("Không thể đăng nhập lại vào vCenter"); } diff --git a/app/Models/ModuleVmware.php b/app/Models/ModuleVmware.php index 7c4e691..f31fac4 100644 --- a/app/Models/ModuleVmware.php +++ b/app/Models/ModuleVmware.php @@ -30,7 +30,9 @@ class ModuleVmware extends Model 'status' ]; - protected $casts = ['password' => 'hashed']; + // NOTE: password must NOT use 'hashed' cast — it is a VM guest credential + // used in plaintext by PowerShell scripts (Invoke-VMScript) via SSH/PowerCLI. + protected $casts = []; public function customer() { return $this->belongsTo(\App\Models\Customer::class); @@ -56,6 +58,11 @@ class ModuleVmware extends Model return $this->belongsTo(\App\Models\Order::class); } + public function services() + { + return $this->hasMany(\App\Models\Service::class, 'vm_id', 'vmware_id'); + } + public function getStatus() { return match ($this->status) { diff --git a/app/Models/Service.php b/app/Models/Service.php index 5cecc86..3bdfec7 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -61,6 +61,12 @@ class Service extends Model return $this->hasMany(ServiceAddon::class, 'service_id'); } + /** The VPS/VM associated with this service (if applicable). */ + public function vmware() + { + return $this->belongsTo(ModuleVmware::class, 'vm_id', 'vmware_id'); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- diff --git a/routes/client.php b/routes/client.php index 6d5a70d..d433625 100644 --- a/routes/client.php +++ b/routes/client.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Api\Client\ClientController; use App\Http\Controllers\Api\Service\OrderController; use App\Http\Controllers\Api\Service\ProductCategoryController; use App\Http\Controllers\Api\Service\ServiceController; +use App\Http\Controllers\Api\Service\VmController; use App\Http\Controllers\Api\Client\Service\ServiceController as ServiceClientController; use App\Http\Controllers\Api\Transaction\TransactionController; use Illuminate\Support\Facades\Route; @@ -163,6 +164,14 @@ Route::prefix('v1')->as('api.')->group(function () { Route::get('v1/payment/vnpay/return', [TransactionController::class, 'returnCallback']); Route::prefix('v1/system')->middleware(['api.auth'])->group(function () { + // VM power & network operations + Route::controller(VmController::class)->group(function () { + Route::post('vm-power-on', 'powerOn') ->name('vm.power-on'); + Route::post('vm-power-off', 'powerOff') ->name('vm.power-off'); + Route::post('vm-change-ip', 'changeIp') ->name('vm.change-ip'); + Route::post('vm-change-port','changePort') ->name('vm.change-port'); + }); + Route::get('get-ticket-console', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { $product_id = $request->get('product'); if (!$product_id) { @@ -249,15 +258,36 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () { ]); }); - Route::post('vm-create-snapshot', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + Route::post('vm-create-snapshot', function (\Illuminate\Http\Request $request) { $customer = $request->user(); $payload = $request->validate([ - 'product' => ['required', 'integer'], - 'name' => ['nullable', 'string', 'max:191'], + 'product' => ['required'], 'description' => ['nullable', 'string', 'max:1000'], ]); - $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($payload['product']); + // Kiểm tra nếu là UUID (Service UUID) + $isUuid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $payload['product']); + + if ($isUuid) { + // Tìm qua Service UUID + $service = \App\Models\Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter') + ->where('uuid', $payload['product']) + ->where('customer_id', $customer->id) + ->first(); + + if (!$service || !$service->vmware) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + $product = $service->vmware; + } elseif (is_numeric($payload['product'])) { + $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find((int)$payload['product']); + } else { + $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter') + ->where('vmware_id', $payload['product']) + ->where('customer_id', $customer->id) + ->first(); + } + if (!$product) { return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); } @@ -265,44 +295,68 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () { return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); } - $result = $serviceVCenter->CreateSnapShot($product, $payload['name'] ?? null); - if (is_string($result) || ($result['error'] ?? true)) { - return response()->json([ - 'error' => true, - 'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể tạo snapshot'), - ], 400); - } + // Auto-generate snapshot name: {vm_name}-{date}-{count} + $today = now()->format('d-m-Y'); + $existingCount = \App\Models\ModuleSnapshot::where('module_vmware_id', $product->id) + ->whereDate('created_at', today()) + ->count(); + $snapshotName = sprintf('%s-%s-%d', $product->name, $today, $existingCount + 1); - $snapshotName = $result['data'] ?? ($payload['name'] ?? null); + // Create snapshot record with pending status $snapshot = \App\Models\ModuleSnapshot::create([ 'name' => $snapshotName, 'description' => $payload['description'] ?? null, 'customer_id' => $customer->id, 'module_vmware_id' => $product->id, - 'status' => 'success', - 'snapshot_ref' => $snapshotName, + 'status' => 'pending', + 'snapshot_ref' => null, ]); + // Dispatch job to create snapshot + \App\Jobs\VirtualMachine\Snapshot\CreateSnapshot::dispatch($snapshot->id); + return response()->json([ 'error' => false, - 'message' => 'Đã tạo snapshot thành công', + 'message' => 'Đã gửi yêu cầu tạo snapshot vào hàng đợi', 'data' => [ 'id' => $snapshot->id, 'name' => $snapshot->name, 'description' => $snapshot->description, - 'snapshot_ref' => $snapshot->snapshot_ref, + 'status' => 'pending', ], ]); }); Route::get('vm-snapshots', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { $customer = $request->user(); - $productId = (int)$request->query('product'); - if (!$productId) { + $productParam = $request->query('product'); + if (!$productParam) { return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422); } - $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($productId); + // Kiểm tra nếu là UUID (Service UUID) + $isUuid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $productParam); + + if ($isUuid) { + // Tìm qua Service UUID + $service = \App\Models\Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter') + ->where('uuid', $productParam) + ->where('customer_id', $customer->id) + ->first(); + + if (!$service || !$service->vmware) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + $product = $service->vmware; + } elseif (is_numeric($productParam)) { + $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find((int)$productParam); + } else { + $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter') + ->where('vmware_id', $productParam) + ->where('customer_id', $customer->id) + ->first(); + } + if (!$product) { return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); } @@ -374,14 +428,35 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () { ]); }); - Route::post('vm-revert-snapshot', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + Route::post('vm-revert-snapshot', function (\Illuminate\Http\Request $request) { $customer = $request->user(); $payload = $request->validate([ - 'product' => ['required', 'integer'], - 'snapshot' => ['required', 'string'], + 'product' => ['required'], + 'snapshot_id' => ['required', 'numeric'], ]); - $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($payload['product']); + // Kiểm tra nếu là UUID (Service UUID) + $isUuid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $payload['product']); + + if ($isUuid) { + // Tìm qua Service UUID + $service = \App\Models\Service::with('vmware') + ->where('uuid', $payload['product']) + ->where('customer_id', $customer->id) + ->first(); + + if (!$service || !$service->vmware) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + $product = $service->vmware; + } elseif (is_numeric($payload['product'])) { + $product = \App\Models\ModuleVmware::find((int)$payload['product']); + } else { + $product = \App\Models\ModuleVmware::where('vmware_id', $payload['product']) + ->where('customer_id', $customer->id) + ->first(); + } + if (!$product) { return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); } @@ -389,28 +464,54 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () { return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); } - $result = $serviceVCenter->RevertSnapShot($product, $payload['snapshot']); - if (is_string($result) || ($result['error'] ?? true)) { - return response()->json([ - 'error' => true, - 'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể revert snapshot'), - ], 400); + // Find snapshot + $snapshot = \App\Models\ModuleSnapshot::where('id', $payload['snapshot_id']) + ->where('module_vmware_id', $product->id) + ->where('customer_id', $customer->id) + ->first(); + + if (!$snapshot) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy snapshot'], 404); } + // Dispatch job + \App\Jobs\VirtualMachine\Snapshot\RevertSnapshot::dispatch($product->id, $snapshot->id); + return response()->json([ 'error' => false, - 'message' => 'Đã gửi lệnh revert snapshot', + 'message' => 'Đã gửi yêu cầu revert snapshot vào hàng đợi', ]); }); - Route::post('vm-delete-snapshot', function (\Illuminate\Http\Request $request, \App\Http\Services\vCenter $serviceVCenter) { + Route::post('vm-delete-snapshot', function (\Illuminate\Http\Request $request) { $customer = $request->user(); $payload = $request->validate([ - 'product' => ['required', 'integer'], - 'snapshot' => ['required', 'string'], + 'product' => ['required'], + 'snapshot_id' => ['required', 'numeric'], ]); - $product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($payload['product']); + // Kiểm tra nếu là UUID (Service UUID) + $isUuid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $payload['product']); + + if ($isUuid) { + // Tìm qua Service UUID + $service = \App\Models\Service::with('vmware') + ->where('uuid', $payload['product']) + ->where('customer_id', $customer->id) + ->first(); + + if (!$service || !$service->vmware) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); + } + $product = $service->vmware; + } elseif (is_numeric($payload['product'])) { + $product = \App\Models\ModuleVmware::find((int)$payload['product']); + } else { + $product = \App\Models\ModuleVmware::where('vmware_id', $payload['product']) + ->where('customer_id', $customer->id) + ->first(); + } + if (!$product) { return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); } @@ -418,25 +519,22 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () { return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); } - $result = $serviceVCenter->RemoveSnapShot($product, $payload['snapshot']); - if (is_string($result) || ($result['error'] ?? true)) { - return response()->json([ - 'error' => true, - 'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể xoá snapshot'), - ], 400); - } - - \App\Models\ModuleSnapshot::query() + // Find snapshot + $snapshot = \App\Models\ModuleSnapshot::where('id', $payload['snapshot_id']) ->where('module_vmware_id', $product->id) ->where('customer_id', $customer->id) - ->where(function ($q) use ($payload) { - $q->where('snapshot_ref', $payload['snapshot'])->orWhere('name', $payload['snapshot']); - }) - ->delete(); + ->first(); + + if (!$snapshot) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy snapshot'], 404); + } + + // Dispatch job + \App\Jobs\VirtualMachine\Snapshot\RemoveSnapshot::dispatch($product->id, $snapshot->id); return response()->json([ 'error' => false, - 'message' => 'Đã xoá snapshot thành công', + 'message' => 'Đã gửi yêu cầu xóa snapshot vào hàng đợi', ]); });