validate([ 'server_name' => ['required', 'string', 'max:255'], ]); $service = $this->findOwnedServiceByServerName($validated['server_name'], $request); if ($service instanceof JsonResponse) { return $service; } $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); } $networkId = $vm->ipAddress?->module_network_id; if (!$networkId) { return response()->json(['success' => false, 'message' => 'Không xác định được mạng của VM.'], 422); } $ips = app(VmIpReservationService::class)->freeIpsInNetwork($networkId); return response()->json([ 'success' => true, 'data' => $ips->map(fn (IpPool $ip) => [ 'ip_address' => $ip->ip_address, 'gateway' => $ip->gateway, ])->values(), ]); } /** * 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 — must be a free IP from the VM's network pool */ /** * Dispatches ChangeIpVMJob — returns immediately, runs async. */ public function changeIp(Request $request, VmIpReservationService $ipReservation): JsonResponse { $validated = $request->validate([ 'server_name' => ['required', 'string', 'max:255'], 'ip_address' => ['required', 'ip'], 'dns' => ['nullable', 'ip'], 'guest_user' => ['required', 'string', 'max:255'], 'guest_password' => ['required', 'string', 'max:255'], ]); $service = $this->findOwnedServiceByServerName($validated['server_name'], $request); if ($service instanceof JsonResponse) { return $service; } $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); } $networkId = $vm->ipAddress?->module_network_id; if (!$networkId) { return response()->json(['success' => false, 'message' => 'Không xác định được mạng của VM.'], 422); } $reservation = $ipReservation->reserveInNetwork($networkId, $validated['ip_address'], $service->id); if (!$reservation) { return response()->json(['success' => false, 'message' => 'IP không thuộc pool khả dụng hoặc đã được sử dụng.'], 422); } $oldIpPoolId = IpPool::where('module_network_id', $networkId) ->where('service_id', $service->id) ->where('status', 'assigned') ->where('id', '!=', $reservation['ip_pool_id']) ->value('id'); // 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'; $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) { $ipReservation->releaseByPoolId($reservation['ip_pool_id']); return $this->vmLocked(); } ChangeIpVMJob::dispatch( $vm->id, $validated['ip_address'], $reservation['gateway'], $osType, $dns, $validated['guest_user'], $validated['guest_password'], $reservation['ip_pool_id'], $oldIpPoolId, ); $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' => $reservation['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.', ]); } /** * 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); } /** * Look up a Service by server_name, verify ownership and active status. * Returns JsonResponse on error, Service on success. */ private function findOwnedServiceByServerName(string $serverName, Request $request): Service|JsonResponse { $customer = $request->user(); // 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', $serverName) ->where('customer_id', $customer->id) ->first(); if (!$service) { return response()->json(['success' => false, 'message' => 'Không tìm thấy dịch vụ.'], 404); } if ($service->status !== 'active') { return response()->json(['success' => false, 'message' => 'Dịch vụ không ở trạng thái hoạt động, không thể thao tác.'], 403); } return $service; } }