- 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>
193 lines
7.1 KiB
PHP
193 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs\VirtualMachine\Provision\Concerns;
|
|
|
|
use App\Models\IpPool;
|
|
use App\Models\ModuleIpAddress;
|
|
use App\Models\IpPoolLog;
|
|
use App\Models\ModuleVmware;
|
|
use App\Models\Service;
|
|
use App\Models\VirtualMachines;
|
|
use App\Services\VirtualMachine\VmProvisionLogger;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
trait ManagesIpReservation
|
|
{
|
|
/**
|
|
* Reserve a free IP from the pool inside a DB transaction with row-level locks.
|
|
*
|
|
* Strategy:
|
|
* 1. Lock all existing ModuleIpAddress rows for the network → prevents double-pick
|
|
* 2. Lock the chosen IpPool row → prevents concurrent reservations
|
|
* 3. Create a pending ModuleIpAddress (status=0) to hold the slot
|
|
*
|
|
* Returns the pending ModuleIpAddress record, or null if pool is exhausted.
|
|
*/
|
|
/**
|
|
* @return array{reservation:ModuleIpAddress,ip_pool_id:int,gateway:string,cidr:int}|null
|
|
*/
|
|
private function reserveIp(
|
|
int $networkId,
|
|
int $hostId,
|
|
string $dns,
|
|
): ?array {
|
|
return DB::transaction(function () use ($networkId, $hostId, $dns) {
|
|
// ip_pools.status is the single source of truth for availability.
|
|
$ipPool = IpPool::where('module_network_id', $networkId)
|
|
->whereIn('status', ['free', 'reserved'])
|
|
->lockForUpdate()
|
|
->inRandomOrder()
|
|
->first();
|
|
|
|
if (!$ipPool) {
|
|
return null;
|
|
}
|
|
|
|
// Hold the pool slot
|
|
$ipPool->update(['status' => 'reserved']);
|
|
|
|
$cidr = (int) ($ipPool->cidr ?: 24);
|
|
$gateway = (string) ($ipPool->gateway ?? '');
|
|
$netmask = $ipPool->netmask ?: $this->cidrToNetmask($cidr);
|
|
|
|
// module_ip_address mirrors ip_pools — update the existing synced row
|
|
// (created when the IP was added to ip_pools) instead of inserting a new one.
|
|
$reservation = ModuleIpAddress::where('ip', $ipPool->ip_address)
|
|
->where('module_network_id', $networkId)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
$attrs = [
|
|
'vm_name' => '__pending__',
|
|
'module_host_id' => $hostId,
|
|
'module_network_id' => $networkId,
|
|
'ip' => $ipPool->ip_address,
|
|
'gateway' => $gateway,
|
|
'netmask' => $netmask,
|
|
'dns' => $dns,
|
|
'status' => 0, // 0 = pending/reserved, not yet confirmed
|
|
];
|
|
|
|
if ($reservation) {
|
|
$reservation->update($attrs);
|
|
} else {
|
|
$reservation = ModuleIpAddress::create($attrs);
|
|
}
|
|
|
|
return [
|
|
'reservation' => $reservation,
|
|
'ip_pool_id' => $ipPool->id,
|
|
'gateway' => $gateway,
|
|
'cidr' => $cidr,
|
|
];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load a reservation that was created upstream (e.g. by CreateVirtualMachineJob).
|
|
* Returns the same shape as reserveIp() so the rest of the flow is identical.
|
|
*
|
|
* @return array{reservation:ModuleIpAddress,ip_pool_id:int,gateway:string,cidr:int}|null
|
|
*/
|
|
private function loadReservation(int $reservationId, ?int $ipPoolId, VmProvisionLogger $logger): ?array
|
|
{
|
|
$reservation = ModuleIpAddress::find($reservationId);
|
|
if (!$reservation) {
|
|
$logger->error('upstream reservation missing', [
|
|
'reservation_id' => $reservationId,
|
|
]);
|
|
return null;
|
|
}
|
|
|
|
$ipPool = $ipPoolId ? IpPool::find($ipPoolId) : null;
|
|
$cidr = (int) ($ipPool->cidr ?? 24);
|
|
$gateway = (string) ($reservation->gateway ?: ($ipPool->gateway ?? ''));
|
|
|
|
return [
|
|
'reservation' => $reservation,
|
|
'ip_pool_id' => $ipPoolId ?? 0,
|
|
'gateway' => $gateway,
|
|
'cidr' => $cidr ?: 24,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Release a failed reservation by resetting the mirrored ModuleIpAddress row.
|
|
* This returns the IP to the available pool.
|
|
*/
|
|
private function releaseIp(ModuleIpAddress $reservation, ?int $ipPoolId, VmProvisionLogger $logger): void
|
|
{
|
|
try {
|
|
$reservation->update(['vm_name' => null, 'status' => 0]);
|
|
if ($ipPoolId) {
|
|
IpPool::where('id', $ipPoolId)->update(['status' => 'free']);
|
|
}
|
|
$logger->step('IP reservation released', ['ip' => $reservation->ip]);
|
|
} catch (\Throwable $e) {
|
|
$logger->warning('could not release IP reservation', [
|
|
'ip' => $reservation->ip,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Confirm a successful IP assignment:
|
|
* - Update ModuleIpAddress: status → 1 (assigned), vm_name → real name
|
|
* - Update ModuleVmware: ip, dns, module_ip_address FK
|
|
* - Update VirtualMachines: ip, ip_pool_id FK (matched by vmId)
|
|
*
|
|
* IP is only written to DB AFTER guest-side verification passes.
|
|
*/
|
|
private function persistIp(
|
|
ModuleIpAddress $reservation,
|
|
ModuleVmware $moduleVmware,
|
|
?int $ipPoolId,
|
|
?int $serviceId,
|
|
): void {
|
|
DB::transaction(function () use ($reservation, $moduleVmware, $ipPoolId, $serviceId) {
|
|
$reservation->update([
|
|
'vm_name' => $moduleVmware->name ?? ('VM-' . $this->vmId),
|
|
'status' => 1, // assigned
|
|
]);
|
|
|
|
$moduleVmware->update([
|
|
'ip' => $reservation->ip,
|
|
'dns' => $reservation->dns,
|
|
'module_ip_address' => $reservation->id,
|
|
]);
|
|
|
|
// Sync to virtual_machines — matched by vSphere vmId
|
|
VirtualMachines::where('vm_id', $this->vmId)->update([
|
|
'ip' => $reservation->ip,
|
|
'ip_pool_id' => $ipPoolId,
|
|
]);
|
|
|
|
// Flip ip_pools row to assigned and link service
|
|
if ($ipPoolId) {
|
|
$statusFrom = (string) IpPool::whereKey($ipPoolId)->value('status');
|
|
IpPool::where('id', $ipPoolId)->update([
|
|
'status' => 'assigned',
|
|
'service_id' => $serviceId,
|
|
]);
|
|
IpPoolLog::recordVm($ipPoolId, IpPoolLog::VM_ASSIGNED, $statusFrom, $moduleVmware, 'Tạo VM');
|
|
}
|
|
|
|
// Touch services row so server_name reflects the provisioned VM.
|
|
// Note: services.vm_id is an FK to module_vmware.id (already set by VmServiceSyncService) — do not overwrite with vSphere vmId string.
|
|
if ($serviceId) {
|
|
Service::where('id', $serviceId)->update([
|
|
'server_name' => $moduleVmware->name,
|
|
]);
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Convert CIDR prefix length to dotted-decimal netmask (e.g. 24 → 255.255.255.0). */
|
|
private function cidrToNetmask(int $cidr): string
|
|
{
|
|
$cidr = max(0, min(32, $cidr));
|
|
return long2ip(-1 << (32 - $cidr));
|
|
}
|
|
}
|