feat: Implement asynchronous VM power operations and refactor vCenter snapshot retrieval to use API calls.

This commit is contained in:
pbao9
2026-01-27 16:35:42 +07:00
parent bfb5f9f4a5
commit 057ba58760
16 changed files with 2869 additions and 2521 deletions
@@ -422,122 +422,6 @@ class ServiceController extends Controller
}
}
/**
* Lấy danh sách snapshot với bảo mật và tối ưu hóa
*/
// public function getSnapshots(Request $request, vCenter $serviceVCenter)
// {
// try {
// // Kiểm tra authentication
// $user = customer();
// if (!$user) {
// return response()->json([
// 'error' => true,
// 'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.')
// ], 401);
// }
// // Validation với sanitization
// $validator = Validator::make($request->all(), [
// 'product_id' => 'required|string|max:255',
// ]);
// if ($validator->fails()) {
// return response()->json([
// 'error' => true,
// 'message' => __('Dữ liệu không hợp lệ'),
// 'errors' => $validator->errors()
// ], 422);
// }
// $productId = $this->sanitizeInput($request->product_id);
// $userId = $user->id;
// // Cache key cho snapshot data
// $cacheKey = "snapshots_{$userId}_{$productId}";
// $cacheDuration = 300; // 5 phút
// // Thử lấy từ cache trước
// if (Cache::has($cacheKey)) {
// $cachedData = Cache::get($cacheKey);
// if ($cachedData && !$this->isCacheExpired($cachedData)) {
// return response()->json([
// 'error' => false,
// 'message' => __('Lấy danh sách snapshot thành công'),
// 'data' => $cachedData,
// 'cached' => true
// ]);
// }
// }
// // Tìm VPS với eager loading tối ưu
// $product = $this->findVmwareProduct($productId, $userId);
// if (!$product) {
// return response()->json([
// 'error' => true,
// 'message' => __('Không tìm thấy VPS hoặc bạn không có quyền truy cập')
// ], 404);
// }
// // Kiểm tra cấu hình vCenter
// $vcenter = $this->validateVcenterConfiguration($product);
// if (!$vcenter) {
// return response()->json([
// 'error' => true,
// 'message' => __('Cấu hình vCenter không hợp lệ')
// ], 400);
// }
// // Lấy snapshot từ vCenter
// $snapshots = $this->fetchSnapshotsFromVCenter($serviceVCenter, $product, $vcenter);
// if ($snapshots === false) {
// return response()->json([
// 'error' => true,
// 'message' => __('Không thể kết nối đến vCenter')
// ], 500);
// }
// // Lấy thông tin bổ sung từ database
// $dbSnapshots = $this->getDatabaseSnapshots($userId, $product->id);
// // Merge và sanitize data
// $processedSnapshots = $this->processSnapshotsData($snapshots, $dbSnapshots);
// // Chuẩn bị response data
// $responseData = [
// 'snapshots' => $processedSnapshots,
// 'total_count' => count($processedSnapshots),
// 'vm_name' => $this->sanitizeOutput($product->name),
// 'vm_id' => $product->vmware_id,
// 'last_updated' => now()->format('Y-m-d H:i:s')
// ];
// // Cache kết quả
// Cache::put($cacheKey, $responseData, $cacheDuration);
// return response()->json([
// 'error' => false,
// 'message' => __('Lấy danh sách snapshot thành công'),
// 'data' => $responseData,
// 'cached' => false
// ]);
// } catch (\Exception $e) {
// Log::error('Lỗi khi lấy danh sách snapshot', [
// 'user_id' => $user->id ?? 'guest',
// 'product_id' => $request->product_id ?? 'not_provided',
// 'error' => $e->getMessage(),
// 'file' => $e->getFile(),
// 'line' => $e->getLine()
// ]);
// return response()->json([
// 'error' => true,
// 'message' => __('Có lỗi xảy ra khi lấy danh sách snapshot')
// ], 500);
// }
// }
public function getSnapshots(Request $request)
{
$user = customer();
@@ -1002,7 +886,10 @@ class ServiceController extends Controller
/**
* Secure start VM using session authentication - NO BEARER TOKEN
*/
public function startVM(Request $request, vCenter $vCenterService)
/**
* Secure start VM using session authentication - NO BEARER TOKEN
*/
public function startVM(Request $request)
{
try {
$user = customer();
@@ -1037,28 +924,19 @@ class ServiceController extends Controller
], 404);
}
$result = $vCenterService->StartVirtualMachine($product, $product->vmware_id);
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
// Clear VM info cache after starting VM
Cache::forget('vm_info_' . $product->vmware_id);
return response()->json([
'error' => false,
'message' => __('Yêu cầu khởi động máy ảo đã được gửi thành công.')
]);
} else {
$msg = is_array($result) && isset($result['data']) ? $result['data'] : __('Không thể khởi động máy ảo');
return response()->json([
'error' => true,
'message' => $msg
], 500);
}
// Dispatch Job
\App\Jobs\VirtualMachine\Power\StartVmJob::dispatch($product->id, $userId);
return response()->json([
'error' => false,
'message' => __('Yêu cầu khởi động máy ảo đã được gửi. Vui lòng chờ trong giây lát.')
]);
} catch (\Exception $e) {
Log::error('Lỗi khi khởi động máy ảo', [
'user_id' => $user->id ?? 'guest',
'product_id' => $request->product_id ?? 'not_provided',
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
'error' => $e->getMessage()
]);
return response()->json([
'error' => true,
@@ -1070,7 +948,7 @@ class ServiceController extends Controller
/**
* Secure stop VM using session authentication - NO BEARER TOKEN
*/
public function stopVM(Request $request, vCenter $vCenterService)
public function stopVM(Request $request)
{
try {
$user = customer();
@@ -1105,27 +983,19 @@ class ServiceController extends Controller
], 404);
}
$result = $vCenterService->ShutdownGuestVirtualMachine($product, $product->vmware_id);
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
Cache::forget('the_gioi_so_database_the_gioi_so_cache:vm_info_' . $product->vmware_id);
return response()->json([
'error' => false,
'message' => __('Yêu cầu tắt máy ảo đã được gửi thành công.')
]);
} else {
$msg = is_array($result) && isset($result['data']) ? $result['data'] : __('Không thể tắt máy ảo');
return response()->json([
'error' => true,
'message' => $msg
], 500);
}
// Dispatch Job
\App\Jobs\VirtualMachine\Power\StopVmJob::dispatch($product->id, $userId);
return response()->json([
'error' => false,
'message' => __('Yêu cầu tắt máy ảo đã được gửi. Vui lòng chờ trong giây lát.')
]);
} catch (\Exception $e) {
Log::error('Lỗi khi tắt máy ảo', [
'user_id' => $user->id ?? 'guest',
'product_id' => $request->product_id ?? 'not_provided',
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
'error' => $e->getMessage()
]);
return response()->json([
'error' => true,
@@ -1137,7 +1007,7 @@ class ServiceController extends Controller
/**
* Secure reboot VM using session authentication - NO BEARER TOKEN
*/
public function rebootVM(Request $request, vCenter $vCenterService)
public function rebootVM(Request $request)
{
try {
$user = customer();
@@ -1172,26 +1042,19 @@ class ServiceController extends Controller
], 404);
}
$result = $vCenterService->RebootVirtualMachine($product, $product->vmware_id);
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
return response()->json([
'error' => false,
'message' => __('Yêu cầu khởi động lại máy ảo đã được gửi thành công.')
]);
} else {
$msg = is_array($result) && isset($result['data']) ? $result['data'] : __('Không thể khởi động lại máy ảo');
return response()->json([
'error' => true,
'message' => $msg
], 500);
}
// Dispatch Job
\App\Jobs\VirtualMachine\Power\RebootVmJob::dispatch($product->id, $userId);
return response()->json([
'error' => false,
'message' => __('Yêu cầu khởi động lại máy ảo đã được gửi. Vui lòng chờ trong giây lát.')
]);
} catch (\Exception $e) {
Log::error('Lỗi khi reboot VM', [
'user_id' => $user->id ?? 'guest',
'product_id' => $request->product_id ?? 'not_provided',
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
'error' => $e->getMessage()
]);
return response()->json([
'error' => true,
-25
View File
@@ -115,22 +115,6 @@ class VPSReinstallModal extends Component
// Try to get IP from vCenter API first, fallback to database
$this->vpsIpAddress = $this->getIPFromVCenter() ?? $this->moduleVmware?->ipAddress?->ip ?? null;
// Debug ModuleVmware info
Log::info('ModuleVmware debug info', [
'invoice_item_id' => $this->invoiceItem->id,
'invoice_item_relid' => $this->invoiceItem->relid,
'invoice_item_type_id' => $this->invoiceItem->type_id,
'invoice_customer_id' => $this->invoice?->customer_id ?? 'null',
'module_vmware' => $this->moduleVmware ? 'found' : 'not_found',
'module_vmware_id' => $this->moduleVmware?->id ?? 'null',
'module_vmware_name' => $this->moduleVmware?->name ?? 'null',
'module_vmware_customer_id' => $this->moduleVmware?->customer_id ?? 'null',
'module_ip_address_id' => $this->moduleVmware?->module_ip_address_id ?? 'null',
'ip_address_relationship' => $this->moduleVmware?->ipAddress ? 'found' : 'not_found',
'ip_address_from_db' => $this->moduleVmware?->ipAddress?->ip ?? 'null',
'ip_address_from_vcenter' => $this->vpsIpAddress ?? 'null'
]);
} else {
Log::warning('InvoiceItem not found', [
'invoice_id' => $this->invoiceId,
@@ -139,15 +123,6 @@ class VPSReinstallModal extends Component
]);
}
}
Log::info('OS data loaded successfully', [
'product_id' => $productId,
'os_groups_count' => is_object($this->osGroups) ? $this->osGroups->count() : count($this->osGroups),
'current_os' => $this->currentOs ? $this->currentOs->id : null,
'module_vmware_id' => $this->moduleVmware ? $this->moduleVmware->id : null,
'ip_address' => $this->moduleVmware?->ipAddress?->ip ?? 'N/A'
]);
} catch (\Exception $e) {
Log::error('Error loading OS data', [
'product_id' => $productId,
+175 -101
View File
@@ -861,13 +861,6 @@ PS;
'hot_add_enabled' => true
]
];
Log::info("📤 Payload CPU", [
'payload' => $payload,
'cpu_count' => $cpuCount,
'cores_per_socket' => $coresPerSocket
]);
$response = Http::withoutVerifying()
->timeout(30)
->withHeaders([
@@ -1627,120 +1620,201 @@ PS;
}
$vmware_name = (string) $product->name;
$vmId = $product->vmware_id;
// Check cache first
$cacheKey = "snapshots_{$product->id}_{$vmware_name}";
$cachedResult = Cache::get($cacheKey);
if ($cachedResult !== null) {
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Snapshot cache hit - Execution time: {$executionTime}ms");
Log::channel('Custom')->info("📸 Snapshot cache hit");
return $cachedResult;
}
$check = $this->checkApiExits();
try {
Log::channel('Custom')->info("🚀 Starting SOAP Snapshot Fetch for VM: $vmId ($vmware_name)");
if ($check) {
try {
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Get Snapshots for VirtualMachine Name - $vmware_name");
Log::channel('Custom')->info("\r\n");
// 1. SOAP Login
$sessionToken = $this->soapLogin();
if (!$sessionToken) {
return "Login_Again";
}
// Use the getsnapshot.ps1 script file
$Command = "\$vm_name = '$vmware_name'; cd /; C:\CLISave\getsnapshot.ps1;";
// 2. Retrieve Snapshot Info
$snapshotInfo = $this->getVMSnapshotInfoSoap($sessionToken, $vmId);
Log::channel('Custom')->info("$Command");
$commandStartTime = microtime(true);
$result = $this->Command($Command);
$commandTime = round((microtime(true) - $commandStartTime) * 1000, 2);
Log::channel('Custom')->info("\r\n");
Log::channel('Custom')->info("Command Result: " . $result);
Log::channel('Custom')->info("Command Execution Time: {$commandTime}ms");
Log::channel('Custom')->info("After run Command |" . Carbon::now());
Log::channel('Custom')->info("===================================================================");
// Parse result
if ($result && $result !== "Login_Fail") {
try {
// Clean up the result - remove any PowerShell formatting
$cleanResult = trim($result);
// Parse the text output from the script
$snapshots = $this->parseSnapshotTextOutput($cleanResult);
if (!empty($snapshots)) {
$response = [
'error' => false,
'data' => $snapshots
];
// Cache the result for 5 minutes
Cache::put($cacheKey, $response, now()->addMinutes(5));
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Snapshot retrieved successfully - Total time: {$totalTime}ms, Command time: {$commandTime}ms");
return $response;
} else {
$response = [
'error' => true,
'message' => 'Không tìm thấy snapshot nào hoặc lỗi parse dữ liệu',
'raw_result' => $result
];
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 No snapshots found - Total time: {$totalTime}ms");
return $response;
}
} catch (\Exception $parseError) {
Log::channel('Custom')->info("Parse Error: " . $parseError->getMessage());
$response = [
'error' => true,
'message' => 'Lỗi parse kết quả snapshot: ' . $parseError->getMessage(),
'raw_result' => $result
];
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Parse error - Total time: {$totalTime}ms");
return $response;
// 3. Flatten Tree
$snapshots = [];
if (isset($snapshotInfo['rootSnapshotList'])) {
// Handle single object vs array in XML to JSON conversion
$roots = $snapshotInfo['rootSnapshotList'];
// Ensure it's an array if single
if (isset($roots['snapshot'])) {
// It's a single node structure, allow recurse
$snapshots = $this->flattenSnapshotTree($roots);
} elseif (is_array($roots)) {
foreach ($roots as $root) {
$snapshots = array_merge($snapshots, $this->flattenSnapshotTree($root));
}
} else {
$response = [
'error' => true,
'message' => 'Không thể thực thi lệnh hoặc đăng nhập thất bại',
'raw_result' => $result
];
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Command failed - Total time: {$totalTime}ms");
return $response;
}
} 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("===================================================================");
}
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Exception occurred - Total time: {$totalTime}ms");
return [
'error' => true,
'message' => $error->getMessage()
// Format similar to old output
$formattedSnapshots = [];
foreach ($snapshots as $snap) {
$formattedSnapshots[] = [
'name' => $snap['name'],
'description' => $snap['description'] ?? '',
'created' => $snap['createTime'],
'id' => $snap['snapshot']['_'] ?? $snap['snapshot'], // MO Ref Value
'is_current' => false, // Simplification, need logic to check against currentSnapshot
'parent_snapshot_id' => '', // Hard to determining flat parent in simple recursion without passing parent id
'size_gb' => 0 // SOAP doesnt always return size in tree
];
}
$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()
{
$body = '
<soapenv:Body>
<Login xmlns="urn:vim25">
<_this type="SessionManager">SessionManager</_this>
<userName>' . htmlspecialchars($this->username) . '</userName>
<password>' . htmlspecialchars($this->password) . '</password>
</Login>
</soapenv:Body>';
$response = $this->callSoap($body);
// Extract session cookie if needed, but Login typically sets checking cookie.
// Actually for Http Client, we need to grab the cookie "vmware_soap_session"
// But the callSoap helper needs to handle cookies.
return $response['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);
// Parse simple XML response to finding snapshot info
// This is tricky with Regex/SimpleXML. We need to extract the returnval.
if (isset($result['body'])) {
$xml = simplexml_load_string($result['body']);
$xml->registerXPathNamespace('u', 'urn:vim25');
// Navigate to returnval -> objects -> propSet -> val -> rootSnapshotList
// Path depends on exact response structure
// Simplified return for now:
$json = json_encode($xml);
$array = json_decode($json, true);
// Deep dive into array to find "snapshot" property
// This needs robust traversing
return $this->findSnapshotPropertyInArray($array);
}
return [];
}
private function findSnapshotPropertyInArray($array)
{
// Recursive search for 'rootSnapshotList' or 'currentSnapshot'
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 = [];
// Add self
$list[] = $node;
// Add children
if (isset($node['childSnapshotList'])) {
$children = $node['childSnapshotList'];
// Check if single child or array of children
if (isset($children['snapshot'])) {
$list = array_merge($list, $this->flattenSnapshotTree($children));
} else {
foreach ($children as $child) {
$list = array_merge($list, $this->flattenSnapshotTree($child));
}
}
}
return $list;
}
private function callSoap($soapBody, $cookie = null)
{
$envelope = '<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
' . $soapBody . '
</soapenv:Envelope>';
$headers = [
'Content-Type' => 'text/xml; charset=utf-8',
'SOAPAction' => 'urn:vim25/2005'
];
if ($cookie) {
$headers['Cookie'] = $cookie;
}
$totalTime = round((microtime(true) - $startTime) * 1000, 2);
Log::channel('Custom')->info("📸 Login again required - Total time: {$totalTime}ms");
$response = Http::withoutVerifying()
->withHeaders($headers)
->post("https://{$this->apiUri}/sdk", $envelope);
return "Login_Again";
$responseCookie = $response->header('Set-Cookie');
return [
'body' => $response->body(),
'cookie' => $responseCookie ? $responseCookie : $cookie
];
}