} */ public function powerOn(Request $request, PowerOnVMAction $action): JsonResponse { $productId = $request->input('product'); if (!$productId) { return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422); } $vm = $this->resolveVmById($productId, $request); if ($vm instanceof JsonResponse) { return $vm; } $lock = $this->acquireVmLock($vm); if (!$lock) { return $this->vmLocked(); } try { $result = $action->execute($vm); } finally { $lock->release(); } $this->activityLogger->log( event: 'power_on', logName: 'VM', description: "Customer {$request->user()->email} bật VM: {$vm->name} (ID: {$vm->vmware_id})", subject: $vm, ); return $this->vmResponse($result, 'Đã gửi lệnh bật VM.'); } // ------------------------------------------------------------------------- // Power OFF // ------------------------------------------------------------------------- /** * POST /v1/system/vm-power-off * * Body: { "product": } */ public function powerOff(Request $request, PowerOffVMAction $action): JsonResponse { $productId = $request->input('product'); if (!$productId) { return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422); } $vm = $this->resolveVmById($productId, $request); if ($vm instanceof JsonResponse) { return $vm; } $lock = $this->acquireVmLock($vm); if (!$lock) { return $this->vmLocked(); } try { $result = $action->execute($vm); } finally { $lock->release(); } $this->activityLogger->log( event: 'power_off', logName: 'VM', description: "Customer {$request->user()->email} tắt VM: {$vm->name} (ID: {$vm->vmware_id})", subject: $vm, ); return $this->vmResponse($result, 'Đã gửi lệnh tắt VM.'); } // ------------------------------------------------------------------------- // Change IP // ------------------------------------------------------------------------- /** * POST /v1/system/vm-change-ip * * Body: * server_name string Service.server_name (e.g. "xasdf.com.vnx") * ip_address string New IP to assign * gateway string (optional) Gateway — falls back to existing IP record */ /** * Dispatches ChangeIpVMJob — returns immediately, runs async. */ public function changeIp(Request $request): JsonResponse { $validated = $request->validate([ 'server_name' => ['required', 'string', 'max:255'], 'ip_address' => ['required', 'ip'], 'gateway' => ['nullable', 'ip'], 'dns' => ['nullable', 'ip'], 'guest_user' => ['required', 'string', 'max:255'], 'guest_password' => ['required', 'string', 'max:255'], ]); $customer = $request->user(); // Look up Service by server_name + owner // VM chain: Service → orderItem → order → vmWare (same as ServiceClientDetailResource) $service = Service::with([ 'orderItem.order.vmWare.vCenter', 'orderItem.order.vmWare.ipAddress.host.datacenter.vCenter', 'orderItem.os.osGroup', ]) ->where('server_name', $validated['server_name']) ->where('customer_id', $customer->id) ->first(); if (!$service) { return response()->json(['success' => false, 'message' => 'Không tìm thấy dịch vụ.'], 404); } $vm = $service->orderItem?->order?->vmWare?->first(); if (!$vm) { return response()->json(['success' => false, 'message' => 'Không tìm thấy VM cho dịch vụ này.'], 404); } // Auto-detect OS type from the service's order item OS group $osGroup = strtolower($service->orderItem?->os?->osGroup?->name ?? ''); $osType = (str_contains($osGroup, 'linux') || str_contains($osGroup, 'ubuntu') || str_contains($osGroup, 'centos')) ? 'linux' : 'windows'; $gateway = $validated['gateway'] ?? ''; $dns = $validated['dns'] ?? '8.8.8.8'; // ponytail: don't release — lock expires after 5 min while the async job runs $lock = $this->acquireVmLock($vm, 300); if (!$lock) { return $this->vmLocked(); } ChangeIpVMJob::dispatch( $vm->id, $validated['ip_address'], $gateway, $osType, $dns, $validated['guest_user'], $validated['guest_password'], ); $this->activityLogger->log( event: 'change_ip', logName: 'VM', description: "Customer {$request->user()->email} yêu cầu đổi IP VM: {$vm->name} → {$validated['ip_address']}", subject: $vm, newValues: ['ip_address' => $validated['ip_address'], 'gateway' => $gateway, 'os_type' => $osType], ); return response()->json([ 'success' => true, 'message' => 'Yêu cầu đổi IP đã được ghi nhận. Hệ thống sẽ xử lý trong vài phút.', ]); } // ------------------------------------------------------------------------- // Change Port (RDP) // ------------------------------------------------------------------------- /** * POST /v1/system/vm-change-port * * Body: * product int|string ModuleVmware.id|vmware_id|service_uuid * new_port int New RDP port (1024–65535) */ public function changePort(Request $request, ChangePortVMAction $action): JsonResponse { $validated = $request->validate([ 'product' => ['required'], 'new_port' => ['required', 'integer', 'min:1024', 'max:65535'], ]); $vm = $this->resolveVmById($validated['product'], $request); if ($vm instanceof JsonResponse) { return $vm; } $lock = $this->acquireVmLock($vm); if (!$lock) { return $this->vmLocked(); } try { $result = $action->execute($vm, (string) $validated['new_port']); } finally { $lock->release(); } // changeRDPPortWindows returns array on success/failure or string on login fail if ($result === 'Login_Again' || $result === 'Login_Fail') { return response()->json([ 'success' => false, 'message' => 'Không thể đăng nhập vào vCenter.', ], 400); } if (is_array($result)) { $ok = isset($result['success']) ? (bool) $result['success'] : !($result['error'] ?? true); if ($ok) { $this->activityLogger->log( event: 'change_port', logName: 'VM', description: "Customer {$request->user()->email} đổi cổng RDP VM: {$vm->name} → port {$validated['new_port']}", subject: $vm, newValues: ['new_port' => $validated['new_port']], ); } return response()->json([ 'success' => $ok, 'message' => $ok ? 'Đổi cổng RDP thành công.' : ($result['message'] ?? 'Đổi cổng thất bại.'), 'data' => $result, ], $ok ? 200 : 400); } \Illuminate\Support\Facades\Log::error('changePort: unexpected vCenter response', [ 'vm_id' => $vm->id, 'result' => $result, ]); return response()->json([ 'success' => false, 'message' => 'Phản hồi không hợp lệ từ vCenter.', ], 400); } // ------------------------------------------------------------------------- // Console Ticket // ------------------------------------------------------------------------- public function consoleTicket(Request $request, vCenter $serviceVCenter): JsonResponse { $productId = $request->get('product'); if (!$productId) { return response()->json(['error' => true, 'message' => 'Please choose product']); } $vm = $this->resolveVmById($productId, $request); if ($vm instanceof JsonResponse) { return $vm; } $result = $this->getConsoleTicket($serviceVCenter, $vm); if ($result instanceof JsonResponse) { return $result; } $this->activityLogger->log( event: 'console_access', logName: 'VM', description: "Customer {$request->user()->email} truy cập console VM: {$vm->name} (ID: {$vm->vmware_id})", subject: $vm, newValues: ['product_id' => $productId, 'vm_name' => $vm->name], ); return response()->json([ 'error' => false, 'data' => $result['data'], ]); } // ------------------------------------------------------------------------- // Restart VM // ------------------------------------------------------------------------- public function restart(Request $request, vCenter $serviceVCenter): JsonResponse { $productId = $request->input('product'); if (!$productId) { return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422); } $vm = $this->resolveVmForClient($productId, $request); if ($vm instanceof JsonResponse) { return $vm; } $lock = $this->acquireVmLock($vm); if (!$lock) { return $this->vmLocked(); } try { $result = $serviceVCenter->restartVM($vm, $vm->vmware_id); } finally { $lock->release(); } if (is_string($result) || ($result['error'] ?? true)) { return response()->json([ 'error' => true, 'message' => is_string($result) ? $result : ($result['data'] ?? 'Không thể restart VM'), ], 400); } $this->activityLogger->log( event: 'restart', logName: 'VM', description: "Customer {$request->user()->email} restart VM: {$vm->name} (ID: {$vm->vmware_id})", subject: $vm, ); return response()->json([ 'error' => false, 'message' => 'Đang restart máy ảo vui lòng chờ!', 'data' => $result['data'] ?? 'success', ]); } // ------------------------------------------------------------------------- // Change Password // ------------------------------------------------------------------------- /** * POST /v1/system/vm-change-password * * Body: { product, current_password, new_password } * Dispatches ChangeVmPasswordJob — returns immediately, runs async. */ public function changePassword(Request $request): JsonResponse { $validated = $request->validate([ 'product' => ['required'], 'current_password' => ['required', 'string'], 'new_password' => ['required', 'string', 'min:8'], ]); $vm = $this->resolveVmById($validated['product'], $request); if ($vm instanceof JsonResponse) { return $vm; } // ponytail: don't release — lock expires after 5 min while the async job runs $lock = $this->acquireVmLock($vm, 300); if (!$lock) { return $this->vmLocked(); } // Detect OS family from VM's os_id record $os = $vm->os_id ? Os::with('osGroup')->find($vm->os_id) : null; $osGroupName = strtolower($os?->osGroup?->name ?? ''); $osFamily = str_contains($osGroupName, 'windows') ? VmProvisionService::OS_WINDOWS : VmProvisionService::OS_LINUX; ChangeVmPasswordJob::dispatch( $vm->id, $vm->vmware_id, $osFamily, $validated['new_password'], $validated['current_password'], ); $this->activityLogger->log( event: 'change_password', logName: 'VM', description: "Customer {$request->user()->email} yêu cầu đổi mật khẩu VM: {$vm->name}", subject: $vm, newValues: ['os_family' => $osFamily], ); return response()->json([ 'success' => true, 'message' => 'Yêu cầu đổi mật khẩu đã được ghi nhận. Hệ thống sẽ xử lý trong vài phút.', ]); } // ------------------------------------------------------------------------- // Power State // ------------------------------------------------------------------------- /** * GET /v1/system/vm-power-state?product={id|uuid|vmware_id} * * Calls vSphere REST API directly: GET /rest/vcenter/vm/{vmware_id}/power * Returns: { success: true, data: { state: "POWERED_ON"|"POWERED_OFF"|"SUSPENDED", clean_power_off: bool } } */ public function powerState(Request $request, vCenter $serviceVCenter): JsonResponse { $productId = $request->input('product'); if (!$productId) { return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422); } $vm = $this->resolveVmById($productId, $request); if ($vm instanceof JsonResponse) { return $vm; } $result = $serviceVCenter->getVmPowerState($vm, $vm->vmware_id); if ($result === 'Login_Again') { return response()->json(['success' => false, 'message' => 'Không thể đăng nhập vào vCenter.'], 400); } if (is_array($result) && ($result['error'] ?? true)) { return response()->json([ 'success' => false, 'message' => $result['data'] ?? 'Không thể lấy trạng thái VM.', ], 400); } return response()->json([ 'success' => true, 'data' => [ 'state' => $result['data']['state'] ?? null, 'clean_power_off' => $result['data']['clean_power_off'] ?? null, ], ]); } // ------------------------------------------------------------------------- // Shared helpers // ------------------------------------------------------------------------- /** * Load a ModuleVmware by ID (int), vmware_id (string), or Service UUID. * Verify the authenticated user owns it. * Returns JsonResponse on error, ModuleVmware on success. */ private function resolveVmById(int|string $productId, Request $request): ModuleVmware|JsonResponse { $customer = $request->user(); $vm = null; // Check if it's a UUID (Service UUID) if (is_string($productId) && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $productId)) { $service = Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter') ->where('uuid', $productId) ->where('customer_id', $customer->id) ->first(); if ($service && $service->vmware) { $vm = $service->vmware; } } elseif (is_numeric($productId)) { // Numeric ID $vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter']) ->find((int) $productId); } else { // vmware_id string (e.g., "vm-3238") $vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter']) ->where('vmware_id', $productId) ->where('customer_id', $customer->id) ->first(); } if (!$vm) { return response()->json(['success' => false, 'message' => 'Không tìm thấy VM.'], 404); } if ((int) $vm->customer_id !== (int) $customer->id) { return response()->json(['success' => false, 'message' => 'Bạn không có quyền thao tác VM này.'], 403); } return $vm; } /** * Load a ModuleVmware by ID, verify the authenticated user owns it. * Returns JsonResponse on error, ModuleVmware on success. * @deprecated Use resolveVmById instead for better ID format support */ private function resolveVm(int $id, Request $request): ModuleVmware|JsonResponse { return $this->resolveVmById($id, $request); } /** Try to acquire a per-VM operation lock. Returns null if already locked. */ private function acquireVmLock(ModuleVmware $vm, int $ttl = 60): ?\Illuminate\Contracts\Cache\Lock { $lock = Cache::lock("vm_op:{$vm->id}", $ttl); return $lock->get() ? $lock : null; } private function vmLocked(): JsonResponse { return response()->json([ 'success' => false, 'message' => 'VM đang có thao tác đang xử lý, vui lòng chờ rồi thử lại.', ], 429); } /** * Normalise the array|string return value from Power ON/OFF actions. */ private function vmResponse(array|string $result, string $successMessage): JsonResponse { if ($result === 'Login_Again') { return response()->json([ 'success' => false, 'message' => 'Không thể đăng nhập vào vCenter.', ], 400); } if (is_string($result)) { return response()->json(['success' => false, 'message' => $result], 400); } if ($result['error'] ?? true) { return response()->json([ 'success' => false, 'message' => $result['data'] ?? 'Thao tác thất bại.', 'data' => $result, ], 400); } return response()->json([ 'success' => true, 'message' => $successMessage, 'data' => $result['data'] ?? 'success', ]); } }