- IP pool: sheet import (xlsx/csv) matched by address, bulk allocate/revoke with notification email (ip_allocated/ip_revoked), ip_pool_logs history. - Orders/invoices: Excel export on branded template (BrandedReportWriter), shared billing filters, order item service info in list responses. - Web design quote Excel export; activity log export. - Service attachments with copy-to-invoice; deployment invoice attachments. - Customer email attachments stored on disk and attached by path. - Admin password change endpoint; user.fullname backfill migration; orders.user_id backfill from logs. - Disable deploy workflow until CI/CD is set up. - Ignore stored customer email and service attachment uploads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.4 KiB
PHP
150 lines
5.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Jobs\VirtualMachine;
|
||
|
||
use App\Actions\VM\ChangeIpVMAction;
|
||
use App\Http\Services\Telegram;
|
||
use App\Models\IpPoolLog;
|
||
use App\Models\ModuleVmware;
|
||
use App\Services\VirtualMachine\VmIpReservationService;
|
||
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;
|
||
|
||
/**
|
||
* ChangeIpVMJob
|
||
*
|
||
* Customer-initiated VM IP change. Runs the same ChangeIpVMAction that used
|
||
* to execute synchronously inside VmController::changeIp — moved to a queued
|
||
* job so the SSH → PowerCLI round trip can no longer blow past nginx/php-fpm's
|
||
* timeout and surface as a 502.
|
||
*/
|
||
class ChangeIpVMJob implements ShouldQueue
|
||
{
|
||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||
|
||
/** No auto-retry — mirrors the previous synchronous behaviour (single attempt). */
|
||
public int $tries = 1;
|
||
public int $timeout = 180;
|
||
|
||
public function __construct(
|
||
public readonly int $moduleVmwareId,
|
||
public readonly string $ipAddress,
|
||
public readonly string $gateway,
|
||
public readonly string $osType,
|
||
public readonly string $dns,
|
||
public readonly string $guestUser,
|
||
public readonly string $guestPassword,
|
||
public readonly ?int $newIpPoolId = null,
|
||
public readonly ?int $oldIpPoolId = null,
|
||
) {}
|
||
|
||
public function handle(ChangeIpVMAction $action, VmIpReservationService $ipReservation): void
|
||
{
|
||
$vm = ModuleVmware::with([
|
||
'vCenter',
|
||
'ipAddress.host.datacenter.vCenter',
|
||
])->find($this->moduleVmwareId);
|
||
|
||
if (!$vm) {
|
||
Log::error('[ChangeIpVMJob] ModuleVmware not found', ['module_vmware_id' => $this->moduleVmwareId]);
|
||
if ($this->newIpPoolId) {
|
||
$ipReservation->releaseByPoolId($this->newIpPoolId);
|
||
}
|
||
return;
|
||
}
|
||
|
||
$raw = $action->execute(
|
||
$vm,
|
||
$this->ipAddress,
|
||
$this->gateway,
|
||
$this->osType,
|
||
$this->dns,
|
||
$this->guestUser,
|
||
$this->guestPassword,
|
||
);
|
||
|
||
$failed = $raw === 'Login_Again'
|
||
|| $raw === 'vCenter_Not_Found'
|
||
|| str_starts_with((string) $raw, 'Error:')
|
||
|| str_contains((string) $raw, '[STDERR]');
|
||
|
||
if ($failed) {
|
||
if ($this->newIpPoolId) {
|
||
$ipReservation->releaseByPoolId($this->newIpPoolId);
|
||
}
|
||
} else {
|
||
$vm->update(['ip' => $this->ipAddress, 'dns' => $this->dns]);
|
||
IpPoolLog::recordVm($this->newIpPoolId, IpPoolLog::VM_ASSIGNED, 'free', $vm, 'Đổi IP VM');
|
||
if ($this->oldIpPoolId) {
|
||
$ipReservation->releaseByPoolId($this->oldIpPoolId);
|
||
IpPoolLog::recordVm($this->oldIpPoolId, IpPoolLog::VM_RELEASED, 'assigned', $vm, 'Đổi IP VM sang ' . $this->ipAddress);
|
||
}
|
||
}
|
||
|
||
Log::channel('Custom')->info('[ChangeIpVMJob] raw output', [
|
||
'module_vmware_id' => $this->moduleVmwareId,
|
||
'ip_address' => $this->ipAddress,
|
||
'gateway' => $this->gateway,
|
||
'os_type' => $this->osType,
|
||
'vm_name' => $vm->name,
|
||
'vmware_id' => $vm->vmware_id,
|
||
'raw' => $raw,
|
||
]);
|
||
|
||
$telegram = new Telegram();
|
||
|
||
if ($raw === 'Login_Again' || $raw === 'vCenter_Not_Found') {
|
||
$errMsg = $raw === 'vCenter_Not_Found'
|
||
? 'Không tìm thấy vCenter cho VM này.'
|
||
: 'Không thể đăng nhập vào vCenter.';
|
||
|
||
$telegram->SendErrorMessage("❌ Đổi IP thất bại [{$this->osType}]\n"
|
||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||
. "📍 IP mới: {$this->ipAddress}\n"
|
||
. "🔗 GW: {$this->gateway}\n"
|
||
. "🚫 Lỗi: {$errMsg}");
|
||
|
||
return;
|
||
}
|
||
|
||
if ($failed) {
|
||
$telegram->SendErrorMessage("❌ Đổi IP thất bại [{$this->osType}]\n"
|
||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||
. "📍 IP mới: {$this->ipAddress}\n"
|
||
. "🔗 GW: {$this->gateway}\n"
|
||
. "📋 Output:\n" . mb_substr((string) $raw, 0, 800));
|
||
|
||
return;
|
||
}
|
||
|
||
// Empty raw = script chạy không có output → coi là thành công (PowerShell script không echo)
|
||
// Nhưng vẫn gửi cảnh báo để theo dõi
|
||
if (trim((string) $raw) === '') {
|
||
$telegram->SendMessage("⚠️ Đổi IP [{$this->osType}] - Script chạy nhưng không có output\n"
|
||
. "🖥 VM: {$vm->name} ({$vm->vmware_id})\n"
|
||
. "📍 IP mới: {$this->ipAddress}\n"
|
||
. "🔗 GW: {$this->gateway}\n"
|
||
. "ℹ️ Kiểm tra lại VM để xác nhận IP đã thay đổi chưa.");
|
||
}
|
||
}
|
||
|
||
public function failed(\Throwable $e): void
|
||
{
|
||
Log::error('[ChangeIpVMJob] job failed with exception', [
|
||
'module_vmware_id' => $this->moduleVmwareId,
|
||
'ip_address' => $this->ipAddress,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
|
||
if ($this->newIpPoolId) {
|
||
app(VmIpReservationService::class)->releaseByPoolId($this->newIpPoolId);
|
||
}
|
||
}
|
||
}
|