whereIn('status', ['free', 'reserved']) ->lockForUpdate() ->inRandomOrder() ->first(); if (!$ipPool) { return null; } // Hold the pool slot $ipPool->update(['status' => 'reserved']); $cidr = (int) ($ipPool->cidr ?: 24); $gateway = (string) ($ipPool->gateway ?? ''); $netmask = $ipPool->netmask ?: $this->cidrToNetmask($cidr); // module_ip_address mirrors ip_pools — update the existing synced row // (created when the IP was added to ip_pools) instead of inserting a new one. $reservation = ModuleIpAddress::where('ip', $ipPool->ip_address) ->where('module_network_id', $networkId) ->lockForUpdate() ->first(); $attrs = [ 'vm_name' => '__pending__', 'module_host_id' => $hostId, 'module_network_id' => $networkId, 'ip' => $ipPool->ip_address, 'gateway' => $gateway, 'netmask' => $netmask, 'dns' => $dns, 'status' => 0, // 0 = pending/reserved, not yet confirmed ]; if ($reservation) { $reservation->update($attrs); } else { $reservation = ModuleIpAddress::create($attrs); } return [ 'reservation' => $reservation, 'ip_pool_id' => $ipPool->id, 'gateway' => $gateway, 'cidr' => $cidr, ]; }); } /** * Load a reservation that was created upstream (e.g. by CreateVirtualMachineJob). * Returns the same shape as reserveIp() so the rest of the flow is identical. * * @return array{reservation:ModuleIpAddress,ip_pool_id:int,gateway:string,cidr:int}|null */ private function loadReservation(int $reservationId, ?int $ipPoolId, VmProvisionLogger $logger): ?array { $reservation = ModuleIpAddress::find($reservationId); if (!$reservation) { $logger->error('upstream reservation missing', [ 'reservation_id' => $reservationId, ]); return null; } $ipPool = $ipPoolId ? IpPool::find($ipPoolId) : null; $cidr = (int) ($ipPool->cidr ?? 24); $gateway = (string) ($reservation->gateway ?: ($ipPool->gateway ?? '')); return [ 'reservation' => $reservation, 'ip_pool_id' => $ipPoolId ?? 0, 'gateway' => $gateway, 'cidr' => $cidr ?: 24, ]; } /** * Release a failed reservation by resetting the mirrored ModuleIpAddress row. * This returns the IP to the available pool. */ private function releaseIp(ModuleIpAddress $reservation, ?int $ipPoolId, VmProvisionLogger $logger): void { try { $reservation->update(['vm_name' => null, 'status' => 0]); if ($ipPoolId) { IpPool::where('id', $ipPoolId)->update(['status' => 'free']); } $logger->step('IP reservation released', ['ip' => $reservation->ip]); } catch (\Throwable $e) { $logger->warning('could not release IP reservation', [ 'ip' => $reservation->ip, 'error' => $e->getMessage(), ]); } } /** * Confirm a successful IP assignment: * - Update ModuleIpAddress: status → 1 (assigned), vm_name → real name * - Update ModuleVmware: ip, dns, module_ip_address FK * - Update VirtualMachines: ip, ip_pool_id FK (matched by vmId) * * IP is only written to DB AFTER guest-side verification passes. */ private function persistIp( ModuleIpAddress $reservation, ModuleVmware $moduleVmware, ?int $ipPoolId, ?int $serviceId, ): void { DB::transaction(function () use ($reservation, $moduleVmware, $ipPoolId, $serviceId) { $reservation->update([ 'vm_name' => $moduleVmware->name ?? ('VM-' . $this->vmId), 'status' => 1, // assigned ]); $moduleVmware->update([ 'ip' => $reservation->ip, 'dns' => $reservation->dns, 'module_ip_address' => $reservation->id, ]); // Sync to virtual_machines — matched by vSphere vmId VirtualMachines::where('vm_id', $this->vmId)->update([ 'ip' => $reservation->ip, 'ip_pool_id' => $ipPoolId, ]); // Flip ip_pools row to assigned and link service if ($ipPoolId) { $statusFrom = (string) IpPool::whereKey($ipPoolId)->value('status'); IpPool::where('id', $ipPoolId)->update([ 'status' => 'assigned', 'service_id' => $serviceId, ]); IpPoolLog::recordVm($ipPoolId, IpPoolLog::VM_ASSIGNED, $statusFrom, $moduleVmware, 'Tạo VM'); } // Touch services row so server_name reflects the provisioned VM. // Note: services.vm_id is an FK to module_vmware.id (already set by VmServiceSyncService) — do not overwrite with vSphere vmId string. if ($serviceId) { Service::where('id', $serviceId)->update([ 'server_name' => $moduleVmware->name, ]); } }); } /** Convert CIDR prefix length to dotted-decimal netmask (e.g. 24 → 255.255.255.0). */ private function cidrToNetmask(int $cidr): string { $cidr = max(0, min(32, $cidr)); return long2ip(-1 << (32 - $cidr)); } }