This commit is contained in:
pbao9
2026-02-10 16:49:59 +07:00
parent 057353a2e1
commit 3ece85430c
6 changed files with 254 additions and 11514 deletions
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Services\vCenter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class VpsActionController extends Controller
{
protected $vCenterService;
public function __construct(vCenter $vCenterService)
{
$this->vCenterService = $vCenterService;
}
public function rebuildVps(Request $request)
{
set_time_limit(300); // Increase timeout to 5 minutes for slow vCenter connection
$request->validate([
'vm_name' => 'required|string',
'vcenter_id' => 'required|integer' // We need to know which vCenter to connect to
]);
$vmName = $request->input('vm_name');
$vcenterId = $request->input('vcenter_id');
// Retrieve vCenter details from DB to authenticate
$vcenterModel = \App\Models\vCenter::find($vcenterId);
if (!$vcenterModel) {
return response()->json([
'error' => true,
'message' => 'vCenter ID not found'
], 404);
}
// Authenticate with vCenter
$token = $this->vCenterService->loginServer($vcenterModel);
if (!$token) {
return response()->json([
'error' => true,
'message' => 'Failed to login to vCenter'
], 401);
}
Log::info("API Rebuild VPS requested for VM: {$vmName} on vCenter: {$vcenterModel->host}");
// Call the service function
$result = $this->vCenterService->reBuildVPS($vmName);
if (is_array($result) && isset($result['error']) && $result['error']) {
return response()->json($result, 500);
}
if ($result === "Login_Again") { // Double check handled in service but good to catch
return response()->json([
'error' => true,
'message' => 'Session expired, login again'
], 401);
}
return response()->json([
'error' => false,
'message' => 'Rebuild command executed',
'data' => $result
]);
}
}
+125 -10
View File
@@ -319,16 +319,29 @@ class vCenter
return "Login_Fail";
}
try {
//Action connect to viserver
$action = "powershell.exe; Connect-viserver -Server '$this->apiUri' -User '$this->username' -Password \"'$this->password'\" -SaveCredentials; $cmd";
$psCommand = <<<PS
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "
Import-Module VMware.PowerCLI;
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:\$false;
\$cred = Import-Clixml -Path 'C:\secure\vcenter.cred';
Connect-VIServer -Server '{$this->apiUri}' -Credential \$cred;
$cmd;
Disconnect-VIServer -Confirm:\$false
"
PS;
//Run action
return $auth->exec($action);
} catch (Exception $error) {
return $error->getCode();
$out = $auth->exec($psCommand);
$err = $auth->getStdError();
return $out . ($err ? "\n[STDERR]\n" . $err : '');
} catch (\Exception $e) {
return $e->getMessage();
}
}
private function LoginAuth()
{
@@ -2090,7 +2103,8 @@ PS;
// Kiểm tra hệ điều hành
if (str_contains($this->os, "Windows")) {
$user = $data['user'] ?? 'administartor';
$this->process = new WinRMClient($ip, $user, $password);;
$this->process = new WinRMClient($ip, $user, $password);
;
return $this->process;
}
@@ -2718,8 +2732,8 @@ PS;
/*
* Get Window Remote Port
*/
* Get Window Remote Port
*/
public function getWindowsRdpPort(string $vmwareId, string $guestUser, string $guestPassword): ?int
{
$ps = "(Get-ItemProperty 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -Name PortNumber).PortNumber";
@@ -2742,4 +2756,105 @@ PS;
return null;
}
public function reBuildVPS(string $vmName): array|string
{
if (!$this->checkApiExits()) {
return "Login_Again";
}
try {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://{$this->apiUri}/rest/vcenter/vm", [
'filter.names' => $vmName
]);
if (!$response->successful()) {
return [
'error' => true,
'message' => 'Failed to search for VM',
'details' => $response->json()
];
}
$vmList = $response->json()['value'] ?? [];
if (empty($vmList)) {
return [
'error' => true,
'message' => "VM not found with name: {$vmName}"
];
}
$vmId = $vmList[0]['vm'];
$detailResponse = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->get("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}");
if (!$detailResponse->successful()) {
return [
'error' => true,
'message' => 'Failed to fetch VM details',
'details' => $detailResponse->json()
];
}
$vmData = $detailResponse->json();
$guestOS = $vmData['value']['guest_OS'] ?? 'UNKNOWN';
$scriptPath = '';
$osUpper = strtoupper($guestOS);
if (Str::contains($osUpper, 'WINDOWS')) {
$scriptPath = 'C:\\CLISave\\Windows\\rebuild.ps1';
} elseif (Str::contains($osUpper, ['UBUNTU', 'LINUX', 'CENTOS', 'DEBIAN'])) {
$scriptPath = 'C:\\CLISave\\Ubuntu\\rebuild.ps1';
} else {
return [
'error' => true,
'message' => "Unsupported OS for rebuild: {$guestOS}"
];
}
$scriptDir = dirname($scriptPath);
$scriptFile = basename($scriptPath);
$cmd = "
Write-Output 'STARTING_REBUILD';
\$vm = Get-VM -Name '$vmName';
cd '$scriptDir';
if (Test-Path '.\\$scriptFile') {
Write-Output 'File found, executing...';
& '.\\$scriptFile' | Out-String
} else {
Write-Error 'File NOT FOUND: $scriptPath';
}
Write-Output 'ENDING_REBUILD';
";
$result = $this->Command($cmd);
return [
'error' => false,
'vm_name' => $vmName,
'vm_id' => $vmId,
'os' => $guestOS,
'script' => $scriptPath,
'result' => $result
];
} catch (\Exception $e) {
Log::error("reBuildVPS Exception: " . $e->getMessage());
return [
'error' => true,
'message' => $e->getMessage()
];
}
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ class RouteServiceProvider extends ServiceProvider
$this->configureRateLimiting();
$this->routes(function () {
Route::prefix('api')
Route::domain('api-agency.tgs.com.vn')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
-11439
View File
File diff suppressed because it is too large Load Diff
@@ -1,86 +1,68 @@
<div x-data="{ open: false }" class="relative">
<!-- Toggle button for mobile (sm and below) -->
<button
@click="open = true"
<button @click="open = true"
class="md:hidden fixed top-4 left-4 z-50 flex items-center px-3 py-2 bg-primary text-white rounded shadow-lg focus:outline-none"
type="button"
aria-label="{{ __('Open menu') }}">
type="button" aria-label="{{ __('Open menu') }}">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
<!-- Sidebar: always visible on md+, offcanvas on sm -->
<aside
id="drawer-navigation"
aria-label="Sidenav"
aria-hidden="true"
:class="{
'fixed inset-0 z-40 flex md:hidden bg-black/40': open,
'hidden md:block': !open,
'md:static md:inset-auto md:z-auto md:flex md:bg-transparent': true
}"
x-show="open || window.innerWidth >= 768"
@keydown.window.escape="open = false"
@click.self="open = false"
x-cloak
>
<div class="flex flex-col h-full overflow-y-auto py-5 pl-3 pr-1 scrollbar-hide bg-white/90 md:bg-transparent md:shadow-none w-64 md:w-auto">
<!-- Close button for mobile -->
<button
@click="open = false"
<aside id="drawer-navigation" aria-label="Sidenav" aria-hidden="true" :class="{
'fixed inset-0 z-40 flex md:hidden bg-black/40': open,
'hidden md:block': !open,
'md:static md:inset-auto md:z-auto md:flex md:bg-transparent': true
}" x-show="open || window.innerWidth >= 768" @keydown.window.escape="open = false"
@click.self="open = false" x-cloak>
<div
class="flex flex-col h-full overflow-y-auto py-5 pl-3 pr-1 scrollbar-hide bg-white/90 md:bg-transparent md:shadow-none w-64 md:w-auto">
<button @click="open = false"
class="md:hidden mb-4 ml-auto flex items-center px-2 py-1 text-gray-700 hover:text-primary focus:outline-none"
aria-label="{{ __('Close menu') }}">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
<ul x-data="{ visibleItems: [], showAll: true }" x-init="if (showAll) {
let total = {{ count($menu) }};
for (let i = 0; i < total; i++) {
setTimeout(() => visibleItems.push(i), i * 100);
}
}" class="space-y-2">
let total = {{ count($menu) }};
for (let i = 0; i < total; i++) {
setTimeout(() => visibleItems.push(i), i * 100);
}
}" class="space-y-2">
@foreach ($menu as $index => $item)
@php
$isActive = request()->routeIs($item['route_name']);
@endphp
<li x-show="visibleItems.includes({{ $index }})"
x-transition:enter="transition ease-out duration-300"
<li x-show="visibleItems.includes({{ $index }})" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 -translate-x-5"
x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 -translate-x-5" x-cloak
x-data="{ open: {{ $isActive ? 'true' : 'false' }} }"
wire:key="menu-item-{{ $index }}">
x-data="{ open: {{ $isActive ? 'true' : 'false' }} }" wire:key="menu-item-{{ $index }}">
{{-- Nếu có submenu --}}
@if (count($item['children'] ?? []) > 0)
<button @click="open = !open" type="button"
class="relative flex items-center pl-3 py-3 pr-2 text-base font-medium text-white rounded-lg overflow-hidden w-full
hover:bg-white/30 dark:text-white dark:hover:bg-gray-700 cursor-pointer
{{ $isActive ? 'bg-primary-light/80 shadow-lg before:content-[\'\'] before:absolute before:inset-0 before:rounded-lg before:border-b-[3px] before:border-solid before:border-black/10 before:bg-white/10' : '' }}">
class="relative flex items-center pl-3 py-3 pr-2 text-base font-medium text-white rounded-lg overflow-hidden w-full
hover:bg-white/30 dark:text-white dark:hover:bg-gray-700 cursor-pointer
{{ $isActive ? 'bg-primary-light/80 shadow-lg before:content-[\'\'] before:absolute before:inset-0 before:rounded-lg before:border-b-[3px] before:border-solid before:border-black/10 before:bg-white/10' : '' }}">
<span class="relative z-10 flex-shrink-0 w-6 h-6 text-white dark:text-gray-400">
<i data-lucide="{{ $item['icon'] }}"></i>
</span>
<span class="relative z-10 ml-3 text-white text-[.875rem]">{{ __($item['title']) }}</span>
<i data-lucide="chevron-down"
class="w-4 h-4 ml-auto transition-transform duration-300 transform"
:class="{ 'rotate-180': open }"></i>
class="w-4 h-4 ml-auto transition-transform duration-300 transform"
:class="{ 'rotate-180': open }"></i>
</button>
{{-- Sub menu items --}}
<ul x-show="open" x-cloak x-data="{ visibleSubItems: [] }" x-init="$watch('open', (val) => {
if (val) {
visibleSubItems = [];
setTimeout(() => {
@foreach($item['children'] as $i => $subItem)
setTimeout(() => visibleSubItems.push({{ $i }}), {{ $i * 80 }});
@endforeach
}, 50);
} else {
visibleSubItems = [];
}
})"
class="pl-6 mt-1 space-y-1 bg-white/10 rounded-lg">
if (val) {
visibleSubItems = [];
setTimeout(() => {
@foreach($item['children'] as $i => $subItem)
setTimeout(() => visibleSubItems.push({{ $i }}), {{ $i * 80 }});
@endforeach
}, 50);
} else {
visibleSubItems = [];
}
})" class="pl-6 mt-1 space-y-1 bg-white/10 rounded-lg">
@foreach ($item['children'] as $i => $subItem)
@if (empty($subItem['roles']) || auth()->guard('admin')->user()?->rolesIn($subItem['roles']))
<li x-show="visibleSubItems.includes({{ $i }})"
@@ -91,10 +73,9 @@
x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 -translate-x-4" x-cloak>
<a href="{{ !empty($subItem['route_name']) ? route($subItem['route_name']) : url($subItem['slug'] ?? '#') }}"
class="relative flex items-center p-2 text-sm font-medium text-white/90 rounded-lg overflow-hidden
hover:text-white dark:hover:bg-gray-700 transition-transform transform hover:translate-x-1">
<span
class="relative z-10 flex-shrink-0 w-6 h-6 text-white dark:text-gray-400">
class="relative flex items-center p-2 text-sm font-medium text-white/90 rounded-lg overflow-hidden
hover:text-white dark:hover:bg-gray-700 transition-transform transform hover:translate-x-1">
<span class="relative z-10 flex-shrink-0 w-6 h-6 text-white dark:text-gray-400">
<i data-lucide="{{ $subItem['icon'] }}"></i>
</span>
<span class="relative z-10 ml-3 truncate">{{ __($subItem['title']) }}</span>
@@ -104,14 +85,14 @@
@endforeach
</ul>
@else
{{-- Không có submenu --}}
<a href="{{ !empty($item['route_name']) ? route($item['route_name']) : url($item['slug'] ?? '#') }}"
class="relative flex items-center p-3 text-base font-medium rounded-lg overflow-hidden
cursor-pointer
hover:bg-white/30 dark:text-white dark:hover:bg-gray-700
{{ $isActive ? 'bg-primary-light/80 shadow-lg before:content-[\'\'] before:absolute before:inset-0 before:rounded-lg before:border-b-[3px] before:border-solid before:border-black/10 before:bg-white/10' : 'text-white' }}
">
<span class="relative z-10 flex-shrink-0 w-6 h-6 text-white dark:text-gray-400">
class="relative flex items-center p-3 text-base font-medium rounded-lg overflow-hidden
cursor-pointer
hover:bg-white/30 dark:text-white dark:hover:bg-gray-700
{{ $isActive ? 'bg-primary-light/80 shadow-lg before:content-[\'\'] before:absolute before:inset-0 before:rounded-lg before:border-b-[3px] before:border-solid before:border-black/10 before:bg-white/10' : 'text-white' }}
">
<span class="relative z-10 flex-shrink-0 w-6 h-6 text-white dark:text-gray-400">
<i data-lucide="{{ $item['icon'] }}"></i>
</span>
<span
@@ -123,4 +104,4 @@
</ul>
</div>
</aside>
</div>
</div>
+12
View File
@@ -44,6 +44,7 @@ use Illuminate\Support\Str;
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::get('/', fn() => response()->json(['api' => 'ok']));
Route::prefix('v1')
/* Api for Admin */
@@ -63,6 +64,10 @@ Route::prefix('v1')
Log::info('🧪 API Test SSH Connection - Starting test via route');
return response()->json($serviceVCenter->testSSHConnection());
})->name('api.testSSHConnection');
Route::post('rebuild-vps', [\App\Http\Controllers\Api\VpsActionController::class, 'rebuildVps'])
->name('api.system.rebuildVps');
Route::get('get-list-product', function () {
$type_id = \request()->get('type');
if (!$type_id) {
@@ -1536,3 +1541,10 @@ Route::prefix('v1')
});
});
Route::any('{any}', function () {
return response()->json([
'error' => true,
'message' => 'API Route not found'
], 404);
})->where('any', '.*');