for('UpdateVmConfigJob'); $logger->step('UpdateVmConfigJob started', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, ]); $moduleVmware = ModuleVmware::with(['order.orderItems.product', 'vcenter'])->find($this->moduleVmwareId); if (!$moduleVmware) { Log::error('UpdateVmConfigJob: ModuleVmware not found', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, ]); $this->nextStep(); return; } $isManualOverride = $this->cpuOverride !== null || $this->ramOverride !== null; $addonCpu = $addonRamGb = 0; $product = null; if (!$isManualOverride) { $orderItem = $moduleVmware->order?->orderItems?->first(); $product = $orderItem?->product; $addonsMeta = is_array($orderItem?->addons_meta) ? $orderItem->addons_meta : []; [$addonCpu, $addonRamGb] = $this->resolveAddonCpuRam($addonsMeta); if ($addonCpu === 0 && $addonRamGb === 0) { $logger->step('no cpu/ram addon — powering on and skipping resize', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, ]); $vcenterForPowerOn = $moduleVmware->vcenter ?? vCenter::first(); if ($vcenterForPowerOn) { $this->powerCycle($vcenterForPowerOn, (string) $moduleVmware->vmware_id, $logger, powerOff: false); } $this->nextStep(); return; } } // Load vCenter credentials — same pattern as ChangePasswordViaVsphereJob $vcenter = $moduleVmware->vcenter ?? vCenter::first(); if (!$vcenter) { $msg = "UpdateVmConfigJob: no vCenter record found for server [{$moduleVmware->name}]"; Log::error($msg, ['module_vmware_id' => $this->moduleVmwareId, 'server_name' => $moduleVmware->name]); $telegram->SendMessage("🚨 UpdateVmConfigJob: Lỗi\n⚠️ Không tìm thấy vCenter\n🖥️ Server: {$moduleVmware->name}\n🕒 " . Carbon::now()->format('d/m/Y H:i:s')); throw new \RuntimeException($msg); } $vcenterHost = $vcenter->host; $vcenterUser = $vcenter->user; $vcenterPass = SignRSA::DecryptFunc($vcenter->password); $serverName = (string) $moduleVmware->name; if ($isManualOverride) { $totalCpu = $this->cpuOverride ?? (int) $moduleVmware->cpu; $totalRam = $this->ramOverride ?? (int) ($moduleVmware->ram_mb / 1024); } else { $totalCpu = ((int) ($product?->cpu ?? 0)) + $addonCpu; $totalRam = ((int) ($product?->ram ?? 0)) + $addonRamGb; } $logger->step('calling updateVMConfig.ps1', [ 'module_vmware_id' => $this->moduleVmwareId, 'server_name' => $serverName, 'vcenter_host' => $vcenterHost, 'manual_override' => $isManualOverride, 'total_cpu' => $totalCpu, 'total_ram_gb' => $totalRam, ]); // Power off before resizing (both the create-flow addon path and admin // manual overrides) — hot CPU/RAM resize isn't reliable across guest // OSes and is what caused Set-VM to reject the config as invalid. $this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: true); try { $output = $ssh->updateVmConfig( $serverName, $totalCpu, $totalRam, $vcenterHost, $vcenterUser, $vcenterPass, ); } finally { $this->powerCycle($vcenter, (string) $moduleVmware->vmware_id, $logger, powerOff: false); } Log::info('UpdateVmConfigJob: PS1 output', [ 'module_vmware_id' => $this->moduleVmwareId, 'server_name' => $serverName, 'output' => trim($output), ]); if (!str_contains(strtoupper(trim($output)), 'SUCCESS')) { $msg = "updateVMConfig.ps1 failed for server [{$serverName}]. Output: " . trim($output); Log::error('UpdateVmConfigJob: script returned FAIL', [ 'module_vmware_id' => $this->moduleVmwareId, 'server_name' => $serverName, 'total_cpu' => $totalCpu, 'total_ram_gb' => $totalRam, 'output' => trim($output), 'attempt' => $this->attempts(), 'max_tries' => $this->tries, ]); $telegram->SendMessage( "⚠️ UpdateVmConfigJob thất bại\n" . "🖥️ Server: {$serverName}\n" . "🔁 Lần thử: {$this->attempts()}/{$this->tries}\n" . "💻 CPU: {$totalCpu} | RAM: {$totalRam} GB\n" . "📋 Output: " . htmlspecialchars(trim($output) ?: '(trống)') . "\n" . "🕒 " . Carbon::now()->format('d/m/Y H:i:s') ); throw new \RuntimeException($msg); } $logger->success('VM config updated', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, 'total_cpu' => $totalCpu, 'total_ram_gb' => $totalRam, 'output' => trim($output), ]); if ($isManualOverride) { $moduleVmware->update(['cpu' => $totalCpu, 'ram_mb' => $totalRam * 1024]); $this->syncServiceAddons($moduleVmware, $totalCpu, $totalRam, $logger); return; } $this->nextStep(); } public function failed(\Throwable $e): void { Log::error('UpdateVmConfigJob: all retries exhausted', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); ModuleVmware::where('id', $this->moduleVmwareId)->update(['status' => 0]); Service::where('vm_id', $this->moduleVmwareId)->update(['status' => 'failed']); try { $telegram = app(Telegram::class); $vm = ModuleVmware::with(['order.customer'])->find($this->moduleVmwareId); $customer = $vm?->order?->customer; $telegram->SendMessage( "🚨 UpdateVmConfigJob: Thất bại hoàn toàn\n" . "━━━━━━━━━━━━━━━━━━━━\n" . "🖥️ VM ID: {$this->vmId}\n" . "📦 Server: " . ($vm?->name ?? 'N/A') . "\n" . "👤 Khách hàng: " . ($customer ? trim($customer->firstname . ' ' . $customer->lastname) : 'N/A') . "\n" . "📧 Email: " . ($customer?->email ?? 'N/A') . "\n" . "❌ Lỗi: " . htmlspecialchars($e->getMessage()) . "\n" . "⚡ Trạng thái: VM đã bị đánh dấu inactive\n" . "🕒 " . Carbon::now()->format('d/m/Y H:i:s') . "\n" . "━━━━━━━━━━━━━━━━━━━━\n" . "⚠️ Vui lòng kiểm tra và xử lý thủ công!" ); } catch (\Throwable $te) { Log::warning('UpdateVmConfigJob: could not send Telegram failure notification', [ 'error' => $te->getMessage(), ]); } } // ───────────────────────────────────────────────────────────────────────── /** * Resolve cpu/ram qty from addons_meta. * Uses 'code' field saved by OrderService::resolveAddons() — no extra DB query needed. * Falls back to Addon::find() if 'code' is missing (legacy meta without code). * * @param array $addonsMeta * @return array{int, int} [cpu_count, ram_gb] */ private function resolveAddonCpuRam(array $addonsMeta): array { $cpu = 0; $ram = 0; foreach ($addonsMeta as $meta) { $qty = (int) ($meta['qty'] ?? 0); $code = strtolower((string) ($meta['code'] ?? '')); // Fallback: look up code from DB if not in meta (old orders) if ($code === '' && isset($meta['addon_id'])) { $addon = Addon::find($meta['addon_id']); $code = strtolower((string) ($addon?->code ?? '')); } if ($code === 'cpu') { $cpu += $qty; } elseif ($code === 'ram') { $ram += $qty; } } return [$cpu, $ram]; } /** * Power off/on the VM via vCenter REST and wait (up to 30s) for the state to land. * Hot-add isn't reliable for CPU/RAM resize on every guest OS, so we * power-cycle around the resize (and around the power-on-only skip path) * to guarantee the config applies and the VM ends up running either way. */ private function powerCycle(vCenter $vcenter, string $vmwareId, VmProvisionLogger $logger, bool $powerOff): void { if ($vmwareId === '') { return; } $service = new \App\Http\Services\vCenter(); if (!$service->loginServer($vcenter)) { Log::warning('UpdateVmConfigJob: cannot login to vCenter for power cycle', ['vmware_id' => $vmwareId]); return; } $action = $powerOff ? 'stop' : 'start'; $targetState = $powerOff ? 'POWERED_OFF' : 'POWERED_ON'; Http::withoutVerifying() ->withHeaders(['vmware-api-session-id' => $service->token]) ->post("https://{$service->apiUri}/rest/vcenter/vm/{$vmwareId}/power/{$action}"); for ($i = 0; $i < 15; $i++) { sleep(2); $state = Http::withoutVerifying() ->withHeaders(['vmware-api-session-id' => $service->token]) ->get("https://{$service->apiUri}/rest/vcenter/vm/{$vmwareId}") ->json('value.power_state'); if ($state === $targetState) { $logger->step("power cycle reached {$targetState}", ['vmware_id' => $vmwareId]); return; } } $logger->step("power cycle timed out waiting for {$targetState}", ['vmware_id' => $vmwareId]); } /** * Reflect the new CPU/RAM on the service's addon rows (service_addons.addon_value) * so billing/admin views match what's actually running on the VM. */ private function syncServiceAddons(ModuleVmware $moduleVmware, int $totalCpu, int $totalRam, VmProvisionLogger $logger): void { $service = Service::where('vm_id', $moduleVmware->id)->first(); if (!$service) { $logger->step('syncServiceAddons: no service found for vm_id — skipping', [ 'module_vmware_id' => $moduleVmware->id, ]); return; } ServiceAddon::updateOrCreate( ['service_id' => $service->id, 'addon_type' => 'cpu'], ['addon_value' => (string) $totalCpu], ); ServiceAddon::updateOrCreate( ['service_id' => $service->id, 'addon_type' => 'ram'], ['addon_value' => (string) ($totalRam * 1024)], ); $logger->step('service_addons synced', [ 'service_id' => $service->id, 'total_cpu' => $totalCpu, 'total_ram_mb' => $totalRam * 1024, ]); } private function nextStep(): void { AssignIpToGuestJob::dispatch( $this->moduleVmwareId, $this->vmId, $this->osFamily, VmProvisionService::TEMPLATE_PASSWORD, $this->reservationId, $this->reservedIpPoolId, ); } }