boolean('all') ? 20000 : min((int) $request->get('per_page', 15), 500); $filter = $request->get('filter'); $networkId = $request->get('network_id'); $query = IpPool::with(['network:id,name,type,network', 'service:id,server_name,status,customer_id']); if ($filter) { $query->where('ip_address', 'like', "%{$filter}%"); } if ($networkId) { $query->where('module_network_id', $networkId); } $paginator = $query->paginate($perPage); // Enrich each item with usage info $usedIps = ModuleIpAddress::whereIn('ip', $paginator->pluck('ip_address')->toArray()) ->where('status', 1) ->with('host:id,name') ->select(['id', 'ip', 'vm_name', 'module_host_id', 'module_network_id', 'status']) ->get() ->keyBy('ip'); // Get vmware info for used IPs $vmwareMap = []; if ($usedIps->isNotEmpty()) { $vmwares = DB::table('module_vmware as mv') ->join('module_ip_address as mia', 'mv.module_ip_address', '=', 'mia.id') ->whereIn('mia.ip', $usedIps->keys()->toArray()) ->leftJoin('services as s', 's.id', '=', 'mv.service_id') ->leftJoin('customers as c', 'c.id', '=', 'mv.customer_id') ->select([ 'mia.ip', 'mv.name as vm_name', 'mv.id as vm_id', 's.id as service_id', 's.status as service_status', 'c.fullname', 'c.email', ]) ->get(); foreach ($vmwares as $vm) { $vmwareMap[$vm->ip] = $vm; } } $items = $paginator->map(function (IpPool $pool) use ($usedIps, $vmwareMap) { $data = $pool->toArray(); $ip = $pool->ip_address; if (isset($usedIps[$ip])) { $mia = $usedIps[$ip]; $vm = $vmwareMap[$ip] ?? null; $data['usage'] = [ 'assigned' => true, 'vm_name' => $vm?->vm_name ?? $mia->vm_name, 'vm_id' => $vm?->vm_id ?? null, 'service_id' => $vm?->service_id ?? null, 'service_status' => $vm?->service_status ?? null, 'customer_name' => $vm?->fullname ?? null, 'customer_email' => $vm?->email ?? null, ]; } else { $data['usage'] = ['assigned' => false]; } return $data; }); return response()->json([ 'error' => false, 'data' => [ 'items' => $items->values(), 'total' => $paginator->total(), 'per_page' => $paginator->perPage(), 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), ], ]); } public function show(int $id): JsonResponse { $pool = IpPool::with(['network:id,name,type,network', 'service:id,server_name,status,customer_id'])->find($id); if (!$pool) { return response()->json(['error' => true, 'message' => "Không tìm thấy IP Pool #{$id}"], 404); } return response()->json(['error' => false, 'data' => $pool]); } /** * Bulk create: ip_address may be separated by commas and/or whitespace (e.g. "10.0.0.1, 10.0.0.2 10.0.0.3"). */ public function store(Request $request): JsonResponse { $request->validate([ 'ip_address' => 'required|string', 'module_network_id' => 'required|integer|exists:module_network,id', 'gateway' => 'nullable|string|max:45', 'cidr' => 'nullable|integer|min:0|max:32', 'netmask' => 'nullable|string|max:45', 'status' => 'nullable|in:free,assigned,reserved', 'service_id' => 'nullable|integer|exists:services,id', 'contact_email' => ['nullable', 'string', $this->multiEmailRule()], 'contact_cc' => ['nullable', 'string', $this->multiEmailRule()], 'monthly_cost' => 'nullable|numeric', 'selling_price' => 'nullable|numeric', ]); $networkId = (int) $request->module_network_id; $rawIps = $request->ip_address; $ips = array_filter(array_map('trim', preg_split('/[\s,]+/', $rawIps))); if (empty($ips)) { return response()->json(['error' => true, 'message' => 'Không có địa chỉ IP hợp lệ'], 422); } // Validate each IP $invalid = []; foreach ($ips as $ip) { if (!filter_var($ip, FILTER_VALIDATE_IP)) { $invalid[] = $ip; } } if (!empty($invalid)) { return response()->json([ 'error' => true, 'message' => 'Địa chỉ IP không hợp lệ: ' . implode(', ', $invalid), ], 422); } // Check duplicates already in pool $existing = IpPool::where('module_network_id', $networkId) ->whereIn('ip_address', $ips) ->pluck('ip_address') ->toArray(); if (!empty($existing)) { return response()->json([ 'error' => true, 'message' => 'Địa chỉ IP đã tồn tại trong pool: ' . implode(', ', $existing), ], 422); } $hostId = ModuleNetwork::find($networkId)?->module_host_id; $netmask = $request->netmask ?: '255.255.255.0'; $cidr = $request->cidr ?? 24; $extra = $request->only(self::EDITABLE_FIELDS); DB::beginTransaction(); try { $created = []; foreach ($ips as $ip) { $pool = IpPool::create([ 'ip_address' => $ip, 'module_network_id' => $networkId, 'gateway' => $request->gateway, 'cidr' => $cidr, 'netmask' => $netmask, 'status' => $request->status ?: 'free', 'service_id' => $request->service_id, ...$extra, ]); // Mirror into module_ip_address so both tables stay in sync. // Reservation logic (VmIpReservationService/AssignIpToGuestJob) treats // ip_pools.status as the source of truth and just updates this row. ModuleIpAddress::updateOrCreate( ['ip' => $ip, 'module_network_id' => $networkId], [ 'module_host_id' => $hostId, 'gateway' => $request->gateway, 'netmask' => $netmask, 'status' => 0, ] ); $created[] = $pool; } DB::commit(); return response()->json(['error' => false, 'data' => $created, 'count' => count($created)], 201); } catch (\Exception $e) { DB::rollBack(); return response()->json(['error' => true, 'message' => $e->getMessage()], 500); } } public function update(int $id, Request $request): JsonResponse { $pool = IpPool::find($id); if (!$pool) { return response()->json(['error' => true, 'message' => "Không tìm thấy IP Pool #{$id}"], 404); } $request->validate([ 'ip_address' => 'required|string|max:45', 'module_network_id' => 'required|integer|exists:module_network,id', 'gateway' => 'nullable|string|max:45', 'cidr' => 'nullable|integer|min:0|max:32', 'netmask' => 'nullable|string|max:45', 'status' => 'nullable|in:free,assigned,reserved', 'service_id' => 'nullable|integer|exists:services,id', 'contact_email' => ['nullable', 'string', $this->multiEmailRule()], 'contact_cc' => ['nullable', 'string', $this->multiEmailRule()], 'monthly_cost' => 'nullable|numeric', 'selling_price' => 'nullable|numeric', ]); if (!filter_var($request->ip_address, FILTER_VALIDATE_IP)) { return response()->json(['error' => true, 'message' => 'Địa chỉ IP không hợp lệ'], 422); } // Check duplicate (excluding self) $duplicate = IpPool::where('module_network_id', $request->module_network_id) ->where('ip_address', $request->ip_address) ->where('id', '!=', $id) ->exists(); if ($duplicate) { return response()->json([ 'error' => true, 'message' => 'Địa chỉ IP đã tồn tại trong pool này', ], 422); } $oldIp = $pool->ip_address; $oldNetworkId = $pool->module_network_id; $netmask = $request->netmask ?: '255.255.255.0'; DB::beginTransaction(); try { $pool->update([ ...$request->only(['ip_address', 'module_network_id', 'gateway', 'cidr', 'service_id']), ...$request->only(self::EDITABLE_FIELDS), 'netmask' => $netmask, 'status' => $request->status ?: $pool->status, ]); $hostId = ModuleNetwork::find($pool->module_network_id)?->module_host_id; // Keep the module_ip_address mirror in sync with the new ip/network/host/gateway/netmask. ModuleIpAddress::where('ip', $oldIp) ->where('module_network_id', $oldNetworkId) ->update([ 'ip' => $pool->ip_address, 'module_network_id' => $pool->module_network_id, 'module_host_id' => $hostId, 'gateway' => $pool->gateway, 'netmask' => $pool->netmask, ]); DB::commit(); return response()->json(['error' => false, 'data' => $pool->fresh(['network', 'service:id,server_name,status,customer_id'])]); } catch (\Exception $e) { DB::rollBack(); return response()->json(['error' => true, 'message' => $e->getMessage()], 500); } } public function destroy(int $id): JsonResponse { $pool = IpPool::find($id); if (!$pool) { return response()->json(['error' => true, 'message' => "Không tìm thấy IP Pool #{$id}"], 404); } // Check if IP is currently assigned $inUse = ModuleIpAddress::where('ip', $pool->ip_address) ->where('module_network_id', $pool->module_network_id) ->where('status', 1) ->exists(); if ($inUse) { return response()->json([ 'error' => true, 'message' => 'Không thể xoá — IP này đang được gán cho một máy chủ', ], 422); } ModuleIpAddress::where('ip', $pool->ip_address) ->where('module_network_id', $pool->module_network_id) ->delete(); $pool->delete(); return response()->json(['error' => false, 'message' => 'Đã xoá IP Pool thành công']); } public function bulkUpdateGateway(Request $request): JsonResponse { $request->validate([ 'ids' => 'required|array|min:1', 'ids.*' => 'integer', 'gateway' => 'required|string|max:45', ]); $pools = IpPool::whereIn('id', $request->ids)->get(['id', 'ip_address', 'module_network_id']); if ($pools->isEmpty()) { return response()->json(['error' => true, 'message' => 'Không tìm thấy IP Pool nào'], 404); } DB::beginTransaction(); try { IpPool::whereIn('id', $pools->pluck('id'))->update(['gateway' => $request->gateway]); // Mirror into module_ip_address, grouped by network to keep this to a handful of queries. foreach ($pools->groupBy('module_network_id') as $networkId => $group) { ModuleIpAddress::whereIn('ip', $group->pluck('ip_address')) ->where('module_network_id', $networkId) ->update(['gateway' => $request->gateway]); } DB::commit(); } catch (\Exception $e) { DB::rollBack(); return response()->json(['error' => true, 'message' => $e->getMessage()], 500); } return response()->json([ 'error' => false, 'message' => "Đã cập nhật gateway cho {$pools->count()} IP", 'updated' => $pools->count(), ]); } public function bulkDelete(Request $request): JsonResponse { $request->validate([ 'ids' => 'required|array|min:1', 'ids.*' => 'integer', ]); $ids = $request->ids; $pools = IpPool::whereIn('id', $ids)->get(); if ($pools->isEmpty()) { return response()->json(['error' => true, 'message' => 'Không tìm thấy IP Pool nào'], 404); } // Block deletion if any IP is currently assigned $inUseIps = ModuleIpAddress::whereIn('ip', $pools->pluck('ip_address')) ->where('status', 1) ->pluck('ip') ->toArray(); if (!empty($inUseIps)) { return response()->json([ 'error' => true, 'message' => 'Không thể xoá — các IP sau đang được gán cho máy chủ: ' . implode(', ', $inUseIps), ], 422); } foreach ($pools as $p) { ModuleIpAddress::where('ip', $p->ip_address) ->where('module_network_id', $p->module_network_id) ->delete(); } $deleted = IpPool::whereIn('id', $ids)->delete(); return response()->json([ 'error' => false, 'message' => "Đã xoá {$deleted} IP Pool thành công", 'deleted' => $deleted, ]); } public function import(Request $request): JsonResponse { $request->validate([ 'file' => 'required|file|mimes:csv,txt|max:10240', ]); $countBefore = IpPool::withTrashed()->count(); try { Excel::import(new IpPoolImport(), $request->file('file')); } catch (ValidationException $e) { $errors = []; foreach ($e->failures() as $failure) { $errors[] = 'Dòng ' . $failure->row() . ': ' . implode(', ', $failure->errors()); } return response()->json([ 'error' => true, 'message' => 'Import thất bại', 'errors' => $errors, ], 422); } catch (\Exception $e) { return response()->json(['error' => true, 'message' => 'Import thất bại: ' . $e->getMessage()], 500); } $countAfter = IpPool::withTrashed()->count(); IpPool::resyncAllContacts(); return response()->json([ 'error' => false, 'message' => 'Import thành công', 'created' => max(0, $countAfter - $countBefore), ]); } /** * Backfill/repair the module_ip_address mirror for every ip_pools row. * Existing mirror rows only get gateway/netmask refreshed — status/vm_name * (assigned VM state) is left untouched. Missing rows are created with * status derived from the current ip_pools.status. */ public function sync(): JsonResponse { $created = 0; $updated = 0; $hostByNet = ModuleNetwork::pluck('module_host_id', 'id'); IpPool::chunkById(500, function ($pools) use (&$created, &$updated, $hostByNet) { foreach ($pools as $pool) { $hostId = $hostByNet[$pool->module_network_id] ?? null; $netmask = $pool->netmask ?: '255.255.255.0'; $mia = ModuleIpAddress::where('ip', $pool->ip_address) ->where('module_network_id', $pool->module_network_id) ->first(); if ($mia) { $mia->update([ 'module_host_id' => $hostId, 'gateway' => $pool->gateway, 'netmask' => $netmask, ]); $updated++; } else { ModuleIpAddress::create([ 'ip' => $pool->ip_address, 'module_network_id' => $pool->module_network_id, 'module_host_id' => $hostId, 'gateway' => $pool->gateway, 'netmask' => $netmask, 'status' => $pool->status === 'assigned' ? 1 : 0, ]); $created++; } } }); return response()->json([ 'error' => false, 'message' => "Đã đồng bộ: tạo mới {$created}, cập nhật {$updated}", 'created' => $created, 'updated' => $updated, ]); } /** * Service IDs already tied to an IP — either directly via ip_pools.service_id, * or indirectly via a VM (module_vmware) sitting on that IP. Used by the admin * form to disable services that are already in use on another IP. */ public function usedServices(): JsonResponse { $direct = IpPool::whereNotNull('service_id') ->select(['id as ip_pool_id', 'service_id']); $viaVm = DB::table('ip_pools as ip') ->join('module_ip_address as mia', function ($join) { $join->on('mia.ip', '=', 'ip.ip_address') ->on('mia.module_network_id', '=', 'ip.module_network_id'); }) ->join('module_vmware as mv', 'mv.module_ip_address', '=', 'mia.id') ->whereNotNull('mv.service_id') ->select(['ip.id as ip_pool_id', 'mv.service_id']); $used = $direct->toBase()->union($viaVm)->get()->unique(fn ($row) => "{$row->service_id}-{$row->ip_pool_id}")->values(); return response()->json(['error' => false, 'data' => $used]); } public function networks(): JsonResponse { $networks = ModuleNetwork::select(['id', 'name', 'type', 'network', 'module_host_id']) ->with(['host:id,name,module_datacenter_id', 'host.datacenter:id,name']) ->orderBy('name') ->get(); return response()->json(['error' => false, 'data' => $networks]); } }