feat: Implement asynchronous VM power operations and refactor vCenter snapshot retrieval to use API calls.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class VirtualMachineStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $message;
|
||||
public $vmId;
|
||||
public $status;
|
||||
public $userId;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($userId, $vmId, $status, $message)
|
||||
{
|
||||
$this->userId = $userId;
|
||||
$this->vmId = $vmId;
|
||||
$this->status = $status;
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return \Illuminate\Broadcasting\Channel|array
|
||||
*/
|
||||
public function broadcastOn()
|
||||
{
|
||||
// Broadcast on a private channel for the user
|
||||
return new PrivateChannel('user.' . $this->userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The event's broadcast name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'vm.status.changed';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Power;
|
||||
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\VirtualMachine\Handle\WaitStatusVirtualMachine;
|
||||
use App\Models\ModuleVmware;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RebootVmJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 120;
|
||||
public $tries = 3;
|
||||
|
||||
protected $productId;
|
||||
protected $userId;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($productId, $userId)
|
||||
{
|
||||
$this->productId = $productId;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(vCenter $vCenterService)
|
||||
{
|
||||
Log::info("Job RebootVmJob started for Product ID: {$this->productId}");
|
||||
|
||||
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')->find($this->productId);
|
||||
|
||||
if (!$product) {
|
||||
Log::error("RebootVmJob: Product not found ID: {$this->productId}");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $vCenterService->RebootVirtualMachine($product, $product->vmware_id);
|
||||
|
||||
if ($result === 'Login_Again') {
|
||||
$vCenterService->loginServer($product->ipAddress->host->datacenter->vCenter);
|
||||
$result = $vCenterService->RebootVirtualMachine($product, $product->vmware_id);
|
||||
}
|
||||
|
||||
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
|
||||
Log::info("RebootVmJob: Command sent successfully for VM {$product->vmware_id}");
|
||||
|
||||
// Clear VM info cache
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
|
||||
|
||||
$product->update(['status' => 3]); // Processing
|
||||
|
||||
// In VMJobs: $jobs = new WaitStatusVirtualMachine($product->vmware_id,$serviceVCenter,$product->customer_id,false);
|
||||
|
||||
dispatch(new WaitStatusVirtualMachine(
|
||||
$product->vmware_id,
|
||||
$vCenterService,
|
||||
$this->userId,
|
||||
false
|
||||
))->delay(now()->addSeconds(5));
|
||||
|
||||
} else {
|
||||
$msg = is_array($result) ? ($result['data'] ?? 'Unknown error') : $result;
|
||||
Log::error("RebootVmJob: Failed for VM {$product->vmware_id}. Error: {$msg}");
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("RebootVmJob: Exception for VM {$product->vmware_id}: " . $e->getMessage());
|
||||
$this->fail($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Power;
|
||||
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\VirtualMachine\Handle\WaitStatusVirtualMachine;
|
||||
use App\Models\ModuleVmware;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class StartVmJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 120; // 2 minutes
|
||||
public $ries = 3;
|
||||
|
||||
protected $productId;
|
||||
protected $userId;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($productId, $userId)
|
||||
{
|
||||
$this->productId = $productId;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(vCenter $vCenterService)
|
||||
{
|
||||
Log::info("Job StartVmJob started for Product ID: {$this->productId}");
|
||||
|
||||
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')->find($this->productId);
|
||||
|
||||
if (!$product) {
|
||||
Log::error("StartVmJob: Product not found ID: {$this->productId}");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Call vCenter Service
|
||||
$result = $vCenterService->StartVirtualMachine($product, $product->vmware_id);
|
||||
|
||||
// Check for Login_Again
|
||||
if ($result === 'Login_Again') {
|
||||
$vCenterService->loginServer($product->ipAddress->host->datacenter->vCenter);
|
||||
$result = $vCenterService->StartVirtualMachine($product, $product->vmware_id);
|
||||
}
|
||||
|
||||
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
|
||||
Log::info("StartVmJob: Command sent successfully for VM {$product->vmware_id}");
|
||||
|
||||
// Clear VM info cache
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
|
||||
|
||||
// Update status to 'processing' (e.g., 3) if needed, or leave it to WaitStatus
|
||||
$product->update(['status' => 3]); // 3 = Processing/Pending
|
||||
|
||||
// Dispatch wait status job
|
||||
// Note: WaitStatusVirtualMachine expects ($vmware_id, $service, $user, $status_to_wait_for?)
|
||||
// Looking at existing code:
|
||||
// $status = 2; // Suspend
|
||||
// $jobs = new WaitStatusVirtualMachine($product->vmware_id,$serviceVCenter,$product->customer_id,true); (true = wait for operations_ready?)
|
||||
|
||||
// For Start:
|
||||
// $jobs = new WaitStatusVirtualMachine($product->vmware_id, $serviceVCenter, $product->customer_id, false);
|
||||
|
||||
dispatch(new WaitStatusVirtualMachine(
|
||||
$product->vmware_id,
|
||||
$vCenterService,
|
||||
$this->userId,
|
||||
false // Expect operations_ready = false initially? Logic in WaitStatus is weird.
|
||||
))->delay(now()->addSeconds(5));
|
||||
|
||||
} else {
|
||||
$msg = is_array($result) ? ($result['data'] ?? 'Unknown error') : $result;
|
||||
Log::error("StartVmJob: Failed for VM {$product->vmware_id}. Error: {$msg}");
|
||||
// Optionally notify user via Pusher/Telegram here
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("StartVmJob: Exception for VM {$product->vmware_id}: " . $e->getMessage());
|
||||
$this->fail($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Power;
|
||||
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\VirtualMachine\Handle\WaitStatusVirtualMachine;
|
||||
use App\Models\ModuleVmware;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class StopVmJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 120;
|
||||
public $tries = 3;
|
||||
|
||||
protected $productId;
|
||||
protected $userId;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($productId, $userId)
|
||||
{
|
||||
$this->productId = $productId;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(vCenter $vCenterService)
|
||||
{
|
||||
Log::info("Job StopVmJob started for Product ID: {$this->productId}");
|
||||
|
||||
$product = ModuleVmware::with('ipAddress.host.datacenter.vCenter')->find($this->productId);
|
||||
|
||||
if (!$product) {
|
||||
Log::error("StopVmJob: Product not found ID: {$this->productId}");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Call vCenter Service - Graceful Shutdown
|
||||
$result = $vCenterService->ShutdownGuestVirtualMachine($product, $product->vmware_id);
|
||||
|
||||
// Check for Login_Again
|
||||
if ($result === 'Login_Again') {
|
||||
$vCenterService->loginServer($product->ipAddress->host->datacenter->vCenter);
|
||||
$result = $vCenterService->ShutdownGuestVirtualMachine($product, $product->vmware_id);
|
||||
}
|
||||
|
||||
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
|
||||
// Clear VM info cache
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
|
||||
|
||||
$product->update(['status' => 3]); // Processing
|
||||
|
||||
dispatch(new WaitStatusVirtualMachine(
|
||||
$product->vmware_id,
|
||||
$vCenterService,
|
||||
$this->userId,
|
||||
true
|
||||
))->delay(now()->addSeconds(5));
|
||||
|
||||
} else {
|
||||
$msg = is_array($result) ? ($result['data'] ?? 'Unknown error') : $result;
|
||||
Log::error("StopVmJob: Failed for VM {$product->vmware_id}. Error: {$msg}");
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("StopVmJob: Exception for VM {$product->vmware_id}: " . $e->getMessage());
|
||||
$this->fail($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
"opcodesio/log-viewer": "^3.18",
|
||||
"phpseclib/phpseclib": "*",
|
||||
"predis/predis": "^2.2",
|
||||
"pusher/pusher-php-server": "^7.2",
|
||||
"spatie/laravel-permission": "^6.3"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
Generated
+159
-2
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f6b92aa9f90f1c65f035ffcbe8f13168",
|
||||
"content-hash": "0b3914c4b479edcf81cb2cd819d1c9d3",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@@ -5095,6 +5095,102 @@
|
||||
},
|
||||
"time": "2020-10-15T08:29:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/sodium_compat",
|
||||
"version": "v2.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paragonie/sodium_compat.git",
|
||||
"reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/4714da6efdc782c06690bc72ce34fae7941c2d9f",
|
||||
"reference": "4714da6efdc782c06690bc72ce34fae7941c2d9f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php-64bit": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"infection/infection": "^0",
|
||||
"nikic/php-fuzzer": "^0",
|
||||
"phpunit/phpunit": "^7|^8|^9|^10|^11",
|
||||
"vimeo/psalm": "^4|^5|^6"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"autoload.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"ParagonIE\\Sodium\\": "namespaced/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"ISC"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paragon Initiative Enterprises",
|
||||
"email": "security@paragonie.com"
|
||||
},
|
||||
{
|
||||
"name": "Frank Denis",
|
||||
"email": "jedisct1@pureftpd.org"
|
||||
}
|
||||
],
|
||||
"description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"BLAKE2b",
|
||||
"ChaCha20",
|
||||
"ChaCha20-Poly1305",
|
||||
"Chapoly",
|
||||
"Curve25519",
|
||||
"Ed25519",
|
||||
"EdDSA",
|
||||
"Edwards-curve Digital Signature Algorithm",
|
||||
"Elliptic Curve Diffie-Hellman",
|
||||
"Poly1305",
|
||||
"Pure-PHP cryptography",
|
||||
"RFC 7748",
|
||||
"RFC 8032",
|
||||
"Salpoly",
|
||||
"Salsa20",
|
||||
"X25519",
|
||||
"XChaCha20-Poly1305",
|
||||
"XSalsa20-Poly1305",
|
||||
"Xchacha20",
|
||||
"Xsalsa20",
|
||||
"aead",
|
||||
"cryptography",
|
||||
"ecdh",
|
||||
"elliptic curve",
|
||||
"elliptic curve cryptography",
|
||||
"encryption",
|
||||
"libsodium",
|
||||
"php",
|
||||
"public-key cryptography",
|
||||
"secret-key cryptography",
|
||||
"side-channel resistant"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/paragonie/sodium_compat/issues",
|
||||
"source": "https://github.com/paragonie/sodium_compat/tree/v2.5.0"
|
||||
},
|
||||
"time": "2025-12-30T16:12:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "1.29.11",
|
||||
@@ -5992,6 +6088,67 @@
|
||||
},
|
||||
"time": "2025-06-23T02:35:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pusher/pusher-php-server",
|
||||
"version": "7.2.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pusher/pusher-http-php.git",
|
||||
"reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7",
|
||||
"reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"paragonie/sodium_compat": "^1.6|^2.0",
|
||||
"php": "^7.3|^8.0",
|
||||
"psr/log": "^1.0|^2.0|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"overtrue/phplint": "^2.3",
|
||||
"phpunit/phpunit": "^9.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Pusher\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "Library for interacting with the Pusher REST API",
|
||||
"keywords": [
|
||||
"events",
|
||||
"messaging",
|
||||
"php-pusher-server",
|
||||
"publish",
|
||||
"push",
|
||||
"pusher",
|
||||
"real time",
|
||||
"real-time",
|
||||
"realtime",
|
||||
"rest",
|
||||
"trigger"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/pusher/pusher-http-php/issues",
|
||||
"source": "https://github.com/pusher/pusher-http-php/tree/7.2.7"
|
||||
},
|
||||
"time": "2025-01-06T10:56:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ralouphie/getallheaders",
|
||||
"version": "3.0.3",
|
||||
@@ -11661,5 +11818,5 @@
|
||||
"ext-ssh2": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
Generated
+18
@@ -19,6 +19,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"pusher-js": "^8.4.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
}
|
||||
},
|
||||
@@ -9039,6 +9040,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pusher-js": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
|
||||
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
@@ -10496,6 +10507,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||
"dev": true,
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"pusher-js": "^8.4.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
# Performance & Production Readiness Audit
|
||||
|
||||
**Date:** 2026-01-27
|
||||
**Auditor:** Antigravity (AI)
|
||||
**Target:** Laravel 10 / PHP 8.1+ / MySQL / Redis
|
||||
|
||||
---
|
||||
|
||||
## 1. CRITICAL PERFORMANCE RISKS (Must fix before production)
|
||||
|
||||
### 🔴 Synchronous vCenter Calls in Livewire
|
||||
**File:** `app/Http/Livewire/VPSReinstallModal.php`
|
||||
**Description:** The `getIPFromVCenter()` method calls `vCenterService->loginServer()` and `GetIPAddressVirtualMachine` **synchronously** during component initialization (`loadOsData`).
|
||||
**Risk:** If vCenter is slow (common) or down, the user's browser will hang indefinitely/timeout when opening the modal. This blocks the PHP-FPM worker.
|
||||
**Fix:**
|
||||
- Move vCenter lookup to a background Queue Job.
|
||||
- Use `wire:poll` or Echo to check for the IP status after the modal opens.
|
||||
- **Immediate:** Wrap the call in a short timeout or remove it from the read path if not strictly necessary for the initial view.
|
||||
|
||||
### 🔴 Self-Calling HTTP Requests (Loopback)
|
||||
**File:** `app/Http/Livewire/VPSReinstallModal.php`
|
||||
**Description:** `confirmReinstall` uses `Http::withoutVerifying()->post(route('service.reinstall.vm')...)` to trigger logic.
|
||||
**Risk:**
|
||||
1. **Ineufficiency:** It consumes an extra PHP-FPM worker for the loopback request (Request A waits for Request B).
|
||||
2. **Security:** `withoutVerifying()` bypasses SSL checks.
|
||||
**Fix:** Refactor the logic inside `ServiceController::reinstallVM` into a `ReinstallService` and call it directly from the Livewire component.
|
||||
|
||||
### 🔴 Job-within-Job Synchronous Execution
|
||||
**File:** `app/Jobs/VirtualMachine/ProcessOrderPaymentJob.php`
|
||||
**Description:** `ProcessOrderPaymentJob` instantiates `CreateVirtualMachineJob` and calls `$vmJob->handle(...)` directly.
|
||||
**Risk:** If `CreateVirtualMachineJob` takes long (e.g., 2 minutes for vCenter), `ProcessOrderPaymentJob` might timeout (default 60s/90s). If creating 5 VMs, it will surely fail.
|
||||
**Fix:**
|
||||
- Dispatch `CreateVirtualMachineJob::dispatch(...)` to the queue.
|
||||
- Let `ProcessOrderPaymentJob` only *initiate* the process or use a `Batch` to monitor progress.
|
||||
|
||||
---
|
||||
|
||||
## 2. DATABASE PERFORMANCE REVIEW
|
||||
|
||||
### ⚠️ `Select *` in Repositories
|
||||
**File:** `app/Repositories/BaseCachedRepository.php`
|
||||
**Description:** `getAll()` uses `parent::getAll()` which likely does `$model->all()`.
|
||||
**Risk:** Tables like `vps` (with `text` description) or `user_logs` will eventually destroy memory when `getAll()` is called and cached.
|
||||
**Fix:**
|
||||
- Remove `getAll()` from `BaseCachedRepository`.
|
||||
- Implement `paginate()` by default.
|
||||
- Use `select(['id', 'name', ...])` for lists.
|
||||
|
||||
### ⚠️ Missing Explicit Indices
|
||||
**File:** `database/migrations/2026_01_27_151851_update_forigin_key_module_ip_table.php`
|
||||
**Description:** FKs added to `module_host_id` and `module_network_id`. While MySQL often indexes FKs, explicit indexing ensures query performance for `where('module_host_id', $id)`.
|
||||
**Fix:** Verify indices exist or add `$table->index('module_host_id')`.
|
||||
|
||||
---
|
||||
|
||||
## 3. LIVEWIRE & FRONTEND PERFORMANCE
|
||||
|
||||
### ⚠️ Heavy Mount Logic
|
||||
**File:** `app/Http/Livewire/VPSReinstallModal.php`
|
||||
**Description:** `loadOsData` performs multiple DB queries (`VPS::with`, `OsGroup::with` with closures) and checking Invoice items.
|
||||
**Fix:** Use **Lazy Loading** (`wire:init` or `lazy` attribute in Livewire 3) to load heavy data *after* the modal opens.
|
||||
|
||||
---
|
||||
|
||||
## 4. QUEUE, REDIS & BACKGROUND JOBS
|
||||
|
||||
### ⚠️ Queue Blocking
|
||||
**Description:** `ProcessOrderPaymentJob` combines "Payment Logic" (fast) with "VM Creation" (slow).
|
||||
**Fix:**
|
||||
- Separate queues: `queue:work --queue=high,default,low`.
|
||||
- Put `CreateVirtualMachineJob` on `low` or `provisioning` queue.
|
||||
- Keep `ProcessOrderPaymentJob` on `high` to update payment status quickly.
|
||||
|
||||
---
|
||||
|
||||
## 5. CACHING STRATEGY REVIEW
|
||||
|
||||
### 🔴 Dangerous 'getAll' Cache
|
||||
**File:** `app/Repositories/BaseCachedRepository.php`
|
||||
**Description:** Caches the *entire table result* of `getAll()`.
|
||||
**Risk:** Redis memory exhaustion.
|
||||
**Fix:**
|
||||
- **Delete** `getAll` caching logic.
|
||||
- Cache *individual* items by ID (`find($id)` is fine).
|
||||
- Cache configuration/settings, not transactional data lists.
|
||||
|
||||
---
|
||||
|
||||
## 6. SECURITY & PRODUCTION CONFIG CHECK
|
||||
|
||||
### ⚠️ SSL Verification Disabled
|
||||
**File:** `app/Http/Livewire/VPSReinstallModal.php`
|
||||
**Description:** `Http::withoutVerifying()` is used.
|
||||
**Fix:** Fix the root cause (self-signed certs?) or use internal service calls instead of HTTP loopbacks.
|
||||
|
||||
### ⚠️ Sensitive Logging
|
||||
**File:** `app/Jobs/VirtualMachine/ProcessOrderPaymentJob.php`
|
||||
**Description:** Sends detailed error logs to Telegram.
|
||||
**Risk:** Ensure `trace` or `message` doesn't contain API keys / credit card info (unlikely in exception message, but possible).
|
||||
|
||||
---
|
||||
|
||||
## 7. SCALABILITY BOTTLENECKS
|
||||
|
||||
### At 1k Users
|
||||
- `BaseCachedRepository::getAll()` will start causing 500 errors (OOM).
|
||||
|
||||
### At 10k Users
|
||||
- Synchronous vCenter calls will cause "No child processes" in PHP-FPM during traffic spikes.
|
||||
- Redis will fill up if `getAll` isn't removed.
|
||||
|
||||
---
|
||||
|
||||
## 8. FINAL PRODUCTION CHECKLIST
|
||||
|
||||
### Must Fix Before Launch
|
||||
- [ ] **Refactor**: Remove `Http::withoutVerifying()` loopback in `VPSReinstallModal`.
|
||||
- [ ] **Async**: Move `getIPFromVCenter` to background or make it lazy/optional.
|
||||
- [ ] **Cache**: Disable `getAll()` caching in `BaseCachedRepository`.
|
||||
- [ ] **Queue**: Decouple `CreateVirtualMachineJob` from `ProcessOrderPaymentJob` (dispatch don't call handle).
|
||||
|
||||
### Should Fix Soon
|
||||
- [ ] **Database**: Add explicit indices for searching fields in `vps` and `orders`.
|
||||
- [ ] **Refactor**: Move logic from Controllers/Livewire to Services to avoid loopback HTTP calls.
|
||||
|
||||
@@ -159,11 +159,11 @@
|
||||
<td class="text-muted">vCenter:</td>
|
||||
<td>
|
||||
@if (
|
||||
isset($product->ipaddress) &&
|
||||
isset($product->ipaddress->host) &&
|
||||
isset($product->ipaddress->host->datacenter) &&
|
||||
isset($product->ipaddress->host->datacenter->vCenter)
|
||||
)
|
||||
isset($product->ipaddress) &&
|
||||
isset($product->ipaddress->host) &&
|
||||
isset($product->ipaddress->host->datacenter) &&
|
||||
isset($product->ipaddress->host->datacenter->vCenter)
|
||||
)
|
||||
{{ $product->ipaddress->host->datacenter->vCenter->host ?? 'N/A' }}
|
||||
@else
|
||||
N/A
|
||||
@@ -249,7 +249,7 @@
|
||||
<div class="col-md-3 col-sm-6 mb-2">
|
||||
<button type="button" data-toggle="modal" data-target="#modal-lg"
|
||||
class="btn btn-outline-secondary btn-block" data-ajax="{{ route('task.reinstall') }}">
|
||||
<i class="fas fa-download"></i> Cài lại
|
||||
Cài lại
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+12
-5
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Broadcast;
|
||||
| Broadcast Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may register all the event broadcasting channels that your
|
||||
| Here you may register all of the event broadcasting channels that your
|
||||
| application supports. The given channel authorization callbacks are
|
||||
| used to check if an authenticated user can listen to the channel.
|
||||
|
|
||||
@@ -18,13 +18,20 @@ use Illuminate\Support\Facades\Broadcast;
|
||||
//});
|
||||
Broadcast::channel('Payment-Success.{id}', function ($user, $id) {
|
||||
return (int)$user->id === (int)$id;
|
||||
}, ['guards' => 'users']);
|
||||
}, ['guards' => ['users', 'customer']]);
|
||||
|
||||
Broadcast::channel('Expiry-Time.{id}', function ($user, $id) {
|
||||
return (int)$user->id === (int)$id;
|
||||
}, ['guards' => 'users']);
|
||||
}, ['guards' => ['users', 'customer']]);
|
||||
|
||||
Broadcast::channel('Created.Success.{id}', function ($user, $id) {
|
||||
return (int)$user->id === (int)$id;
|
||||
}, ['guards' => 'users']);
|
||||
}, ['guards' => ['users', 'customer']]);
|
||||
|
||||
Broadcast::channel('Panel.Status.{id}', function ($user, $id) {
|
||||
return (int)$user->id === (int)$id;
|
||||
}, ['guards' => 'users']);
|
||||
}, ['guards' => ['users', 'customer']]);
|
||||
|
||||
Broadcast::channel('user.{id}', function ($user, $id) {
|
||||
return (int) $user->id === (int) $id;
|
||||
}, ['guards' => ['users', 'customer']]);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Danh Sách Công Việc Cần Xử Lý (Project Todo List)
|
||||
|
||||
Tài liệu này liệt kê các vấn đề cần khắc phục và cải thiện cho dự án Portal VPS/Hosting, được sắp xếp theo độ ưu tiên từ Cao đến Thấp dựa trên đánh giá rủi ro vận hành (Production Readiness).
|
||||
|
||||
## 🚨 Mức Độ 1: Cực Kỳ Quan Trọng (Critical) - Phải Xử Lý Trước Khi Chạy Production
|
||||
*Các vấn đề đe dọa trực tiếp đến khả năng vận hành, bảo mật và hiệu năng của hệ thống.*
|
||||
|
||||
- [x] **Xử lý cuộc gọi đồng bộ vCenter (Blocking Calls)**
|
||||
- [x] **Mô tả:** Các thao tác bật/tắt/khởi động lại VPS hiện đang chờ phản hồi từ vCenter ngay trong request của người dùng (nếu vCenter chậm 10s, web treo 10s).
|
||||
- [x] **Giải pháp:** Đã chuyển tất cả hành động này vào Laravel Queue (Background Jobs: `StartVmJob`, `StopVmJob`, `RebootVmJob`). API trả về "Đang xử lý".
|
||||
- [x] **File:** `app/Http/Controllers/frontend/ServiceController.php` (Đã xử lý), `App\Http\Controllers\frontend\ManagerService.php` (Đã sử dụng Job từ trước).
|
||||
|
||||
- [ ] **Loại bỏ "Jump Host" PowerShell (Kiến trúc mỏng manh)**
|
||||
- [ ] **Mô tả:** Hệ thống phụ thuộc vào việc SSH vào một máy Windows (`10.10.10.173`) để chạy lệnh PowerShell. Nếu máy này chết hoặc lỗi mạng, toàn bộ hệ thống tê liệt.
|
||||
- [ ] **Giải pháp:** Chuyển sang dùng vCenter REST API trực tiếp cho các tác vụ: Snapshot, lấy thông tin VM, đổi cấu hình. Chỉ dùng PowerShell khi **thực sự** không có cách nào khác (ví dụ: chạy script bên trong Guest OS).
|
||||
- [ ] **File:** `app/Http/Services/vCenter.php`.
|
||||
|
||||
- [ ] **Bảo mật SSL (Security)**
|
||||
- [ ] **Mô tả:** Đang sử dụng `Http::withoutVerifying()` ở khắp nơi. Dễ bị tấn công Man-in-the-Middle.
|
||||
- [ ] **Giải pháp:** Thêm chứng chỉ CA của vCenter vào server và bật xác thực SSL, hoặc cấu hình Guzzle client đúng cách.
|
||||
- [ ] **File:** `app/Http/Services/vCenter.php`, `app/Http/Livewire/VPSReinstallModal.php`.
|
||||
|
||||
- [ ] **Hiệu năng Redis & Database (Memory Leak)**
|
||||
- [ ] **Mô tả:** Hàm `getAll()` trong `BaseCachedRepository` cache toàn bộ bảng dữ liệu. Khi dữ liệu lớn sẽ làm tràn RAM Redis/Server.
|
||||
- [ ] **Giải pháp:** Xóa việc cache `getAll()`. Sử dụng phân trang (`paginate`) cho các danh sách.
|
||||
- [ ] **File:** `app/Repositories/BaseCachedRepository.php`.
|
||||
|
||||
- [ ] **Loại bỏ gọi ngược nội bộ (Self-Calling HTTP)**
|
||||
- [ ] **Mô tả:** Livewire `VPSReinstallModal` gọi `Http::post(route('service.reinstall.vm'))`. Đây là một anti-pattern (tốn tài nguyên gấp đôi: 1 request của user + 1 request của server tự gọi mình).
|
||||
- [ ] **Giải pháp:** Gọi trực tiếp Service hoặc Controller method thay vì bắn HTTP request.
|
||||
- [ ] **File:** `app/Http/Livewire/VPSReinstallModal.php`.
|
||||
|
||||
## ⚠️ Mức Độ 2: Quan Trọng (Major) - Cần Xử Lý Sớm
|
||||
*Các tính năng cần thiết để hệ thống hoạt động trơn tru và chuyên nghiệp.*
|
||||
|
||||
- [ ] **Tối ưu Console Access (WebMKS)**
|
||||
- [ ] **Mô tả:** Việc lấy Ticket Console hiện đang đồng bộ.
|
||||
- [ ] **Giải pháp:** Đặt timeout thấp cho request lấy ticket (ví dụ 5s) hoặc hiển thị trạng thái loading rõ ràng hơn nến kết nối chậm. Cải thiện xử lý lỗi khi ticket không lấy được.
|
||||
|
||||
- [ ] **Cải thiện Quy trình Reinstall VPS**
|
||||
- [ ] **Mô tả:** Logic reinstall hiện tại xóa VM (`DeleteVirtualMachine`) rồi mới tạo lại. Nếu bước tạo lại lỗi, khách hàng mất VM và không có gì thay thế.
|
||||
- [ ] **Giải pháp:**
|
||||
1. Tạo VM mới song song (tên tạm clone).
|
||||
2. Nếu thành công -> Xóa VM cũ -> Rename VM mới -> IP Swap.
|
||||
3. Nếu thất bại -> Báo lỗi, giữ nguyên VM cũ.
|
||||
|
||||
- [ ] **Thêm Index Database**
|
||||
- [ ] **Mô tả:** Các khóa ngoại trong bảng `vps` và `module_ip_address` cần được đánh index để tăng tốc độ truy vấn khi số lượng row tăng lên.
|
||||
- [ ] **Giải pháp:** Kiểm tra và thêm index migration.
|
||||
|
||||
## 📝 Mức Độ 3: Cải Thiện (Minor/Enhancement) - Làm Cho Đẹp & Tiện
|
||||
*Tính năng nâng cao trải nghiệm người dùng.*
|
||||
|
||||
- [ ] **Biểu đồ giám sát (Graphs)**
|
||||
- [ ] **Mô tả:** Khách hàng chưa xem được biểu đồ CPU/RAM/Network.
|
||||
- [ ] **Giải pháp:** Viết Scheduled Job (Cron) định kỳ 5 phút/lần lấy stats từ vCenter lưu vào DB (bảng `vm_stats`) và vẽ biểu đồ bằng Chart.js.
|
||||
|
||||
- [ ] **Phân tách Queue**
|
||||
- [ ] **Mô tả:** Hiện tại có thể đang dùng chung queue `default`.
|
||||
- [ ] **Giải pháp:** Tách queue: `high` (thanh toán, mail OTP), `default` (tác vụ thường), `provisioning` (tạo VM - rất lâu, cần worker riêng).
|
||||
|
||||
- [ ] **Logging & Monitoring**
|
||||
- [ ] **Mô tả:** Log đang ghi vào file hoặc DB rải rác.
|
||||
- [ ] **Giải pháp:** Tập trung Log các lỗi quan trọng ra kênh Telegram/Slack (đã có implementation nhưng cần review để không spam tin nhắn rác).
|
||||
|
||||
---
|
||||
*Cập nhật lần cuối: 27/01/2026*
|
||||
Reference in New Issue
Block a user