Files
portal-api/app/Http/Services/vCenter/SnapshotService.php
T
pbao9 bb73d6c5a2 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.
2026-04-15 23:38:38 +07:00

351 lines
12 KiB
PHP

<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use App\Http\Facade\SignRSA;
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|array
{
try {
// Note: credentials setup logic from original vCenter.php seemed redundant with Client logic,
// 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 = $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()];
}
$vmware_name = (string) $product->name;
// In original code, it checks checkApiExits(), if true proceed.
if ($this->client->checkApiExits()) {
try {
$Snapshot = $vmware_name . "_" . date('d_m_Y_H_i_s');
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Create Snapshot with VirtualMachine Name - $vmware_name - Snap #$Snapshot");
$Command = "\$vm_name = '$vmware_name';\$name = '$Snapshot'; cd /; C:\CLISave\createsnapshot.ps1;";
Log::channel('Custom')->info("$Command");
$this->powerShellService->executePowerCLI($Command);
$this->clearSnapshotCache($product);
return [
'error' => false,
'data' => $Snapshot
];
} catch (\Exception $error) {
return [
'error' => true,
'message' => $error->getMessage()
];
}
}
return "Login_Again";
}
private function clearSnapshotCache($product): void
{
$vmware_name = (string) $product->name;
$cacheKey = "snapshots_{$product->id}_{$vmware_name}";
Cache::forget($cacheKey);
Log::channel('Custom')->info("🗑️ Cleared snapshot cache for VM: {$vmware_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()];
}
$vmware_name = (string) $product->name;
$vmId = $product->vmware_id;
$cacheKey = "snapshots_{$product->id}_{$vmware_name}";
$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;
}
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, array(
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_SSL_VERIFYHOST => false,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HEADER => true // We need headers for cookie
));
$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];
}
}