Network dùng chung giữa các host bị unique('network') giữ lại host đầu tiên.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
385 lines
17 KiB
PHP
385 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Admin\VCenter;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Jobs\VirtualMachine\Sync\SyncAllPackage;
|
|
use App\Models\ModuleDatacenter;
|
|
use App\Models\ModuleDatastore;
|
|
use App\Models\ModuleFolder;
|
|
use App\Models\ModuleHost;
|
|
use App\Models\ModuleNetwork;
|
|
use App\Models\ModuleResourcePool;
|
|
use App\Models\vCenter;
|
|
use App\Models\VirtualMachines;
|
|
use App\Http\Services\vCenter as ServiceVCenter;
|
|
use App\Http\Facade\SignRSA;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
|
|
class VCenterController extends Controller
|
|
{
|
|
private const CONSOLE_HOST = 'console.tgs.com.vn';
|
|
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'host' => 'required|string|max:255',
|
|
'user' => 'required|string|max:255',
|
|
'password' => 'required|string|max:255',
|
|
]);
|
|
|
|
$vc = vCenter::create([
|
|
'host' => $request->input('host'),
|
|
'user' => $request->input('user'),
|
|
'password' => SignRSA::EncryptFunc($request->input('password')),
|
|
'status' => 1,
|
|
]);
|
|
|
|
return response()->json(['error' => false, 'data' => $vc], 201);
|
|
}
|
|
|
|
public function consoleTicket(Request $request, ServiceVCenter $serviceVCenter): JsonResponse
|
|
{
|
|
$vmDbId = $request->integer('vm_id');
|
|
if (!$vmDbId) {
|
|
return response()->json(['error' => true, 'message' => 'Thiếu vm_id'], 422);
|
|
}
|
|
|
|
$vm = VirtualMachines::with('datacenter.vCenter')->find($vmDbId);
|
|
if (!$vm) {
|
|
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
|
}
|
|
|
|
$vcenter = $vm->datacenter?->vCenter ?? null;
|
|
if (!$vcenter) {
|
|
return response()->json(['error' => true, 'message' => 'Không tìm thấy thông tin vCenter'], 404);
|
|
}
|
|
|
|
if (!$serviceVCenter->loginServer($vcenter)) {
|
|
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vào vCenter'], 500);
|
|
}
|
|
|
|
$ticket = $serviceVCenter->GetConsoleTickets($vm->vm_id);
|
|
|
|
if ($ticket === 'Login_Again') {
|
|
if ($serviceVCenter->loginServer($vcenter)) {
|
|
$ticket = $serviceVCenter->GetConsoleTickets($vm->vm_id);
|
|
} else {
|
|
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập lại vào vCenter'], 500);
|
|
}
|
|
}
|
|
|
|
if (is_string($ticket)) {
|
|
return response()->json(['error' => true, 'message' => "Lấy ticket thất bại: $ticket"], 500);
|
|
}
|
|
|
|
if ($ticket['error']) {
|
|
return response()->json(['error' => true, 'message' => $ticket['data']], 500);
|
|
}
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => 'wss://' . self::CONSOLE_HOST . '/ticket/' . $ticket['data']['ticket'] . '?host=' . $ticket['data']['host'],
|
|
]);
|
|
}
|
|
|
|
public function sync(): JsonResponse
|
|
{
|
|
dispatch(new SyncAllPackage())->delay(now()->addSeconds(5));
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Sync job dispatched, running in background.',
|
|
]);
|
|
}
|
|
|
|
|
|
public function storeNode(Request $request, string $type): JsonResponse
|
|
{
|
|
$rules = match ($type) {
|
|
'datacenter' => [
|
|
'vcenter_id' => 'required|integer|exists:module_vcenter,id',
|
|
'name' => 'required|string|max:255',
|
|
'datacenter_id' => 'nullable|string|max:255',
|
|
],
|
|
'host' => [
|
|
'module_datacenter_id' => 'required|integer|exists:module_datacenter,id',
|
|
'name' => 'required|string|max:255',
|
|
'host_id' => 'nullable|string|max:255',
|
|
'template' => 'nullable|string|max:500',
|
|
'console' => 'nullable|string|max:500',
|
|
'iso_path' => 'nullable|string|max:500',
|
|
],
|
|
'network' => [
|
|
'module_host_id' => 'required|integer|exists:module_host,id',
|
|
'name' => 'required|string|max:255',
|
|
'network' => 'nullable|string|max:255',
|
|
'type' => 'nullable|string|max:255',
|
|
],
|
|
default => abort(422, 'Không hỗ trợ tạo node loại này'),
|
|
};
|
|
|
|
$data = $request->validate($rules);
|
|
$data['status'] = 1;
|
|
|
|
$model = match ($type) {
|
|
'datacenter' => ModuleDatacenter::create($data),
|
|
'host' => ModuleHost::create($data),
|
|
'network' => ModuleNetwork::create($data),
|
|
};
|
|
|
|
return response()->json(['error' => false, 'data' => $model], 201);
|
|
}
|
|
|
|
public function updateNode(Request $request, string $type, int $id): JsonResponse
|
|
{
|
|
$model = $this->resolveModel($type, $id);
|
|
|
|
$rules = match ($type) {
|
|
'vcenter' => ['host' => 'sometimes|string|max:255', 'user' => 'sometimes|string|max:255', 'password' => 'sometimes|string|max:255', 'status' => 'sometimes|integer|in:0,1'],
|
|
'datacenter' => ['name' => 'sometimes|string|max:255', 'datacenter_id' => 'sometimes|string|max:255'],
|
|
'host' => ['name' => 'sometimes|string|max:255', 'host_id' => 'sometimes|string|max:255', 'template' => 'sometimes|nullable|string|max:500', 'console' => 'sometimes|nullable|string|max:500', 'iso_path' => 'sometimes|nullable|string|max:500'],
|
|
'datastore' => ['name' => 'sometimes|string|max:255', 'datastore' => 'sometimes|string|max:255', 'type' => 'sometimes|nullable|string|max:255', 'space' => 'sometimes|nullable|integer', 'status' => 'sometimes|integer|in:0,1'],
|
|
'network' => ['name' => 'sometimes|string|max:255', 'network' => 'sometimes|string|max:255', 'type' => 'sometimes|nullable|string|max:255'],
|
|
'folder' => ['name' => 'sometimes|string|max:255', 'folder' => 'sometimes|string|max:255', 'type' => 'sometimes|nullable|string|max:255'],
|
|
'resource_pool' => ['name' => 'sometimes|string|max:255', 'resource_pool' => 'sometimes|string|max:255', 'status' => 'sometimes|integer|in:0,1'],
|
|
'virtual_machine' => ['name' => 'sometimes|string|max:255', 'vm_id' => 'sometimes|string|max:255'],
|
|
default => [],
|
|
};
|
|
|
|
$request->validate($rules);
|
|
|
|
$data = $request->only(array_keys($rules));
|
|
|
|
if ($type === 'vcenter' && isset($data['password'])) {
|
|
$data['password'] = SignRSA::EncryptFunc($data['password']);
|
|
}
|
|
|
|
$model->update($data);
|
|
|
|
return response()->json(['error' => false, 'data' => $model]);
|
|
}
|
|
|
|
public function destroyNode(string $type, int $id): JsonResponse
|
|
{
|
|
$model = $this->resolveModel($type, $id);
|
|
|
|
$childCheck = match ($type) {
|
|
'vcenter' => $model->datacenters()->exists(),
|
|
'datacenter' => ModuleHost::where('module_datacenter_id', $id)->exists(),
|
|
'host' => $model->datastore()->exists() || $model->network()->exists() || $model->resourcePool()->exists(),
|
|
default => false,
|
|
};
|
|
|
|
if ($childCheck) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => 'Không thể xoá vì vẫn còn dữ liệu liên kết bên dưới.',
|
|
], 422);
|
|
}
|
|
|
|
$model->delete();
|
|
|
|
return response()->json(['error' => false, 'message' => 'Đã xoá thành công.']);
|
|
}
|
|
|
|
public function tree(Request $request): JsonResponse
|
|
{
|
|
$vcenters = vCenter::with(['datacenters'])->get();
|
|
$allHosts = ModuleHost::with(['datastore', 'network', 'resourcePool'])->get()->groupBy('module_datacenter_id');
|
|
$allFolders = ModuleFolder::all()->groupBy('module_datacenter_id');
|
|
// VMs with a host assignment — grouped by host DB id
|
|
$allVMs = VirtualMachines::with('vmware')->whereNotNull('host_id')->get()->groupBy('host_id');
|
|
// VMs without a host assignment (synced before host_id was tracked) — grouped by datacenter DB id
|
|
$orphanVMs = VirtualMachines::with('vmware')->whereNull('host_id')->get()->groupBy('datacenter_id');
|
|
|
|
$tree = $vcenters->map(function ($vc) use ($allHosts, $allFolders, $allVMs, $orphanVMs) {
|
|
$dcGroups = ($vc->datacenters ?? collect())->groupBy('datacenter_id');
|
|
$datacenters = $dcGroups->map(function ($dcGroup) use ($allHosts, $allFolders, $allVMs, $orphanVMs) {
|
|
$dc = $dcGroup->first();
|
|
$dcIds = $dcGroup->pluck('id');
|
|
$uniqueHosts = $dcIds->flatMap(fn($id) => $allHosts->get($id) ?? collect())
|
|
->unique('host_id')->values();
|
|
|
|
// Networks — deduplicated at datacenter level
|
|
$dcNetworks = $uniqueHosts
|
|
->flatMap(fn($host) => $host->network ?? collect())
|
|
->groupBy('network')
|
|
->map(fn($nets) => $nets->first()->setAttribute('rows_by_host', $nets->pluck('id', 'module_host_id')))
|
|
->map(fn($net) => [
|
|
'id' => 'network-' . $net->id,
|
|
'name' => $net->name,
|
|
'type' => 'network',
|
|
'meta' => [
|
|
'db_id' => $net->id,
|
|
'network' => $net->network,
|
|
'net_type' => $net->type,
|
|
'module_host_id' => $net->module_host_id,
|
|
'rows_by_host' => $net->rows_by_host,
|
|
'status' => $net->status,
|
|
],
|
|
]);
|
|
|
|
// Hosts — VMs placed under their host, then Datastores
|
|
$dcHosts = $uniqueHosts->map(function ($host) use ($allVMs) {
|
|
$hostVMs = ($allVMs->get($host->id) ?? collect())
|
|
->map(fn($vm) => [
|
|
'id' => 'virtual_machine-' . $vm->id,
|
|
'name' => $vm->name,
|
|
'type' => 'virtual_machine',
|
|
'meta' => [
|
|
'db_id' => $vm->id,
|
|
'vmware_db_id' => $vm->vmware?->id,
|
|
'uuid' => $vm->uuid,
|
|
'vm_id' => $vm->vm_id,
|
|
'ip' => $vm->ip,
|
|
'username' => $vm->vmware?->user,
|
|
'password' => $vm->vmware?->plain_password,
|
|
'power_state' => $vm->power_state,
|
|
'folder' => $vm->folder,
|
|
'cpu' => $vm->cpu,
|
|
'ram_mb' => $vm->ram_mb,
|
|
'is_template' => $vm->is_template,
|
|
'is_system' => $vm->is_system,
|
|
'last_synced_at' => $vm->last_synced_at,
|
|
],
|
|
'children' => [],
|
|
]);
|
|
|
|
$datastores = ($host->datastore ?? collect())
|
|
->unique('datastore')
|
|
->map(fn($ds) => [
|
|
'id' => 'datastore-' . $ds->id,
|
|
'name' => $ds->name,
|
|
'type' => 'datastore',
|
|
'meta' => [
|
|
'db_id' => $ds->id,
|
|
'datastore' => $ds->datastore,
|
|
'store_type' => $ds->type,
|
|
'space' => $ds->space,
|
|
'space_gb' => $ds->space ? $ds->space / 1024 : null,
|
|
'status' => $ds->status,
|
|
'module_host_id' => $ds->module_host_id,
|
|
],
|
|
]);
|
|
|
|
$resourcePools = ($host->resourcePool ?? collect())
|
|
->unique('resource_pool')
|
|
->map(fn($rp) => [
|
|
'id' => 'resource_pool-' . $rp->id,
|
|
'name' => $rp->name,
|
|
'type' => 'resource_pool',
|
|
'meta' => [
|
|
'db_id' => $rp->id,
|
|
'resource_pool' => $rp->resource_pool,
|
|
'status' => $rp->status,
|
|
'module_host_id' => $rp->module_host_id,
|
|
],
|
|
]);
|
|
|
|
return [
|
|
'id' => 'host-' . $host->id,
|
|
'name' => $host->name,
|
|
'type' => 'host',
|
|
'meta' => [
|
|
'db_id' => $host->id,
|
|
'host_id' => $host->host_id,
|
|
'template' => $host->template,
|
|
'console' => $host->console,
|
|
'iso_path' => $host->iso_path,
|
|
'module_datacenter_id' => $host->module_datacenter_id,
|
|
'vm_count' => $hostVMs->count(),
|
|
'status' => $host->status,
|
|
],
|
|
// VMs first, then Datastores, then Resource Pools — correct vSphere order
|
|
'children' => array_values($hostVMs->concat($datastores)->concat($resourcePools)->toArray()),
|
|
];
|
|
});
|
|
|
|
// Folders — datacenter level
|
|
$dcFolders = $dcIds
|
|
->flatMap(fn($id) => $allFolders->get($id) ?? collect())
|
|
->unique('folder')
|
|
->map(fn($f) => [
|
|
'id' => 'folder-' . $f->id,
|
|
'name' => $f->name,
|
|
'type' => 'folder',
|
|
'meta' => [
|
|
'db_id' => $f->id,
|
|
'folder' => $f->folder,
|
|
'folder_type' => $f->type,
|
|
'module_datacenter_id' => $f->module_datacenter_id,
|
|
],
|
|
]);
|
|
|
|
// Orphan VMs — no host_id, grouped directly under datacenter
|
|
$dcOrphanVMs = $dcIds
|
|
->flatMap(fn($id) => $orphanVMs->get($id) ?? collect())
|
|
->unique('id')
|
|
->map(fn($vm) => [
|
|
'id' => 'virtual_machine-' . $vm->id,
|
|
'name' => $vm->name,
|
|
'type' => 'virtual_machine',
|
|
'meta' => [
|
|
'db_id' => $vm->id,
|
|
'vmware_db_id' => $vm->vmware?->id,
|
|
'uuid' => $vm->uuid,
|
|
'vm_id' => $vm->vm_id,
|
|
'ip' => $vm->ip,
|
|
'username' => $vm->vmware?->user,
|
|
'password' => $vm->vmware?->plain_password,
|
|
'power_state' => $vm->power_state,
|
|
'folder' => $vm->folder,
|
|
'cpu' => $vm->cpu,
|
|
'ram_mb' => $vm->ram_mb,
|
|
'is_template' => $vm->is_template,
|
|
'is_system' => $vm->is_system,
|
|
'last_synced_at' => $vm->last_synced_at,
|
|
],
|
|
'children' => [],
|
|
]);
|
|
|
|
return [
|
|
'id' => 'datacenter-' . $dc->id,
|
|
'name' => $dc->name,
|
|
'type' => 'datacenter',
|
|
'meta' => [
|
|
'db_id' => $dc->id,
|
|
'datacenter_id' => $dc->datacenter_id,
|
|
'vcenter_id' => $dc->vcenter_id,
|
|
'status' => $dc->status,
|
|
],
|
|
// vSphere order: Hosts → Networks → Folders → Orphan VMs
|
|
'children' => array_values($dcHosts->concat($dcNetworks)->concat($dcFolders)->concat($dcOrphanVMs)->toArray()),
|
|
];
|
|
})->values();
|
|
|
|
return [
|
|
'id' => 'vcenter-' . $vc->id,
|
|
'name' => $vc->host,
|
|
'type' => 'vcenter',
|
|
'meta' => ['db_id' => $vc->id, 'host' => $vc->host, 'status' => $vc->status],
|
|
'children' => array_values($datacenters->toArray()),
|
|
];
|
|
});
|
|
|
|
return response()->json(['error' => false, 'data' => $tree->values()]);
|
|
}
|
|
|
|
private function resolveModel(string $type, int $id)
|
|
{
|
|
return match ($type) {
|
|
'vcenter' => vCenter::findOrFail($id),
|
|
'datacenter' => ModuleDatacenter::findOrFail($id),
|
|
'host' => ModuleHost::findOrFail($id),
|
|
'datastore' => ModuleDatastore::findOrFail($id),
|
|
'network' => ModuleNetwork::findOrFail($id),
|
|
'folder' => ModuleFolder::findOrFail($id),
|
|
'resource_pool' => ModuleResourcePool::findOrFail($id),
|
|
'virtual_machine' => VirtualMachines::findOrFail($id),
|
|
default => abort(404, 'Unknown node type'),
|
|
};
|
|
}
|
|
}
|