227 lines
8.2 KiB
PHP
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_DATACENTER => [ModuleDatacenter::class, 'vcenter_id', [self::TYPE_HOST]],
|
|
self::TYPE_HOST => [ModuleHost::class, 'module_datacenter_id', [self::TYPE_DATASTORE, self::TYPE_NETWORK]],
|
|
self::TYPE_DATASTORE => [ModuleDatastore::class, 'module_host_id', []],
|
|
self::TYPE_NETWORK => [ModuleNetwork::class, 'module_host_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 $hostId): ?ModuleDatastore
|
|
{
|
|
return ModuleDatastore::where('module_host_id', $hostId)
|
|
->where('status', 1)
|
|
->first();
|
|
}
|
|
|
|
public function activeNetwork(int $hostId): ?ModuleNetwork
|
|
{
|
|
return ModuleNetwork::where('module_host_id', $hostId)
|
|
->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($host->id);
|
|
if (!$datastore) {
|
|
throw new RuntimeException("Host active id={$host->id} chưa có Datastore nào active.");
|
|
}
|
|
|
|
$network = $this->activeNetwork($host->id);
|
|
if (!$network) {
|
|
throw new RuntimeException("Host active id={$host->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];
|
|
}
|
|
}
|