- Mail campaign: mẫu nhắc gia hạn thanh toán QR, chọn dịch vụ + chu kỳ từng dòng (renewal_services) - Hosting: nhiều server Plesk cho một gói (service_plan_servers), chuyển server khi server chết - Admin xem/sửa giấy tờ định danh khách, tuỳ chọn gia hạn cho khách, nhật ký cấp phát dịch vụ - Đánh dấu mẫu email dịch vụ là non-marketing Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
383 lines
16 KiB
PHP
383 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;
|
|
use App\Support\ServiceProvisionLog;
|
|
|
|
/**
|
|
* 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::templatePassword(),
|
|
);
|
|
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::templatePassword(),
|
|
'plain_password' => \App\Services\VmProvisionService::templatePassword(),
|
|
'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();
|
|
|
|
ServiceProvisionLog::record($service, 'provision_completed', 'Khởi tạo lại hoàn tất — dịch vụ đã hoạt động', ['ip' => $moduleVmware->ip]);
|
|
|
|
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}"
|
|
);
|
|
}
|
|
}
|