client = $client; $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 $snapshotName = null): string|array { $vcenter = $this->resolveVCenterFromProduct($product); if (!$vcenter) { return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; } $loginResult = $this->client->loginServer($vcenter); if (!$loginResult) { return "Login_Again"; } $vmName = (string) $product->name; $name = $snapshotName ?? ($vmName . '_' . date('d_m_Y_H_i_s')); $check = $this->client->checkApiExits(); if (!$check) { return "Login_Again"; } try { Log::channel('Custom')->info("==================================================================="); Log::channel('Custom')->info("Create Snapshot with VirtualMachine Name - $vmName - Snap #$name"); $command = "& 'C:\CLISave\createsnapshot.ps1' -vm_name " . PowerShellEscaper::singleQuote($vmName) . ' -name ' . PowerShellEscaper::singleQuote($name); Log::channel('Custom')->info($command); $cmdResult = $this->powerShellService->executePowerCLI($command); Log::channel('Custom')->info("Command Result: " . $cmdResult); 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("After run Command |" . now()); Log::channel('Custom')->info("==================================================================="); $this->clearSnapshotCache($product); return ['error' => false, 'data' => $name]; } catch (\Exception $error) { Log::channel('Custom')->error("Create Snapshot error: " . $error->getMessage()); return ['error' => true, 'message' => $error->getMessage()]; } } private function clearSnapshotCache($product): void { $cacheKey = CacheKey::build('snapshot', 'list', ['vm_id' => $product->id]); Cache::forget($cacheKey); Log::channel('Custom')->info("🗑️ Cleared snapshot cache for VM: {$product->name}"); } public function getSnapShot($product): string|array { // ... (Similar logic, using SOAP) // Since SOAP logic is quite complex and self-contained in vCenter.php, // I'll copy it here but need to adapt $this->callSoap etc. // Actually, callSoap uses curl with manual implementation. // I should probably simplify or copy that helper too. // This method heavily relies on internal SOAP helpers. // For simplicity in this 'splitting' task, I will implement the public method // and add the private helpers it depends on. try { $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()]; } $vmId = $product->vmware_id; $cacheKey = CacheKey::build('snapshot', 'list', ['vm_id' => $product->id]); $cachedResult = Cache::get($cacheKey); if ($cachedResult !== null) { return $cachedResult; } try { $sessionToken = $this->soapLogin(); if (!$sessionToken) { return "Login_Again"; } $snapshotInfo = $this->getVMSnapshotInfoSoap($sessionToken, $vmId); $snapshots = []; if (isset($snapshotInfo['rootSnapshotList'])) { $roots = $snapshotInfo['rootSnapshotList']; if (isset($roots['snapshot'])) { $snapshots = $this->flattenSnapshotTree($roots); } elseif (is_array($roots)) { foreach ($roots as $root) { $snapshots = array_merge($snapshots, $this->flattenSnapshotTree($root)); } } } $formattedSnapshots = []; foreach ($snapshots as $snap) { $formattedSnapshots[] = [ 'name' => $snap['name'], 'description' => $snap['description'] ?? '', 'created' => $snap['createTime'], 'id' => $snap['snapshot']['_'] ?? $snap['snapshot'], 'is_current' => false, 'parent_snapshot_id' => '', 'size_gb' => 0 ]; } $response = [ 'error' => false, 'data' => $formattedSnapshots ]; Cache::put($cacheKey, $response, now()->addMinutes(5)); return $response; } catch (\Exception $e) { Log::channel('Custom')->error("❌ SOAP Snapshot Error: " . $e->getMessage()); return [ 'error' => true, 'message' => 'Lỗi lấy snapshot qua SOAP: ' . $e->getMessage() ]; } } private function soapLogin() { $username = $this->client->getUsername(); $password = $this->client->getPassword(); $body = ' <_this type="SessionManager">SessionManager ' . htmlspecialchars($username) . ' ' . htmlspecialchars($password) . ' '; $result = $this->callSoap($body); return $result['cookie'] ?? null; } private function getVMSnapshotInfoSoap($cookie, $vmId) { $body = ' <_this type="PropertyCollector">propertyCollector VirtualMachine false snapshot ' . $vmId . ' '; $result = $this->callSoap($body, $cookie); if (isset($result['body'])) { try { $xml = simplexml_load_string($result['body']); if ($xml === false) return []; $xml->registerXPathNamespace('u', 'urn:vim25'); $json = json_encode($xml); $array = json_decode($json, true); return $this->findSnapshotPropertyInArray($array); } catch (\Exception $e) { return []; } } return []; } private function findSnapshotPropertyInArray($array) { if (isset($array['rootSnapshotList'])) { return $array; } foreach ($array as $key => $value) { if (is_array($value)) { $found = $this->findSnapshotPropertyInArray($value); if ($found) { return $found; } } } return []; } private function flattenSnapshotTree($node) { $list = []; $list[] = $node; if (isset($node['childSnapshotList'])) { $children = $node['childSnapshotList']; // Check if 'snapshot' key exists directly (single child wrapped) if (isset($children['snapshot'])) { // If it's an array of snapshots or single object? // XML conversion: if strictly one child, might be direct. if multiple, array. // But simplexml usually produces array if multiple same tags. // However, structure is usually childSnapshotList -> snapshot (array or single) $snaps = $children['snapshot']; // If $snaps has numeric keys, it's array. Else single. if (array_keys($snaps) !== range(0, count($snaps) - 1)) { $snaps = [$snaps]; } foreach ($snaps as $child) { $list = array_merge($list, $this->flattenSnapshotTree($child)); } } } return $list; } public function RemoveSnapShot($product, $snap = ''): string|array { $vcenter = $this->resolveVCenterFromProduct($product); if (!$vcenter) { return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; } $loginResult = $this->client->loginServer($vcenter); if (!$loginResult) { return "Login_Again"; } $vmware_name = (string) $product->name; $check = $this->client->checkApiExits(); if ($check) { try { Log::channel('Custom')->info("==================================================================="); Log::channel('Custom')->info("Remove Snapshot with VirtualMachine Name - $vmware_name - Snap #$snap"); Log::channel('Custom')->info("\r\n"); $Command = "& 'C:\CLISave\deletesnap.ps1' -vm_name " . PowerShellEscaper::singleQuote($vmware_name) . ' -name ' . PowerShellEscaper::singleQuote((string) $snap); Log::channel('Custom')->info("$Command"); $cmdResult = $this->powerShellService->executePowerCLI($Command); Log::channel('Custom')->info("Command Result: " . $cmdResult); 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"); Log::channel('Custom')->info("After run Command |" . now()); Log::channel('Custom')->info("==================================================================="); $this->clearSnapshotCache($product); return [ 'error' => false ]; } catch (\Exception $error) { Log::channel('Custom')->info("\r\n"); Log::channel('Custom')->info("==================================================================="); Log::channel('Custom')->info("Error Log: "); Log::channel('Custom')->info($error->getMessage()); Log::channel('Custom')->info("==================================================================="); return [ 'error' => true, 'message' => $error->getMessage() ]; } } return "Login_Again"; } public function RevertSnapShot($product, $snap = ''): string|array { $vcenter = $this->resolveVCenterFromProduct($product); if (!$vcenter) { return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; } $loginResult = $this->client->loginServer($vcenter); if (!$loginResult) { return "Login_Again"; } $vmware_name = (string) $product->name; $check = $this->client->checkApiExits(); if ($check) { try { Log::channel('Custom')->info("==================================================================="); Log::channel('Custom')->info("Revert Snapshot with VirtualMachine Name - $vmware_name - Snap #$snap"); Log::channel('Custom')->info("\r\n"); $Command = "& 'C:\CLISave\backSnap.ps1' -vm_name " . PowerShellEscaper::singleQuote($vmware_name) . ' -name ' . PowerShellEscaper::singleQuote((string) $snap); Log::channel('Custom')->info("$Command"); $cmdResult = $this->powerShellService->executePowerCLI($Command); Log::channel('Custom')->info("Command Result: " . $cmdResult); 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"); Log::channel('Custom')->info("After run Command |" . now()); Log::channel('Custom')->info("==================================================================="); $this->clearSnapshotCache($product); return [ 'error' => false ]; } catch (\Exception $error) { Log::channel('Custom')->info("\r\n"); Log::channel('Custom')->info("==================================================================="); Log::channel('Custom')->info("Error Log: "); Log::channel('Custom')->info($error->getMessage()); Log::channel('Custom')->info("==================================================================="); return [ 'error' => true, 'message' => $error->getMessage() ]; } } return "Login_Again"; } private function callSoap($soapBody, $cookie = null) { $curl = curl_init(); $host = $this->client->apiUri; $headers = [ 'Content-Type: text/xml', 'SOAPAction: "urn:vim25/2.0"' ]; if ($cookie) { $headers[] = "Cookie: $cookie"; } $payload = ' ' . $soapBody . ' '; curl_setopt_array($curl, [ CURLOPT_URL => "https://$host/sdk", CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => $headers, CURLOPT_HEADER => true // We need headers for cookie ] + TlsHttpClientFactory::vCenterCurlOptions()); $response = curl_exec($curl); $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE); $header = substr($response, 0, $header_size); $body = substr($response, $header_size); curl_close($curl); // Parse cookie $cookieValue = null; if (preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $header, $matches)) { foreach ($matches[1] as $c) { if (strpos($c, 'vmware_soap_session') !== false) { $cookieValue = $c; break; } } } return ['body' => $body, 'cookie' => $cookieValue]; } }