item = $item; $this->isRetry = $isRetry; } /** * Execute the job. */ public function handle(vCenter $vCenter, Telegram $telegram) { try { $product = $this->item->getProductAttribute(); if (!$product) { $errorMessage = "Không tìm thấy sản phẩm từ OrderItem #{$this->item->id}"; Log::warning($errorMessage); $telegram->SendErrorMessage($errorMessage, 'CreateVirtualMachineJob - Missing Product'); if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); $this->writeCustomerFailureLog($errorMessage); } return false; } $osModel = Os::with(['host.datacenter.vCenter', 'osGroup.host.datacenter.vCenter'])->find($this->item->os_id); if (!$osModel) { $errorMessage = "Không tìm thấy OS id={$this->item->os_id} trong OrderItem #{$this->item->id}. os_id không hợp lệ."; Log::error($errorMessage); $telegram->SendErrorMessage([ 'error' => $errorMessage, 'os_id' => $this->item->os_id, 'order_item_id' => $this->item->id, 'product_id' => $product->id, ], 'CreateVirtualMachineJob - Invalid OS ID'); if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); $this->writeCustomerFailureLog($errorMessage); } return; } $template = $osModel->template_id; // Ưu tiên 1: host gán trực tiếp trên OS $host = $osModel->host; $datacenter = $host?->datacenter; $vcenter = $datacenter?->vCenter; // Ưu tiên 2: host từ osGroup (cũ) if (!$host || !$datacenter || !$vcenter) { $groupHost = $osModel->osGroup?->host; $host = $host ?? $groupHost; $datacenter = $datacenter ?? $groupHost?->datacenter; $vcenter = $vcenter ?? $groupHost?->datacenter?->vCenter; if ($groupHost) { Log::info('CreateVirtualMachineJob: dùng host từ osGroup làm fallback', [ 'os_id' => $osModel->id, 'os_group_id' => $osModel->os_group_id, 'host_id' => $groupHost->id, ]); } } // Ưu tiên 3: host từ sản phẩm (cấu hình tab Provisioning) if ((!$host || !$datacenter || !$vcenter) && $product->module_host_id) { $productHost = ModuleHost::with('datacenter.vCenter')->find($product->module_host_id); if ($productHost) { $host = $host ?? $productHost; $datacenter = $datacenter ?? $productHost->datacenter; $vcenter = $vcenter ?? $productHost->datacenter?->vCenter; Log::info('CreateVirtualMachineJob: dùng host từ sản phẩm làm fallback', [ 'product_id' => $product->id, 'host_id' => $productHost->id, 'datacenter_id' => $productHost->datacenter?->id, ]); } } if (!$vcenter || !$host || !$datacenter) { $errorMessage = "Thiếu thông tin host/datacenter/vcenter từ sản phẩm ID #{$product->id}. " . "Vui lòng cấu hình vCenter trong tab Provisioning của sản phẩm, " . "hoặc gán host cho OS group của OS được chọn."; Log::error($errorMessage); $telegram->SendErrorMessage([ 'error' => $errorMessage, 'product_id' => $product->id, 'order_item_id' => $this->item->id, 'os_id' => $this->item->os_id, 'os_group_name' => $osModel->osGroup?->name, 'product_module_host_id' => $product->module_host_id, 'host_id' => $host?->id, 'datacenter_id' => $datacenter?->id, 'vcenter_id' => $vcenter?->id, ], 'CreateVirtualMachineJob - Missing Infrastructure'); if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); $this->writeCustomerFailureLog($errorMessage); } return; } // Login to vCenter if (!$vCenter->loginServer($vcenter)) { $errorMessage = "Login vào vCenter thất bại với host: {$vcenter->host}"; Log::error($errorMessage); $telegram->SendErrorMessage([ 'error' => $errorMessage, 'vcenter_host' => $vcenter->host, 'vcenter_user' => $vcenter->user, 'product_id' => $product->id, 'order_item_id' => $this->item->id ], 'CreateVirtualMachineJob - vCenter Login Failed'); if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); $this->writeCustomerFailureLog($errorMessage); } return false; } // Lấy thông tin addon (CPU, RAM) $addonData = [ 'cpu' => 0, 'ram' => 0, 'disk' => 0 ]; // Lấy addon từ order item if ($this->item->addons) { foreach ($this->item->addons as $orderAddon) { $addon = $orderAddon->addonItems; if (!$addon) continue; $addonName = strtolower(trim($addon->name)); $quantity = (int) $orderAddon->qty; switch ($addonName) { case 'cpu': $addonData['cpu'] += $quantity; break; case 'ram': $addonData['ram'] += $quantity; break; case 'disk': case 'ssd': $addonData['disk'] += $quantity; break; } } } // Xác định OS family để chọn username và luồng cấu hình phù hợp $osGroupName = $osModel->osGroup?->name ?? ''; $isWindows = str_contains(strtolower($osGroupName), 'windows'); $vmUser = $isWindows ? 'administrator' : 'root'; $vmPort = $isWindows ? 3389 : 22; $result = $vCenter->CreateVirtualMachine([ 'item' => $this->item, 'host' => $host, 'datacenter' => $datacenter, 'vcenter' => $vcenter, 'template' => $template, 'addon' => $addonData, ]); Log::info('CreateVirtualMachineJob - Result: ' . json_encode($result)); if (is_array($result) && ($result['error'] ?? false)) { $errorMessage = $this->translateVcenterError($result); Log::warning($errorMessage); $telegram->SendErrorMessage([ 'error' => $result['error'] ?? 'Unknown error', 'result' => $result, 'product_id' => $product->id, 'order_item_id' => $this->item->id, 'host_name' => $host?->name, 'datacenter_name' => $datacenter?->name, 'vcenter_host' => $vcenter?->host ], 'CreateVirtualMachineJob - VM Creation Failed'); if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); $this->writeCustomerFailureLog($errorMessage); } return false; } elseif ($result === 'Login_Again') { $errorMessage = 'Tạo máy ảo thất bại: cần đăng nhập lại.'; Log::error($errorMessage); $telegram->SendErrorMessage([ 'error' => $errorMessage, 'product_id' => $product->id, 'order_item_id' => $this->item->id, 'vcenter_host' => $vcenter?->host ], 'CreateVirtualMachineJob - vCenter Re-login Required'); if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); $this->writeCustomerFailureLog($errorMessage); } return false; } else { Log::info('Tạo máy ảo thành công: ' . json_encode($result)); // Lấy vm_id từ kết quả $vmId = is_array($result) ? ($result['data'] ?? 'unknown') : $result; if ($vmId !== 'unknown' && $vmId !== 'UNKNOWN') { try { // vm_id được lưu vào tbl.services qua ModuleVmwareObserver::created() $startResult = $this->startVirtualMachineDirectly($vCenter, $vmId); // VM started — SSH vào vSphere host để đổi mật khẩu qua PS1 script try { $customer = $this->item->order->customer; $serverName = $this->item->server_name ?? 'VM-' . $vmId; $months = (int) ($this->item->months ?? 0); $firstPayment = (int) $this->item->price * ($months ?: 1); $nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null; // Create ModuleVmware without IP — AssignIpToGuestJob will // pick, assign, and verify the real IP later, then write it to DB. $moduleVmware = ModuleVmware::create([ 'name' => $serverName, 'module_ip_address' => null, // set by AssignIpToGuestJob 'ip' => null, // set by AssignIpToGuestJob 'port' => $vmPort, 'order_id' => $this->item->order->id, 'first_payment' => $firstPayment, 'next_payment' => $firstPayment, 'next_due_date' => $nextDueDate, 'next_invoice' => $nextDueDate, 'user' => $vmUser, 'password' => VmProvisionService::TEMPLATE_PASSWORD, 'plain_password' => VmProvisionService::TEMPLATE_PASSWORD, 'hostname' => $serverName, 'vmware_id' => $vmId, 'vcenter_id' => $vcenter->id, 'os_id' => $this->item->os_id ?? 1, 'customer_id' => $customer->id, 'status' => 1, ]); // Sync services table $this->syncServiceRecord($moduleVmware); // Step 1 of guest provisioning: change template password. // On success ChangePasswordViaVsphereJob dispatches AssignIpToGuestJob, // which handles IP selection, assignment, verification, and email. ChangePasswordViaVsphereJob::dispatch( $moduleVmware->id, $vmId, $isWindows ? VmProvisionService::OS_WINDOWS : VmProvisionService::OS_LINUX, ); Log::info('ChangePasswordViaVsphereJob dispatched', [ 'module_vmware_id' => $moduleVmware->id, 'vm_id' => $vmId, 'os_family' => $isWindows ? 'WINDOWS' : 'LINUX', ]); } catch (\Exception $e) { Log::error('❌ Lỗi khi lưu dữ liệu VM và IP address: ' . $e->getMessage(), [ 'vmware_id' => $vmId, 'order_item_id' => $this->item->id, 'error' => $e->getMessage() ]); } } catch (\Exception $e) { Log::error('❌ Lỗi khi lưu dữ liệu VM hoặc start VM: ' . $e->getMessage(), [ 'vm_id' => $vmId, 'order_id' => $this->item->order->id, 'trace' => $e->getTraceAsString() ]); } } $telegram->SendJobRunInfo( 'CreateVirtualMachineJob', 'success', [ 'product_id' => $product->id, 'order_item_id' => $this->item->id, 'result' => $result, 'addon_applied' => !empty($addonData) && array_sum($addonData) > 0, 'vm_id_saved' => $vmId !== 'unknown' && $vmId !== 'UNKNOWN', 'vm_started' => isset($startResult) && is_array($startResult) && $startResult['error'] === false ] ); $this->writeCustomerSuccessLog(); try { $order = $this->item->order; $customer = $order->customer; broadcast(new \App\Events\OrderProvisioned( orderId: $order->id, orderNum: $order->order_num, customerName: trim($customer->firstname . ' ' . $customer->lastname), vmId: (string) $vmId, success: true, message: 'Máy ảo đã được khởi tạo thành công cho khách hàng ' . trim($customer->firstname . ' ' . $customer->lastname) . ' (Đơn #' . $order->order_num . ')', )); } catch (\Throwable $e) { Log::warning('Không thể broadcast OrderProvisioned: ' . $e->getMessage()); } return true; } } catch (\Throwable $e) { $errorMessage = 'Lỗi khi tạo máy ảo (Job): ' . $e->getMessage(); Log::error($errorMessage, [ 'exception' => $e, 'order_item_id' => $this->item->id, 'trace' => $e->getTraceAsString() ]); $telegram->SendErrorMessage([ 'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'order_item_id' => $this->item->id, 'trace' => substr($e->getTraceAsString(), 0, 500) // Giới hạn độ dài trace ], 'CreateVirtualMachineJob - Exception'); if (!$this->isRetry) { $this->sendFailureNotification($errorMessage); $this->writeCustomerFailureLog($errorMessage); } return false; } } private function writeCustomerSuccessLog(): void { try { $order = $this->item->order; $customer = $order?->customer; if (!$order || !$customer) { return; } CustomerLog::create([ 'agent' => 'system', 'client_ip' => null, 'customer_id' => $customer->id, 'description' => 'Khởi tạo dịch vụ thành công cho đơn hàng #' . $order->order_num . ' (product_id: ' . $this->item->product_id . ', os_id: ' . $this->item->os_id . ').', ]); } catch (\Throwable $e) { Log::warning('Không thể ghi log thành công khởi tạo VM: ' . $e->getMessage()); } } private function writeCustomerFailureLog(string $errorMessage): void { try { $order = $this->item->order; $customer = $order?->customer; if (!$order || !$customer) { return; } CustomerLog::create([ 'agent' => 'system', 'client_ip' => null, 'customer_id' => $customer->id, 'description' => 'Khởi tạo dịch vụ thất bại cho đơn hàng #' . $order->order_num . ' (product_id: ' . $this->item->product_id . ', os_id: ' . $this->item->os_id . ').' . ' Lỗi: ' . $errorMessage . '. ' . 'Nếu có lỗi xảy ra khi khởi tạo dịch vụ quý khách nhấn khởi tạo tại đây, hoặc quý khách có thể liên hệ với kỹ thuật support@tgs.com.vn', ]); } catch (\Throwable $e) { Log::warning('Không thể ghi log thất bại khởi tạo VM: ' . $e->getMessage()); } } /** * Start VM directly using vCenter API */ private function startVirtualMachineDirectly(vCenter $vCenter, string $vmId): array|string { try { // Kiểm tra xem đã có session chưa $check = $vCenter->checkApiExits(); if (!$check) { return 'Login_Again'; } // Gọi API start VM trực tiếp $response = Http::withoutVerifying() ->withHeaders([ 'vmware-api-session-id' => $vCenter->token ]) ->post("https://{$vCenter->apiUri}/rest/vcenter/vm/{$vmId}/power/start"); if ($response->status() === 200) { return [ 'error' => false, 'data' => 'success' ]; } return [ 'error' => true, 'data' => $response->body() ]; } catch (\Exception $e) { Log::error('Lỗi khi start VM: ' . $e->getMessage()); return [ 'error' => true, 'data' => $e->getMessage() ]; } } /** * Send failure notification to customer */ private function sendFailureNotification(string $errorMessage): void { try { $customer = $this->item->order->customer; $product = $this->item->getProductAttribute(); VMCreationFailedJob::dispatch([ 'customer_email' => $customer->email, 'customer_name' => trim($customer->firstname . ' ' . $customer->lastname), 'product_name' => $product?->name ?? 'Unknown Product', 'order_id' => $this->item->order->id, 'order_num' => $this->item->order->order_num, 'error_message' => $errorMessage, 'order_item_id' => $this->item->id ]); } catch (\Throwable $e) { Log::error('Không thể gửi email thông báo lỗi VM: ' . $e->getMessage()); } } /** * Sync module_vmware info → services table. * Link: module_vmware.order_id → order_items.order_id → services.order_item_id */ private function syncServiceRecord(ModuleVmware $moduleVmware): void { try { $orderItem = OrderItem::where('order_id', $moduleVmware->order_id)->first(); if (!$orderItem) { Log::warning('syncServiceRecord: không tìm thấy OrderItem', [ 'order_id' => $moduleVmware->order_id, 'module_vmware_id' => $moduleVmware->id, ]); return; } $months = (int) ($orderItem->months ?? 0); $updated = Service::where('order_item_id', $orderItem->id) ->update([ 'vm_id' => $moduleVmware->id, 'status' => 'active', 'expires_at' => $months > 0 ? now()->addMonths($months) : null, ]); Log::info('syncServiceRecord: service synced', [ 'module_vmware_id' => $moduleVmware->id, 'vmware_id' => $moduleVmware->vmware_id, 'order_item_id' => $orderItem->id, 'rows_updated' => $updated, ]); } catch (\Throwable $e) { Log::error('syncServiceRecord: exception — ' . $e->getMessage(), [ 'module_vmware_id' => $moduleVmware->id, 'order_id' => $moduleVmware->order_id, ]); } } /** * Gửi email thông báo VM đã khởi tạo thành công cho khách hàng. * Gọi ngay sau khi tạo module_vmware — cho cả Linux lẫn Windows. */ /** * Dịch error từ vCenter API sang tiếng Việt dễ hiểu. */ private function translateVcenterError(array $result): string { $data = $result['data'] ?? []; $errorType = is_array($data) ? ($data['error_type'] ?? '') : ''; $serverName = $this->item->server_name ?? ''; $map = [ 'ALREADY_EXISTS' => "Tên máy chủ \"{$serverName}\" đã tồn tại trong hệ thống. Vui lòng liên hệ hỗ trợ để được xử lý hoặc chọn tên máy chủ khác.", 'NOT_FOUND' => 'Không tìm thấy template hoặc tài nguyên trên vCenter. Vui lòng liên hệ hỗ trợ.', 'RESOURCE_INACCESSIBLE' => 'Tài nguyên vCenter không khả dụng. Vui lòng thử lại sau hoặc liên hệ hỗ trợ.', 'INVALID_ARGUMENT' => 'Thông tin cấu hình máy chủ không hợp lệ. Vui lòng liên hệ hỗ trợ.', 'UNABLE_TO_ALLOCATE_RESOURCE' => 'Không đủ tài nguyên để khởi tạo máy chủ. Vui lòng liên hệ hỗ trợ.', 'TIMED_OUT' => 'Yêu cầu khởi tạo máy chủ hết thời gian chờ. Vui lòng thử lại sau.', ]; if ($errorType && isset($map[$errorType])) { return $map[$errorType]; } // Fallback: không để lộ JSON thô ra email khách hàng return "Không thể khởi tạo máy chủ ảo lúc này. Chúng tôi đã ghi nhận và sẽ xử lý trong thời gian sớm nhất. Vui lòng liên hệ hỗ trợ nếu cần."; } private function getBillingCycleLabel(?int $months): string { return match ($months) { 1 => 'Hàng tháng', 3 => '3 Tháng', 6 => '6 Tháng', 12 => 'Hàng năm', 24 => '2 Năm', default => $months . ' Tháng', }; } }