Files
portal-api/app/Http/Services/vCenter/SnapshotService.php
2026-09-28 08:26:29 +07:00

479 lines
17 KiB
PHP

<?php
namespace App\Http\Services\vCenter;
use App\Support\Cache\CacheKey;
use App\Support\PowerShellEscaper;
use App\Support\TlsHttpClientFactory;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class SnapshotService
{
protected Client $client;
protected PowerShellService $powerShellService;
public function __construct(Client $client, PowerShellService $powerShellService)
{
$this->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 = '<soapenv:Body>
<Login xmlns="urn:vim25">
<_this type="SessionManager">SessionManager</_this>
<userName>' . htmlspecialchars($username) . '</userName>
<password>' . htmlspecialchars($password) . '</password>
</Login>
</soapenv:Body>';
$result = $this->callSoap($body);
return $result['cookie'] ?? null;
}
private function getVMSnapshotInfoSoap($cookie, $vmId)
{
$body = '<soapenv:Body>
<RetrievePropertiesEx xmlns="urn:vim25">
<_this type="PropertyCollector">propertyCollector</_this>
<specSet>
<propSet>
<type>VirtualMachine</type>
<all>false</all>
<pathSet>snapshot</pathSet>
</propSet>
<objectSet>
<obj type="VirtualMachine">' . $vmId . '</obj>
</objectSet>
</specSet>
<options></options>
</RetrievePropertiesEx>
</soapenv:Body>';
$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 = '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:vim25">
' . $soapBody . '
</soapenv:Envelope>';
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];
}
}