505 lines
21 KiB
PHP
505 lines
21 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs\VirtualMachine\Create;
|
|
|
|
use App\Http\Services\Telegram;
|
|
use App\Http\Services\vCenter;
|
|
use App\Jobs\Mail\Customer\VMCreationFailedJob;
|
|
use App\Jobs\VirtualMachine\Provision\ChangePasswordViaVsphereJob;
|
|
use App\Services\VmProvisionService;
|
|
use App\Models\OrderItem;
|
|
use App\Models\Os;
|
|
use App\Models\ModuleVmware;
|
|
use App\Models\Log as CustomerLog;
|
|
use App\Models\Service;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
class CreateVirtualMachineJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
/** Cho phép job chạy tối đa 10 phút (VM clone có thể mất 5-8 phút) */
|
|
public int $timeout = 600;
|
|
|
|
/** Không retry tự động — lỗi VM cần admin kiểm tra thủ công */
|
|
public int $tries = 1;
|
|
|
|
public OrderItem $item;
|
|
public bool $isRetry = false;
|
|
|
|
/**
|
|
* Create a new job instance.
|
|
*/
|
|
public function __construct(OrderItem $item, bool $isRetry = false)
|
|
{
|
|
$this->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;
|
|
|
|
$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.
|
|
*/
|
|
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',
|
|
};
|
|
}
|
|
}
|