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('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; $host = $osModel->osGroup?->host; // từ os_group.module_host_id $datacenter = $host?->datacenter; $vcenter = $datacenter?->vcenter; if (!$vcenter || !$host || !$datacenter) { $errorMessage = "Thiếu thông tin host/datacenter/vcenter từ sản phẩm ID #{$product->id}"; Log::error($errorMessage); $telegram->SendErrorMessage([ 'error' => $errorMessage, 'product_id' => $product->id, 'order_item_id' => $this->item->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 = 'Tạo máy ảo thất bại: ' . json_encode($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; $networkId = $product->ipAddress?->module_network_id ?? 1; $availableIP = $this->getAvailableIPFromPool($networkId); $moduleIpAddress = ModuleIpAddress::create([ 'vm_name' => $serverName, 'module_host_id' => $host->id, 'module_network_id' => $networkId, 'ip' => $availableIP, 'gateway' => $product->ipAddress?->gateway ?? '0.0.0.0', 'netmask' => $product->ipAddress?->netmask ?? '255.255.255.0', 'dns' => $product->ipAddress?->dns ?? '8.8.8.8', 'status' => 1 // 1 = Assigned ]); $months = (int) ($this->item->months ?? 0); $firstPayment = (int) $this->item->price * ($months ?: 1); $nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null; $moduleVmware = ModuleVmware::create([ 'name' => $serverName, 'module_ip_address' => $moduleIpAddress->id, 'ip' => $availableIP, '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); // SSH → vSphere host → PS1 → change password → send 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()); } } private function getAvailableIPFromPool(int $networkId): string { // Lấy tất cả IPs đã được assign trong network này $assignedIPs = ModuleIpAddress::where('module_network_id', $networkId) ->pluck('ip') ->toArray(); // Lấy ngẫu nhiên 1 IP từ pool chưa được assign $available = IpPool::where('module_network_id', $networkId) ->when(!empty($assignedIPs), fn ($q) => $q->whereNotIn('ip_address', $assignedIPs)) ->inRandomOrder() ->first(); if (!$available) { Log::warning('getAvailableIPFromPool: không còn IP trống trong pool', [ 'network_id' => $networkId, 'assigned_count' => count($assignedIPs), ]); return '0.0.0.0'; } Log::info('getAvailableIPFromPool: IP được chọn', [ 'network_id' => $networkId, 'ip' => $available->ip_address, ]); return $available->ip_address; } /** * 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. */ 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', }; } }