Files
portal-api/app/Jobs/VirtualMachine/Provision/RetryProvisioningOrchestrator.php
T
baodev0 520f21cb11 feat: add GeneratesUniqueSlug class for unique slug generation
feat: create PostCategoryFactory for generating post category data

feat: create PostFactory for generating post data

feat: add retry_requested_at column to services table

feat: add vcenter provisioning fields to products table

feat: add module_host_id to os table

feat: create email_notification_templates table for email templates

feat: add scenario and format fields to template table

feat: add admin fields to transactions table

fix: update ticket table columns and types

feat: add uuid to ticket table and backfill existing records

fix: make ticket_reply user_id nullable

feat: create notifications table for user notifications

fix: make ticket uuid nullable

feat: create post_categories table for blog categories

feat: create posts table for blog posts

feat: add invoice email template for PDF generation
2026-05-25 15:39:42 +07:00

380 lines
16 KiB
PHP

<?php
namespace App\Jobs\VirtualMachine\Provision;
use App\Http\Services\Telegram;
use App\Http\Services\vCenter;
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
use App\Models\Invoice;
use App\Models\ModuleVmware;
use App\Models\Os;
use App\Models\OrderItem;
use App\Models\Service;
use App\Models\vCenter as VcenterModel;
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\DB;
use Illuminate\Support\Facades\Log;
/**
* RetryProvisioningOrchestrator
*
* Orchestrator Job — retries VM provisioning for a paid order.
*
* Flow:
* 1. Load order_items by order_id → get server_name
* 2. Resolve vcenter via DB chain (os → osGroup → host → datacenter → vcenter)
* 3. Login vCenter, search VM by server_name
* 4. If VM EXISTS in vSphere → link vm-id to module_vmware + services
* → dispatch AssignIpToGuestJob (if no IP) + SendVmCredentialsEmailJob
* 5. If VM NOT in vSphere → dispatch CreateVirtualMachineJob
* 6. Each failure step notifies Telegram with specific error info
*/
class RetryProvisioningOrchestrator implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 1;
public int $timeout = 300;
public function __construct(
public readonly int $orderId,
public readonly int $customerId,
) {}
public function handle(vCenter $vCenterService, Telegram $telegram): void
{
Log::info('RetryProvisioningOrchestrator: start', [
'order_id' => $this->orderId,
'customer_id' => $this->customerId,
]);
$orderItems = OrderItem::where('order_id', $this->orderId)->get();
if ($orderItems->isEmpty()) {
$this->failStep('order_items', 'Không tìm thấy order_items cho order #' . $this->orderId, $telegram);
return;
}
foreach ($orderItems as $item) {
$this->processItem($item, $vCenterService, $telegram);
}
}
private function processItem(OrderItem $item, vCenter $vCenterService, Telegram $telegram): void
{
$serverName = $item->server_name;
if (empty($serverName)) {
$this->failStep('order_item', "OrderItem #{$item->id} không có server_name", $telegram);
return;
}
// ── Already linked and has IP? Skip ─────────────────────────────
$existingService = Service::where('order_item_id', $item->id)->whereNull('deleted_at')->first();
if ($existingService && $existingService->vm_id) {
$vmware = ModuleVmware::find($existingService->vm_id);
if ($vmware && $vmware->ip) {
Log::info('RetryProvisioningOrchestrator: already linked with IP, skipping', [
'order_item_id' => $item->id,
'service_id' => $existingService->id,
'vm_id' => $existingService->vm_id,
'ip' => $vmware->ip,
]);
return;
}
}
// ── Step 1: Resolve vcenter from DB chain ────────────────────────
$vcenterModel = $this->resolveVcenter($item, $telegram);
if (!$vcenterModel) {
$vcenterModel = VcenterModel::first();
if (!$vcenterModel) {
$this->failStep('vcenter', 'Không tìm thấy vCenter nào trong hệ thống cho order_item #' . $item->id, $telegram);
return;
}
Log::warning('RetryProvisioningOrchestrator: fallback to first vcenter', ['vcenter_id' => $vcenterModel->id]);
}
// ── Step 2: Login vCenter and search VM ─────────────────────────
$loginResult = $vCenterService->loginServer($vcenterModel);
if (!$loginResult) {
$this->failStep('vcenter_login', 'Không thể đăng nhập vào vCenter ' . $vcenterModel->host . ' cho VM ' . $serverName, $telegram);
return;
}
// Search by REST API first
$vm = $vCenterService->findVirtualMachineByName($serverName);
// If REST search fails, try listing all VMs and filter manually
if (!$vm) {
Log::info('RetryProvisioningOrchestrator: REST search returned null, trying list-all fallback', [
'server_name' => $serverName,
]);
$allVms = $vCenterService->getAllVirtualMachine();
if (is_array($allVms)) {
foreach ($allVms as $candidate) {
if (isset($candidate['name']) && $candidate['name'] === $serverName) {
$vm = $candidate;
Log::info('RetryProvisioningOrchestrator: found VM via list-all fallback', [
'server_name' => $serverName,
'vm_id' => $candidate['vm'] ?? null,
]);
break;
}
}
}
}
if ($vm) {
// ── VM EXISTS → link to DB + assign IP + send email ────────────
$vmId = $vm['vm'] ?? null;
if (!$vmId) {
$this->failStep('vm_search', 'Tìm thấy VM nhưng không lấy được vm_id cho ' . $serverName, $telegram);
return;
}
Log::info('RetryProvisioningOrchestrator: VM found in vSphere', [
'server_name' => $serverName,
'vm_id' => $vmId,
'vcenter' => $vcenterModel->host,
]);
try {
$moduleVmware = $this->linkVmToDb($item, $vmId, $vcenterModel, $serverName);
// Determine OS family for AssignIpToGuestJob
$osModel = $item->os_id ? Os::find($item->os_id) : null;
$osFamily = $this->resolveOsFamily($osModel);
// If VM has no IP yet → dispatch AssignIpToGuestJob
if (!$moduleVmware->ip) {
AssignIpToGuestJob::dispatch(
moduleVmwareId: $moduleVmware->id,
vmId: $vmId,
osFamily: $osFamily,
guestPassword: $moduleVmware->plain_password ?? \App\Services\VmProvisionService::TEMPLATE_PASSWORD,
);
Log::info('RetryProvisioningOrchestrator: dispatched AssignIpToGuestJob', [
'module_vmware_id' => $moduleVmware->id,
'vm_id' => $vmId,
]);
} else {
// Already has IP → just send credential email
SendVmCredentialsEmailJob::dispatch($moduleVmware->id);
Log::info('RetryProvisioningOrchestrator: dispatched SendVmCredentialsEmailJob', [
'module_vmware_id' => $moduleVmware->id,
]);
}
$telegram->SendMessage(
"✅ Phục hồi VM thành công\n"
. "━━━━━━━━━━━━━━━━━━━━\n"
. "🖥️ Tên VM: {$serverName}\n"
. "🔑 VM ID: {$vmId}\n"
. "📦 Đơn hàng: #{$this->orderId}\n"
. "🌐 Có IP: " . ($moduleVmware->ip ? 'Có (' . $moduleVmware->ip . ')' : 'Chưa có — đang gán IP') . "\n"
. "⏱️ Thời gian: " . now()->format('d/m/Y H:i:s')
);
} catch (\Throwable $e) {
$this->failStep('link_vm', 'Lỗi liên kết VM ' . $serverName . ': ' . $e->getMessage(), $telegram);
}
} else {
// ── VM NOT in vSphere → dispatch CreateVirtualMachineJob ──────
Log::info('RetryProvisioningOrchestrator: VM not found in vSphere, dispatching CreateVirtualMachineJob', [
'server_name' => $serverName,
'order_item_id' => $item->id,
]);
CreateVirtualMachineJob::dispatch($item, true);
$telegram->SendMessage(
"🆕 Tạo VM mới\n"
. "━━━━━━━━━━━━━━━━━━━━\n"
. "🖥️ Tên VM: {$serverName}\n"
. "📦 Đơn hàng: #{$this->orderId}\n"
. "📝 VM không có trong vSphere, đã gửi job tạo mới\n"
. "⏱️ Thời gian: " . now()->format('d/m/Y H:i:s')
);
}
}
/**
* Resolve vcenter from DB chain:
* Path 1a: order_items.os_id → os.host → datacenter → vcenter (ưu tiên: host gán trực tiếp trên OS)
* Path 1b: order_items.os_id → os → osGroup → host → datacenter → vcenter
* Path 2a: invoices → product → os_product → os.host → datacenter → vcenter
* Path 2b: invoices → product → os_product → os → osGroup → host → datacenter → vcenter
* Path 3: module_vmware with same order_id (partial provisioning)
*/
private function resolveVcenter(OrderItem $item, Telegram $telegram): ?VcenterModel
{
if ($item->os_id) {
$os = Os::with(['host.datacenter.vCenter', 'osGroup.host.datacenter.vCenter'])->find($item->os_id);
// Path 1a: host gán trực tiếp trên OS
$vcenter = $os?->host?->datacenter?->vCenter;
if ($vcenter) {
Log::info('resolveVcenter: found via os.host chain', ['os_id' => $item->os_id, 'vcenter_id' => $vcenter->id]);
return $vcenter;
}
// Path 1b: host qua osGroup (cũ)
$vcenter = $os?->osGroup?->host?->datacenter?->vCenter;
if ($vcenter) {
Log::info('resolveVcenter: found via os → osGroup chain', ['os_id' => $item->os_id, 'vcenter_id' => $vcenter->id]);
return $vcenter;
}
}
$invoice = Invoice::where('order_id', $item->order_id)->first();
if ($invoice) {
$invoiceItem = $invoice->invoiceItems()->whereNotNull('product_id')->first();
if ($invoiceItem) {
$osRecord = DB::table('os_product')
->where('product_id', $invoiceItem->product_id)
->first();
if ($osRecord) {
$os = Os::with(['host.datacenter.vCenter', 'osGroup.host.datacenter.vCenter'])->find($osRecord->os_id);
// Path 2a: host gán trực tiếp trên OS
$vcenter = $os?->host?->datacenter?->vCenter;
if ($vcenter) {
Log::info('resolveVcenter: found via invoice → os.host chain', ['vcenter_id' => $vcenter->id]);
return $vcenter;
}
// Path 2b: host qua osGroup
$vcenter = $os?->osGroup?->host?->datacenter?->vCenter;
if ($vcenter) {
Log::info('resolveVcenter: found via invoice → os → osGroup chain', ['vcenter_id' => $vcenter->id]);
return $vcenter;
}
}
}
}
$existingVmware = ModuleVmware::where('order_id', $item->order_id)->first();
if ($existingVmware?->vcenter_id) {
$vcenter = VcenterModel::find($existingVmware->vcenter_id);
if ($vcenter) {
Log::info('resolveVcenter: found via existing module_vmware', ['vcenter_id' => $vcenter->id]);
return $vcenter;
}
}
$this->failStep('resolve_vcenter', 'Không thể tìm vcenter cho order_item #' . $item->id, $telegram);
return null;
}
/**
* Link an existing VM from vSphere to module_vmware + services tables.
* Returns the ModuleVmware record.
*/
private function linkVmToDb(OrderItem $item, string $vmId, VcenterModel $vcenterModel, string $serverName): ModuleVmware
{
$order = $item->order;
$customer = $order->customer;
$months = (int) ($item->months ?? 0);
$firstPayment = (int) $item->price * ($months ?: 1);
$nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null;
$moduleVmware = ModuleVmware::where('vmware_id', $vmId)
->where('vcenter_id', $vcenterModel->id)
->first();
if (!$moduleVmware) {
$moduleVmware = ModuleVmware::create([
'name' => $serverName,
'module_ip_address' => null,
'ip' => null,
'port' => 3389,
'order_id' => $order->id,
'first_payment' => $firstPayment,
'next_payment' => $firstPayment,
'next_due_date' => $nextDueDate,
'next_invoice' => $nextDueDate,
'user' => 'Administrator',
'password' => \App\Services\VmProvisionService::TEMPLATE_PASSWORD,
'plain_password' => \App\Services\VmProvisionService::TEMPLATE_PASSWORD,
'hostname' => $serverName,
'vmware_id' => $vmId,
'vcenter_id' => $vcenterModel->id,
'os_id' => $item->os_id ?? 1,
'customer_id' => $customer->id,
'status' => 1,
]);
}
$service = Service::withTrashed()->firstOrNew([
'order_item_id' => $item->id,
]);
if ($service->trashed()) {
$service->restore();
}
$service->fill([
'customer_id' => $customer->id,
'product_id' => $item->product_id,
'invoice_id' => $order->invoice?->id,
'vm_id' => $moduleVmware->id,
'server_name' => $serverName,
'status' => 'active',
'started_at' => now(),
'expires_at' => $months > 0 ? now()->addMonths($months) : null,
]);
if (!$service->exists && !$service->uuid) {
$service->uuid = (string) \Illuminate\Support\Str::uuid();
}
$service->save();
if (!$moduleVmware->service_id) {
$moduleVmware->update(['service_id' => $service->id]);
}
Log::info('RetryProvisioningOrchestrator: VM linked to DB', [
'module_vmware_id' => $moduleVmware->id,
'service_id' => $service->id,
'vmware_id' => $vmId,
]);
return $moduleVmware;
}
private function resolveOsFamily(?Os $os): string
{
if (!$os) {
return 'LINUX';
}
$groupName = strtolower($os->osGroup?->name ?? '');
if (str_contains($groupName, 'windows')) {
return 'WINDOWS';
}
return 'LINUX';
}
private function failStep(string $step, string $message, Telegram $telegram): void
{
Log::error("RetryProvisioningOrchestrator [{$step}]: {$message}", [
'order_id' => $this->orderId,
]);
$telegram->SendErrorMessage(
"Bước: {$step}\n"
. "Đơn hàng: #{$this->orderId}\n"
. "Lỗi: {$message}"
);
}
}