$this->moduleVmwareId, 'vm_id' => $this->vmId, 'os_family' => $this->osFamily, ]); // Load ModuleVmware with the relations needed to derive network config $moduleVmware = ModuleVmware::with([ 'order.orderItems.product.ipAddress', 'vcenter', ])->find($this->moduleVmwareId); if (!$moduleVmware) { Log::error('AssignIpToGuestJob: ModuleVmware not found', [ 'module_vmware_id' => $this->moduleVmwareId, ]); return; } // Resolve network config: product template IP → gateway, netmask, dns, networkId $orderItem = $moduleVmware->order?->orderItems?->first(); $osModel = $orderItem?->os_id ? Os::with('osGroup.host')->find($orderItem->os_id) : null; $networkId = $orderItem?->product?->ipAddress?->module_network_id ?? 1; $gateway = $orderItem?->product?->ipAddress?->gateway ?? '0.0.0.0'; $netmask = $orderItem?->product?->ipAddress?->netmask ?? '255.255.255.0'; $dns = $orderItem?->product?->ipAddress?->dns ?? '8.8.8.8'; $hostId = $osModel?->osGroup?->host?->id ?? 1; $cidr = (string) $this->netmaskToCidr($netmask); // vCenter credentials $vcenter = $moduleVmware->vcenter ?? vCenter::first(); $vcenterHost = $vcenter->host; $vcenterUser = $vcenter->user; $vcenterPass = SignRSA::DecryptFunc($vcenter->password); // Guest OS default admin user per OS family $guestUser = strtoupper($this->osFamily) === 'WINDOWS' ? 'Administrator' : 'root'; // ─── Attempt loop ──────────────────────────────────────────────────── for ($attempt = 1; $attempt <= self::MAX_ATTEMPTS; $attempt++) { Log::info('AssignIpToGuestJob: attempt', [ 'attempt' => $attempt, 'module_vmware_id' => $this->moduleVmwareId, 'network_id' => $networkId, ]); // [3] Reserve a free IP with transaction + row lock $reservation = $this->reserveIp($networkId, $hostId, $gateway, $netmask, $dns); if (!$reservation) { Log::error('AssignIpToGuestJob: no free IPs in pool', [ 'network_id' => $networkId, 'attempt' => $attempt, ]); break; // No point retrying — no IPs available } $ip = $reservation->ip; // [5] Assign IP into VM guest OS via PS1 script $assignOutput = $ssh->assignIp( vmId: $this->vmId, ip: $ip, gateway: $gateway, cidr: $cidr, vcenterHost: $vcenterHost, vcenterUser: $vcenterUser, vcenterPassword: $vcenterPass, guestPassword: $this->guestPassword, osFamily: $this->osFamily, dns: $dns, guestUser: $guestUser, ); // [6] If assign failed → release and try next IP if (!$this->isSuccess($assignOutput)) { Log::warning('AssignIpToGuestJob: assignIp FAIL', [ 'ip' => $ip, 'attempt' => $attempt, 'output' => trim($assignOutput), ]); $this->releaseIp($reservation); continue; } // [7] Verify IP is active inside guest OS via PS1 script $verifyOutput = $ssh->verifyIpInGuest( vmId: $this->vmId, ip: $ip, vcenterHost: $vcenterHost, vcenterUser: $vcenterUser, vcenterPassword: $vcenterPass, guestPassword: $this->guestPassword, osFamily: $this->osFamily, guestUser: $guestUser, ); // [8] If verification failed → release and try next IP if (!$this->isSuccess($verifyOutput)) { Log::warning('AssignIpToGuestJob: verifyIpInGuest FAIL', [ 'ip' => $ip, 'attempt' => $attempt, 'output' => trim($verifyOutput), ]); $this->releaseIp($reservation); continue; } // [9] SUCCESS — persist confirmed IP to DB $this->persistIp($reservation, $moduleVmware); Log::info('AssignIpToGuestJob: IP assigned and verified', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, 'ip' => $ip, 'attempt' => $attempt, ]); // Dispatch credential email now that we have the real IP SendVmCredentialsEmailJob::dispatch($this->moduleVmwareId); return; } // [10] All attempts exhausted Log::error('AssignIpToGuestJob: all attempts failed — marking VM inactive', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, 'attempts' => self::MAX_ATTEMPTS, ]); ModuleVmware::where('id', $this->moduleVmwareId)->update(['status' => 0]); // Still send credential email — try to get IP from vSphere guest identity // so the customer at least gets server info even if IP assignment failed try { $moduleVmware = ModuleVmware::find($this->moduleVmwareId); if ($moduleVmware && !$moduleVmware->ip) { $vcenter = $moduleVmware->vCenter ?? vCenter::first(); if ($vcenter && $vCenterService->loginServer($vcenter)) { $guestInfo = $vCenterService->GetIPAddressVirtualMachine($this->vmId); if (isset($guestInfo['error']) && $guestInfo['error'] === false) { $guestIp = $guestInfo['data']['ip_address'] ?? null; if ($guestIp) { $moduleVmware->update(['ip' => $guestIp, 'status' => 1]); Log::info('AssignIpToGuestJob: recovered IP from guest identity', [ 'module_vmware_id' => $this->moduleVmwareId, 'ip' => $guestIp, ]); } } } } SendVmCredentialsEmailJob::dispatch($this->moduleVmwareId); Log::info('AssignIpToGuestJob: dispatched SendVmCredentialsEmailJob after failed attempts', [ 'module_vmware_id' => $this->moduleVmwareId, ]); } catch (\Throwable $e) { Log::error('AssignIpToGuestJob: failed to send email after exhausted attempts', [ 'module_vmware_id' => $this->moduleVmwareId, 'error' => $e->getMessage(), ]); } } public function failed(\Throwable $e): void { Log::error('AssignIpToGuestJob: job failed with exception', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, 'error' => $e->getMessage(), ]); ModuleVmware::where('id', $this->moduleVmwareId)->update(['status' => 0]); } // ───────────────────────────────────────────────────────────────────────── // assignIp / verifyIp helpers // ───────────────────────────────────────────────────────────────────────── /** * Reserve a free IP from the pool inside a DB transaction with row-level locks. * * Strategy: * 1. Lock all existing ModuleIpAddress rows for the network → prevents double-pick * 2. Lock the chosen IpPool row → prevents concurrent reservations * 3. Create a pending ModuleIpAddress (status=0) to hold the slot * * Returns the pending ModuleIpAddress record, or null if pool is exhausted. */ private function reserveIp( int $networkId, int $hostId, string $gateway, string $netmask, string $dns, ): ?ModuleIpAddress { return DB::transaction(function () use ($networkId, $hostId, $gateway, $netmask, $dns) { // Lock existing rows to prevent another job from picking the same IP $usedIps = ModuleIpAddress::where('module_network_id', $networkId) ->lockForUpdate() ->pluck('ip') ->toArray(); $ipPool = IpPool::where('module_network_id', $networkId) ->whereNotIn('ip_address', $usedIps) ->lockForUpdate() ->inRandomOrder() ->first(); if (!$ipPool) { return null; } // Create a pending reservation — acts as a distributed lock on this IP return ModuleIpAddress::create([ '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 ]); }); } /** * Release a failed reservation by deleting the pending ModuleIpAddress row. * This returns the IP to the available pool. */ private function releaseIp(ModuleIpAddress $reservation): void { try { $reservation->delete(); Log::info('AssignIpToGuestJob: IP reservation released', ['ip' => $reservation->ip]); } catch (\Throwable $e) { Log::warning('AssignIpToGuestJob: 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 * * IP is only written to DB AFTER guest-side verification passes. */ private function persistIp(ModuleIpAddress $reservation, ModuleVmware $moduleVmware): void { DB::transaction(function () use ($reservation, $moduleVmware) { $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, ]); }); } // ───────────────────────────────────────────────────────────────────────── // Micro-helpers // ───────────────────────────────────────────────────────────────────────── /** PS1 scripts return JSON: {"status":"ok"} or {"status":"error","message":"..."} */ private function isSuccess(string $output): bool { $data = json_decode(trim($output), true); return is_array($data) && ($data['status'] ?? '') === 'ok'; } /** Convert dotted-decimal netmask to CIDR prefix length (e.g. 255.255.255.0 → 24). */ private function netmaskToCidr(string $netmask): int { $bits = 0; foreach (explode('.', $netmask) as $octet) { $bits += substr_count(decbin((int) $octet), '1'); } return $bits; } }