feat: implement VM snapshot management, enhance client resource loading, and refactor deployment workflow for production
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
<?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 1: order_items.os_id → os → osGroup → host → datacenter → vcenter
|
||||
* Path 2: invoices → invoice_items → product_id → 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('osGroup.host.datacenter.vCenter')->find($item->os_id);
|
||||
$vcenter = $os?->osGroup?->host?->datacenter?->vCenter;
|
||||
if ($vcenter) {
|
||||
Log::info('resolveVcenter: found via os → osGroup chain', ['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('osGroup.host.datacenter.vCenter')->find($osRecord->os_id);
|
||||
$vcenter = $os?->osGroup?->host?->datacenter?->vCenter;
|
||||
if ($vcenter) {
|
||||
Log::info('resolveVcenter: found via invoice_items → os_product 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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user