Files
portal-api/app/Services/VirtualMachine/VmActiveProviderService.php
baodev0 5d3ed57b32 feat: Enhance vCenter integration with resource pool and folder support
- Added methods to ResourceService for fetching resource pools and updated existing methods to include datacenter filtering.
- Modified CreateVirtualMachine to accept folder and resource pool parameters from product data.
- Updated various jobs to synchronize resource pools and include them in the provisioning process.
- Introduced new ModuleResourcePool model and migration for resource pool management.
- Enhanced existing models (ModuleDatastore, ModuleNetwork, Os) to include relationships with vCenter and resource pools.
- Added netmask field to IpPool and updated related logic for IP reservation.
- Improved error handling and logging in VM creation and IP reservation processes.
- Updated API routes to include sync functionality for vCenter.
2026-07-21 21:37:28 +07:00

227 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\VirtualMachine;
use App\Models\ModuleDatacenter;
use App\Models\ModuleDatastore;
use App\Models\ModuleHost;
use App\Models\ModuleNetwork;
use App\Models\vCenter;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use RuntimeException;
/**
* Single source of truth for the "active" provisioning chain:
*
* vCenter (status=1, global)
* └── ModuleDatacenter (status=1 per vCenter)
* └── ModuleHost (status=1 per datacenter)
* ├── ModuleDatastore (status=1 per host)
* └── ModuleNetwork (status=1 per host)
*
* Rules enforced here:
* - At most ONE row per parent scope may have status=1.
* - Setting status=1 on row X auto-deactivates all siblings in same parent.
* - Setting status=0 on row X cascade-deactivates ALL descendants below it.
* - resolveChain() returns the full active chain or throws if any layer is missing.
*
* Settings provider mirroring:
* - activate('vcenter', $id) also writes provider.active_vcenter_id into the
* setting table so the Settings → Provider page reflects the same value
* without owning the truth.
*/
class VmActiveProviderService
{
public const TYPE_VCENTER = 'vcenter';
public const TYPE_DATACENTER = 'datacenter';
public const TYPE_HOST = 'host';
public const TYPE_DATASTORE = 'datastore';
public const TYPE_NETWORK = 'network';
/** type → [modelClass, parentColumn|null (null = global), childTypes[]] */
private const MAP = [
self::TYPE_VCENTER => [vCenter::class, null, [self::TYPE_DATACENTER, self::TYPE_DATASTORE, self::TYPE_NETWORK]],
self::TYPE_DATACENTER => [ModuleDatacenter::class, 'vcenter_id', [self::TYPE_HOST]],
self::TYPE_HOST => [ModuleHost::class, 'module_datacenter_id', []],
self::TYPE_DATASTORE => [ModuleDatastore::class, 'vcenter_id', []],
self::TYPE_NETWORK => [ModuleNetwork::class, 'vcenter_id', []],
];
public function __construct(private readonly VmProviderSettingsService $providerSettings)
{
}
// ─────────────────────────────────────────────────────────────────────────
// Resolve helpers
// ─────────────────────────────────────────────────────────────────────────
public function activeVcenter(): ?vCenter
{
return vCenter::where('status', 1)->first();
}
public function activeDatacenter(int $vcenterId): ?ModuleDatacenter
{
return ModuleDatacenter::where('vcenter_id', $vcenterId)
->where('status', 1)
->first();
}
public function activeHost(int $datacenterId): ?ModuleHost
{
return ModuleHost::where('module_datacenter_id', $datacenterId)
->where('status', 1)
->first();
}
public function activeDatastore(int $vcenterId): ?ModuleDatastore
{
return ModuleDatastore::where('vcenter_id', $vcenterId)
->where('status', 1)
->first();
}
public function activeNetwork(int $vcenterId): ?ModuleNetwork
{
return ModuleNetwork::where('vcenter_id', $vcenterId)
->where('status', 1)
->first();
}
/**
* Return the full active chain. Throws if any layer has no active row.
*
* @return array{vcenter:vCenter,datacenter:ModuleDatacenter,host:ModuleHost,datastore:ModuleDatastore,network:ModuleNetwork}
*/
public function resolveChain(): array
{
$vcenter = $this->activeVcenter();
if (!$vcenter) {
throw new RuntimeException('Không có vCenter nào đang active. Vui lòng kích hoạt 1 vCenter trong admin.');
}
$datacenter = $this->activeDatacenter($vcenter->id);
if (!$datacenter) {
throw new RuntimeException("vCenter active id={$vcenter->id} chưa có Datacenter nào active.");
}
$host = $this->activeHost($datacenter->id);
if (!$host) {
throw new RuntimeException("Datacenter active id={$datacenter->id} chưa có Host nào active.");
}
$datastore = $this->activeDatastore($vcenter->id);
if (!$datastore) {
throw new RuntimeException("vCenter active id={$vcenter->id} chưa có Datastore nào active.");
}
$network = $this->activeNetwork($vcenter->id);
if (!$network) {
throw new RuntimeException("vCenter active id={$vcenter->id} chưa có Network nào active.");
}
return compact('vcenter', 'datacenter', 'host', 'datastore', 'network');
}
// ─────────────────────────────────────────────────────────────────────────
// Mutators
// ─────────────────────────────────────────────────────────────────────────
/**
* Set this row active, deactivating all siblings in the same parent scope.
*/
public function activate(string $type, int $id): Model
{
[$modelClass, $parentColumn] = $this->mapOrFail($type);
return DB::transaction(function () use ($modelClass, $parentColumn, $type, $id) {
/** @var Model $row */
$row = $modelClass::query()->lockForUpdate()->findOrFail($id);
// Deactivate siblings under same parent (or all rows if global scope)
$siblings = $modelClass::query()->where('id', '!=', $row->getKey());
if ($parentColumn) {
$siblings->where($parentColumn, $row->{$parentColumn});
}
$siblings->update(['status' => 0]);
$row->status = 1;
$row->save();
if ($type === self::TYPE_VCENTER) {
$this->providerSettings->setActiveVcenterId((int) $row->getKey());
}
return $row->refresh();
});
}
/**
* Set this row inactive AND cascade-deactivate every descendant beneath it.
*/
public function deactivate(string $type, int $id): Model
{
[$modelClass] = $this->mapOrFail($type);
return DB::transaction(function () use ($modelClass, $type, $id) {
/** @var Model $row */
$row = $modelClass::query()->lockForUpdate()->findOrFail($id);
$row->status = 0;
$row->save();
$this->cascadeDeactivate($type, (int) $row->getKey());
if ($type === self::TYPE_VCENTER) {
$this->providerSettings->setActiveVcenterId(null);
}
return $row->refresh();
});
}
/**
* Recursively set status=0 on every descendant of $parentId under $type.
*/
private function cascadeDeactivate(string $type, int $parentId): void
{
$childTypes = self::MAP[$type][2] ?? [];
foreach ($childTypes as $childType) {
[$childModel, $childParentColumn] = $this->mapOrFail($childType);
if (!$childParentColumn) {
continue;
}
$childIds = $childModel::query()
->where($childParentColumn, $parentId)
->pluck('id')
->all();
if (!$childIds) {
continue;
}
$childModel::query()->whereIn('id', $childIds)->update(['status' => 0]);
foreach ($childIds as $childId) {
$this->cascadeDeactivate($childType, (int) $childId);
}
}
}
/**
* @return array{0:class-string<Model>,1:?string,2:string[]}
*/
private function mapOrFail(string $type): array
{
if (!isset(self::MAP[$type])) {
throw new RuntimeException("Unknown infra type: {$type}");
}
return self::MAP[$type];
}
}