refactor: implement centralized cache key management and add utility traits for vCenter SSH operations.

This commit is contained in:
pbao9
2026-05-16 16:52:21 +07:00
parent 06dbd54ecd
commit 8a115c5175
36 changed files with 1694 additions and 517 deletions
BIN
View File
Binary file not shown.
@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Api\Admin\EmailTemplate;
use App\Http\Controllers\Controller;
use App\Models\Template;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class EmailTemplateController extends Controller
{
public function index(): JsonResponse
{
$templates = Template::orderBy('type_id')->orderBy('id')->get();
return response()->json(['error' => false, 'data' => $templates]);
}
public function show(int $id): JsonResponse
{
$template = Template::findOrFail($id);
return response()->json(['error' => false, 'data' => $template]);
}
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'type_id' => 'required|integer|min:1',
'name' => 'nullable|string|max:100',
'subject' => 'nullable|string|max:100',
'message' => 'nullable|string',
'status' => 'boolean',
]);
$template = DB::transaction(fn () => Template::create($data));
return response()->json([
'error' => false,
'message' => 'Thêm email template thành công',
'data' => $template,
], 201);
}
public function update(Request $request, int $id): JsonResponse
{
$template = Template::findOrFail($id);
$data = $request->validate([
'type_id' => 'sometimes|integer|min:1',
'name' => 'sometimes|nullable|string|max:100',
'subject' => 'sometimes|nullable|string|max:100',
'message' => 'sometimes|nullable|string',
'status' => 'sometimes|boolean',
]);
DB::transaction(fn () => $template->update($data));
return response()->json([
'error' => false,
'message' => 'Cập nhật email template thành công',
'data' => $template->fresh(),
]);
}
public function destroy(int $id): JsonResponse
{
$template = Template::findOrFail($id);
DB::transaction(fn () => $template->delete());
return response()->json([
'error' => false,
'message' => 'Đã xoá email template',
]);
}
}
@@ -0,0 +1,161 @@
<?php
namespace App\Http\Controllers\Api\Admin\InfraManagement;
use App\Http\Controllers\Controller;
use App\Http\Facade\SignRSA;
use App\Models\InfraManagement;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class InfraManagementController extends Controller
{
public function index(Request $request): JsonResponse
{
$perPage = min((int) $request->get('per_page', 15), 500);
$filter = $request->get('filter');
$type = $request->get('type');
$status = $request->get('status');
$query = InfraManagement::query()->select([
'id', 'name', 'host', 'username', 'token', 'type', 'status',
'description', 'meta_data', 'created_at', 'updated_at',
]);
if ($filter) {
$query->where(function ($q) use ($filter) {
$q->where('name', 'like', "%{$filter}%")
->orWhere('host', 'like', "%{$filter}%");
});
}
if ($type !== null && $type !== '') {
if (is_array($type)) {
$query->whereIn('type', $type);
} else {
$query->where('type', $type);
}
}
if ($status !== null && $status !== '') {
$query->where('status', filter_var($status, FILTER_VALIDATE_BOOLEAN));
}
$paginator = $query->latest()->paginate($perPage);
return response()->json([
'error' => false,
'data' => [
'items' => $paginator->items(),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
],
]);
}
public function show(int $id): JsonResponse
{
$infra = InfraManagement::select([
'id', 'name', 'host', 'username', 'token', 'type', 'status',
'description', 'meta_data', 'created_at', 'updated_at',
])->find($id);
if (!$infra) {
return response()->json(['error' => true, 'message' => "Không tìm thấy cấu hình #$id"], 404);
}
return response()->json(['error' => false, 'data' => $infra]);
}
public function store(Request $request): JsonResponse
{
$request->validate([
'name' => 'required|string|max:255',
'host' => 'required|string|max:255',
'username' => 'required|string|max:255',
'password' => 'required|string',
'token' => 'nullable|string',
'type' => 'required|string|in:vcenter,esxi,jumphost',
'status' => 'nullable|boolean',
'description' => 'nullable|string',
'meta_data' => 'nullable|array',
]);
DB::beginTransaction();
try {
$data = $request->except(['password']);
$data['password'] = SignRSA::EncryptFunc($request->password);
$data['status'] = $request->boolean('status', true);
$infra = InfraManagement::create($data);
// Return without password
$infra->makeHidden(['password']);
DB::commit();
return response()->json(['error' => false, 'data' => $infra], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
}
public function update(Request $request, int $id): JsonResponse
{
$infra = InfraManagement::find($id);
if (!$infra) {
return response()->json(['error' => true, 'message' => "Không tìm thấy cấu hình #$id"], 404);
}
$request->validate([
'name' => 'required|string|max:255',
'host' => 'required|string|max:255',
'username' => 'required|string|max:255',
'password' => 'nullable|string',
'token' => 'nullable|string',
'type' => 'required|string|in:vcenter,esxi,jumphost',
'status' => 'nullable|boolean',
'description' => 'nullable|string',
'meta_data' => 'nullable|array',
]);
DB::beginTransaction();
try {
$data = $request->except(['password']);
$data['status'] = $request->boolean('status', $infra->status);
if (!empty($request->password)) {
$data['password'] = SignRSA::EncryptFunc($request->password);
}
$infra->update($data);
$infra->refresh();
$infra->makeHidden(['password']);
DB::commit();
return response()->json(['error' => false, 'data' => $infra]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
}
public function destroy(int $id): JsonResponse
{
$infra = InfraManagement::find($id);
if (!$infra) {
return response()->json(['error' => true, 'message' => "Không tìm thấy cấu hình #$id"], 404);
}
$infra->delete();
return response()->json(['error' => false, 'message' => 'Đã xoá cấu hình thành công']);
}
}
@@ -7,6 +7,7 @@ namespace App\Http\Controllers\Api\Admin\Setting;
use App\Http\Controllers\Controller;
use App\Http\Facade\SignRSA;
use App\Models\Setting;
use App\Support\Cache\CacheKey;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
@@ -90,7 +91,7 @@ class SettingController extends Controller
);
// Flush mail config cache so new SMTP settings take effect immediately
Cache::forget('mail_config');
Cache::forget(CacheKey::build('settings', 'mail'));
}
return response()->json([
@@ -102,7 +102,7 @@ class VPSController extends Controller
], 400);
}
$result = $this->vCenterService->restartVM($vps, $vps->vmware_id);
$result = $this->vCenterService->StartVirtualMachine($vps, $vps->vmware_id);
if (is_string($result)) {
return response()->json([
@@ -114,7 +114,7 @@ class VPSController extends Controller
if ($result['error'] ?? true) {
return response()->json([
'error' => true,
'message' => $result['message'] ?? 'Failed to power on VM'
'message' => $result['data'] ?? 'Failed to power on VM'
], 400);
}
@@ -147,7 +147,7 @@ class VPSController extends Controller
], 400);
}
$result = $this->vCenterService->restartVM($vps, $vps->vmware_id);
$result = $this->vCenterService->ShutdownGuestVirtualMachine($vps, $vps->vmware_id);
if (is_string($result)) {
return response()->json([
@@ -159,7 +159,7 @@ class VPSController extends Controller
if ($result['error'] ?? true) {
return response()->json([
'error' => true,
'message' => $result['message'] ?? 'Failed to power off VM'
'message' => $result['data'] ?? 'Failed to power off VM'
], 400);
}
@@ -10,8 +10,11 @@ use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
use App\Http\Controllers\Controller;
use App\Http\Services\Telegram;
use App\Http\Services\vCenter;
use App\Jobs\VirtualMachine\ChangeVmPasswordJob;
use App\Models\ModuleVmware;
use App\Models\Os;
use App\Models\Service;
use App\Services\VmProvisionService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
@@ -304,6 +307,94 @@ class VmController extends Controller
]);
}
// -------------------------------------------------------------------------
// Change Password
// -------------------------------------------------------------------------
/**
* POST /v1/system/vm-change-password
*
* Body: { product, current_password, new_password }
* Dispatches ChangeVmPasswordJob — returns immediately, runs async.
*/
public function changePassword(Request $request): JsonResponse
{
$validated = $request->validate([
'product' => ['required'],
'current_password' => ['required', 'string'],
'new_password' => ['required', 'string', 'min:8'],
]);
$vm = $this->resolveVmById($validated['product'], $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
// Detect OS family from VM's os_id record
$os = $vm->os_id ? Os::with('osGroup')->find($vm->os_id) : null;
$osGroupName = strtolower($os?->osGroup?->name ?? '');
$osFamily = str_contains($osGroupName, 'windows')
? VmProvisionService::OS_WINDOWS
: VmProvisionService::OS_LINUX;
ChangeVmPasswordJob::dispatch(
$vm->id,
$vm->vmware_id,
$osFamily,
$validated['new_password'],
$validated['current_password'],
);
return response()->json([
'success' => true,
'message' => 'Yêu cầu đổi mật khẩu đã được ghi nhận. Hệ thống sẽ xử lý trong vài phút.',
]);
}
// -------------------------------------------------------------------------
// Power State
// -------------------------------------------------------------------------
/**
* GET /v1/system/vm-power-state?product={id|uuid|vmware_id}
*
* Calls vSphere REST API directly: GET /rest/vcenter/vm/{vmware_id}/power
* Returns: { success: true, data: { state: "POWERED_ON"|"POWERED_OFF"|"SUSPENDED", clean_power_off: bool } }
*/
public function powerState(Request $request, vCenter $serviceVCenter): JsonResponse
{
$productId = $request->input('product');
if (!$productId) {
return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422);
}
$vm = $this->resolveVmById($productId, $request);
if ($vm instanceof JsonResponse) {
return $vm;
}
$result = $serviceVCenter->getVmPowerState($vm, $vm->vmware_id);
if ($result === 'Login_Again') {
return response()->json(['success' => false, 'message' => 'Không thể đăng nhập vào vCenter.'], 400);
}
if (is_array($result) && ($result['error'] ?? true)) {
return response()->json([
'success' => false,
'message' => $result['data'] ?? 'Không thể lấy trạng thái VM.',
], 400);
}
return response()->json([
'success' => true,
'data' => [
'state' => $result['data']['state'] ?? null,
'clean_power_off' => $result['data']['clean_power_off'] ?? null,
],
]);
}
// -------------------------------------------------------------------------
// Shared helpers
// -------------------------------------------------------------------------
@@ -9,6 +9,7 @@ use App\Repositories\DnsManager\Domain\DnsDomainRepositoryInterface;
use App\Services\ServerService;
use Exception;
use Illuminate\Support\Carbon;
use App\Support\Cache\CacheKey;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -356,19 +357,19 @@ class DomainController extends Controller
private function clearDomainCaches($domain): void
{
try {
Cache::forget("domain.{$domain->id}");
Cache::forget("domain.{$domain->domain_name}");
Cache::forget('domains.all');
Cache::forget('domains.filtered');
// Clear specific domain keys
Cache::forget(CacheKey::build('domain', 'detail', ['id' => $domain->id]));
Cache::forget(CacheKey::build('domain', 'detail', ['name' => $domain->domain_name]));
Cache::forget(CacheKey::build('domain', 'all'));
if ($domain->users) {
foreach ($domain->users as $user) {
Cache::forget("user.{$user->id}.domains");
Cache::forget(CacheKey::build('domain', 'user-list', ['user_id' => $user->id]));
}
}
if ($domain->dns_manager_server_id) {
Cache::forget("server.{$domain->dns_manager_server_id}.domains");
Cache::forget(CacheKey::build('domain', 'server-list', ['server_id' => $domain->dns_manager_server_id]));
}
} catch (Exception $e) {
Log::warning('Failed to clear domain caches: ' . $e->getMessage());
+8 -11
View File
@@ -10,6 +10,7 @@ use App\Models\Type;
use App\Repositories\Group\GroupRepositoryInterface;
use App\Repositories\Log\LogRepositoryInterface;
use Illuminate\Http\Request;
use App\Support\Cache\CacheKey;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
@@ -154,23 +155,19 @@ class GroupController extends Controller
/**
* Clear all group and product-related caches
*/
private function clearGroupCaches($typeId)
private function clearGroupCaches($typeId): void
{
// Clear general shop caches
Cache::forget('shop_types_with_groups');
Cache::forget(CacheKey::build('group', 'shop-types'));
if ($typeId) {
// Clear type-specific group cache
Cache::forget("groups_type_{$typeId}");
// Clear all product caches for this type
Cache::forget(CacheKey::build('group', 'by-type', ['type_id' => $typeId]));
$groups = $this->groupRepository->filter(['type_id' => $typeId]);
foreach ($groups as $group) {
Cache::forget("products_type_{$typeId}_group_{$group->id}");
Cache::forget(CacheKey::build('group', 'products', ['type_id' => $typeId, 'group_id' => $group->id]));
if ($group->slug) {
// Clear paginated product caches for multiple pages
for ($page = 1; $page <= 10; $page++) {
Cache::forget("group_products_{$group->slug}_page_{$page}");
Cache::forget(CacheKey::build('group', 'slug-products', ['page' => $page, 'slug' => $group->slug]));
}
}
}
@@ -560,6 +560,105 @@ class VCenterManagerController extends Controller
return response()->json(['error' => true, 'message' => $errorData['value'] ?? $errorData['error_type'] ?? 'Failed to power off VM'], 400);
}
public function restart(Request $request)
{
$vmId = (int) $request->input('vm_id');
$vm = VirtualMachines::find($vmId);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'VM not found'], 404);
}
$vmwareId = $vm->vm_id;
if (!$vmwareId) {
return response()->json(['error' => true, 'message' => 'VM VMware ID not found'], 400);
}
$vcenter = $vm->datacenter?->vCenter ?? $vm->host?->datacenter?->vCenter;
if (!$vcenter) {
return response()->json(['error' => true, 'message' => 'vCenter not found'], 400);
}
$vCenterService = new \App\Http\Services\vCenter();
if (!$vCenterService->loginServer($vcenter)) {
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
}
$response = Http::withoutVerifying()
->withHeaders(['vmware-api-session-id' => $vCenterService->token])
->post("https://{$vCenterService->apiUri}/rest/vcenter/vm/$vmwareId/power/reset");
if ($response->status() === 200) {
return response()->json(['error' => false, 'message' => 'Restart command sent successfully']);
}
$errorData = $response->json();
return response()->json([
'error' => true,
'message' => $errorData['value'] ?? $errorData['error_type'] ?? 'Failed to restart VM',
], 400);
}
public function snapshot(Request $request)
{
$request->validate([
'vm_id' => 'required|integer',
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
]);
$vmId = (int) $request->input('vm_id');
$vm = VirtualMachines::find($vmId);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'VM not found'], 404);
}
$vmwareId = $vm->vm_id;
if (!$vmwareId) {
return response()->json(['error' => true, 'message' => 'VM VMware ID not found'], 400);
}
$vcenter = $vm->datacenter?->vCenter ?? $vm->host?->datacenter?->vCenter;
if (!$vcenter) {
return response()->json(['error' => true, 'message' => 'vCenter not found'], 400);
}
$vCenterService = new \App\Http\Services\vCenter();
if (!$vCenterService->loginServer($vcenter)) {
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
}
$response = Http::withoutVerifying()
->withHeaders(['vmware-api-session-id' => $vCenterService->token])
->post("https://{$vCenterService->apiUri}/rest/vcenter/vm/$vmwareId/snapshot", [
'spec' => [
'name' => $request->input('name'),
'description' => $request->input('description', ''),
'memory' => false,
'quiesce' => false,
],
]);
if ($response->status() === 200) {
return response()->json(['error' => false, 'message' => 'Snapshot created successfully']);
}
$errorData = $response->json();
return response()->json([
'error' => true,
'message' => $errorData['value'] ?? $errorData['error_type'] ?? 'Failed to create snapshot',
], 400);
}
public function getPoweredOffVms()
{
$vms = VirtualMachines::whereNotNull('vm_id')
+98 -254
View File
@@ -13,9 +13,12 @@ use App\Http\Facade\SignRSA;
use App\Models\InfraManagement;
use Illuminate\Support\Str;
use phpseclib3\Net\SSH2;
use App\Http\Services\vCenter\Concerns\ManagesSsh;
class vCenter
{
use ManagesSsh;
public ?string $token = null;
public string $apiUri;
private string $username;
@@ -364,224 +367,7 @@ class vCenter
return $this->getAllNetwork($host);
}
protected function Command($cmd)
{
$auth = $this->LoginAuth();
if (!$auth) {
return "Login_Fail";
}
try {
// Build PowerShell command inline
$psCommand = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "' .
'Import-Module VMware.PowerCLI; ' .
'Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false -Scope Session; ' .
'$cred = Import-Clixml -Path \'C:\secure\vcenter.cred\'; ' .
'Connect-VIServer -Server \'' . $this->apiUri . '\' -Credential $cred; ' .
$cmd . '; ' .
'Disconnect-VIServer -Confirm:$false" 2>&1';
Log::channel('Custom')->info('PowerShell Command: ' . substr($psCommand, 0, 200) . '...');
$out = $auth->exec($psCommand);
$err = $auth->getStdError();
Log::channel('Custom')->info('PowerShell Output length: ' . strlen($out));
return $out . ($err ? "\n[STDERR]\n" . $err : '');
} catch (\Exception $e) {
Log::channel('Custom')->error('Command Exception: ' . $e->getMessage());
return $e->getMessage();
}
}
private function LoginAuth()
{
try {
$infra = InfraManagement::where('type', 'jumphost')
->where('status', true)
->first();
if (!$infra) {
Log::error('SSH Login Failed: No active jumphost configuration found in InfraManagement.');
return false;
}
$sshHost = $infra->host;
$sshPort = 22;
if (isset($infra->meta_data['port'])) {
$sshPort = $infra->meta_data['port'];
}
$sshUser = $infra->username;
$sshPass = SignRSA::DecryptFunc($infra->password);
$connection = new SSH2($sshHost, $sshPort);
if ($connection->login($sshUser, $sshPass)) {
$connection->setTimeout(0);
return $connection;
}
Log::error('SSH Login Failed: Invalid credentials for jumphost.');
return false;
} catch (Exception $error) {
Log::error('SSH Login Failed: ' . $error->getMessage());
return false;
}
}
// Login SSH
public function loginSSH(): bool
{
$conn = $this->LoginAuth();
if ($conn) {
Log::info("✅ Connected to vCenter via SSH successfully");
return true;
}
Log::error("❌ Failed to connect to vCenter via SSH");
return false;
}
public function runPsCommand(string $psCommand): string
{
$auth = $this->LoginAuth();
if (!$auth)
return "Login_Fail";
// gọi trực tiếp -Command, thu cả STDERR
$action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass '
. '-Command "' . str_replace('"', '\"', $psCommand) . '"';
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
}
protected function CommandRSP(string $cmd)
{
$auth = $this->LoginAuth();
if (!$auth)
return "Login_Fail";
// Escape để dùng trong chuỗi single-quoted của PowerShell
$esc = fn(string $v) => str_replace("'", "''", $v);
$server = $esc($this->apiUri ?? '');
$user = $esc($this->username ?? '');
$pass = $esc($this->password ?? '');
// Ghép script PowerShell hoàn chỉnh, chạy trong 1 scriptblock
// Lưu ý: dùng placeholder rồi strtr để tránh lẫn dấu ngoặc
$psTemplate = <<<'PS'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'
Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null
Connect-VIServer -Server '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
# Thực thi lệnh công việc (đã được xây dựng sẵn, ví dụ: & 'C:\path\script.ps1' -Param 'Value' ... )
& {
<CMD>
} | Out-String
PS;
// Chèn dữ liệu
$psScript = strtr($psTemplate, [
'<SERVER>' => $server,
'<USER>' => $user,
'<PASS>' => $pass,
'<CMD>' => $cmd, // ở đây $cmd KHÔNG cần ngoặc kép bao ngoài
]);
// Encode UTF-16LE → Base64 cho -EncodedCommand
$encoded = base64_encode(mb_convert_encoding($psScript, 'UTF-16LE', 'UTF-8'));
// Dùng -EncodedCommand để tránh mọi lỗi escape/quote
$action = "powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded";
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
}
protected function CommandChangePort(string $cmd, bool $expectJson = false)
{
$auth = $this->LoginAuth();
if (!$auth)
return "Login_Fail";
$esc = fn(string $v) => str_replace("'", "''", $v);
$server = $esc($this->apiUri ?? '');
$user = $esc($this->username ?? '');
$pass = $esc($this->password ?? '');
// PowerShell script template
$psTemplate = <<<'PS'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null
Connect-VIServer -Server '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
& {
<CMD>
}
PS;
$psScript = strtr($psTemplate, [
'<SERVER>' => $server,
'<USER>' => $user,
'<PASS>' => $pass,
'<CMD>' => $cmd,
]);
// Encode UTF-16LE cho -EncodedCommand
$encoded = base64_encode(mb_convert_encoding($psScript, 'UTF-16LE', 'UTF-8'));
$action = "powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded";
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
if ($expectJson) {
// CẮT JSON cuối: {...} hoặc [...]
$trimmed = trim($out);
// đơn giản: bắt khối { ... } hoặc [ ... ] ở cuối chuỗi
if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $trimmed, $m)) {
return $m[1];
}
// không tìm thấy → trả nguyên STDOUT (để caller log raw)
return $trimmed;
}
// Trường hợp không kỳ vọng JSON: giữ nguyên hành vi cũ (append STDERR)
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
}
// Moved to App\Http\Services\vCenter\Concerns\ManagesSsh
public function resetGuestPasswordViaPs1(string $vmName, string $guestUser, string $guestPassword, string $newPassword, string $targetUser = 'Administrator'): array|string
{
@@ -1769,51 +1555,72 @@ PS;
public function StartVirtualMachine($product, $vmware_id = ''): array|string
{
$check = $this->loginServer($product->ipaddress->host->datacenter->vCenter);
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->post("https://$this->apiUri/rest/vcenter/vm/$vmware_id/power/start");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
try {
$vmId = $vmware_id ?: ($product->vmware_id ?? null);
if (!$vmId) {
return ['error' => true, 'data' => 'Không tìm thấy vmware_id'];
}
return [
'error' => true,
'data' => $response['value']
];
$vcenter = $product->vCenter ?? null;
if (!$vcenter && $product->ipAddress?->host?->datacenter?->vCenter) {
$vcenter = $product->ipAddress->host->datacenter->vCenter;
}
if (!$vcenter) {
return ['error' => true, 'data' => 'Không tìm thấy thông tin vCenter cho VM'];
}
if (!$this->loginServer($vcenter)) {
return 'Login_Again';
}
$response = Http::withoutVerifying()
->withHeaders(['vmware-api-session-id' => $this->token])
->post("https://$this->apiUri/rest/vcenter/vm/$vmId/power/start");
if ($response->status() === 200) {
return ['error' => false, 'data' => 'success'];
}
return ['error' => true, 'data' => $response->json('value') ?? $response->body()];
} catch (\Throwable $e) {
return ['error' => true, 'data' => $e->getMessage()];
}
return "Login_Again";
}
public function ShutdownGuestVirtualMachine($product, $vmware_id = ''): array|string
{
$check = $this->loginServer($product->ipaddress->host->datacenter->vCenter);
if ($check) {
$response = Http::withoutVerifying()
->withHeaders([
'vmware-api-session-id' => $this->token
])
->post("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/power?action=shutdown");
if ($response->status() === 200) {
return [
'error' => false,
'data' => 'success'
];
try {
$vmId = $vmware_id ?: ($product->vmware_id ?? null);
if (!$vmId) {
return ['error' => true, 'data' => 'Không tìm thấy vmware_id'];
}
return [
'error' => true,
'data' => $response['value']
];
$vcenter = $product->vCenter ?? null;
if (!$vcenter && $product->ipAddress?->host?->datacenter?->vCenter) {
$vcenter = $product->ipAddress->host->datacenter->vCenter;
}
if (!$vcenter) {
return ['error' => true, 'data' => 'Không tìm thấy thông tin vCenter cho VM'];
}
if (!$this->loginServer($vcenter)) {
return 'Login_Again';
}
$response = Http::withoutVerifying()
->withHeaders(['vmware-api-session-id' => $this->token])
->post("https://$this->apiUri/rest/vcenter/vm/$vmId/guest/power?action=shutdown");
if ($response->status() === 200) {
return ['error' => false, 'data' => 'success'];
}
return ['error' => true, 'data' => $response->json('value') ?? $response->body()];
} catch (\Throwable $e) {
return ['error' => true, 'data' => $e->getMessage()];
}
return "Login_Again";
}
public function PowerOffVirtualMachine($vmware_id = ''): array|string
@@ -1839,6 +1646,43 @@ PS;
return "Login_Again";
}
public function getVmPowerState($product, string $vmware_id = ''): array|string
{
try {
$vmId = $vmware_id ?: ($product->vmware_id ?? null);
if (!$vmId) {
return ['error' => true, 'data' => 'Không tìm thấy vmware_id'];
}
$vcenter = $product->vCenter ?? null;
if (!$vcenter && $product->ipAddress?->host?->datacenter?->vCenter) {
$vcenter = $product->ipAddress->host->datacenter->vCenter;
}
if (!$vcenter) {
return ['error' => true, 'data' => 'Không tìm thấy thông tin vCenter cho VM'];
}
if (!$this->loginServer($vcenter)) {
return 'Login_Again';
}
$response = Http::withoutVerifying()
->withHeaders(['vmware-api-session-id' => $this->token])
->timeout(8)
->get("https://$this->apiUri/rest/vcenter/vm/$vmId/power");
if ($response->successful()) {
$data = $response->json();
return ['error' => false, 'data' => $data['value'] ?? $data];
}
return ['error' => true, 'data' => $response->json('value') ?? 'Không thể lấy trạng thái VM'];
} catch (\Throwable $e) {
return ['error' => true, 'data' => $e->getMessage()];
}
}
public function DeleteVirtualMachine($vmware_id = ''): array|string
{
$check = $this->checkApiExits();
@@ -0,0 +1,231 @@
<?php
namespace App\Http\Services\vCenter\Concerns;
use App\Http\Facade\SignRSA;
use App\Models\InfraManagement;
use Exception;
use Illuminate\Support\Facades\Log;
use phpseclib3\Net\SSH2;
/**
* SSH / PowerShell execution via jumphost.
*
* Requires on the host class:
* public ?string $token
* public string $apiUri
* private string $username
* private string $password
*/
trait ManagesSsh
{
// ─── Auth ────────────────────────────────────────────────────────────────
private function LoginAuth(): SSH2|false
{
try {
$infra = InfraManagement::where('type', 'jumphost')
->where('status', true)
->first();
if (!$infra) {
Log::error('SSH Login Failed: No active jumphost configuration found in InfraManagement.');
return false;
}
$sshHost = $infra->host;
$sshPort = 22;
if (isset($infra->meta_data['port'])) {
$sshPort = (int) $infra->meta_data['port'];
}
$connection = new SSH2($sshHost, $sshPort);
if ($connection->login($infra->username, SignRSA::DecryptFunc($infra->password))) {
$connection->setTimeout(0);
return $connection;
}
Log::error('SSH Login Failed: Invalid credentials for jumphost.');
return false;
} catch (Exception $error) {
Log::error('SSH Login Failed: ' . $error->getMessage());
return false;
}
}
public function loginSSH(): bool
{
$conn = $this->LoginAuth();
if ($conn) {
Log::info('✅ Connected to vCenter via SSH successfully');
return true;
}
Log::error('❌ Failed to connect to vCenter via SSH');
return false;
}
// ─── Command variants ────────────────────────────────────────────────────
/**
* Run a PowerShell command using a stored credential file on the jumphost.
* Uses C:\secure\vcenter.cred for vCenter auth — no plain-text password in command.
*/
protected function Command($cmd): string
{
$auth = $this->LoginAuth();
if (!$auth) {
return 'Login_Fail';
}
try {
$psCommand = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "'
. 'Import-Module VMware.PowerCLI; '
. 'Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false -Scope Session; '
. '$cred = Import-Clixml -Path \'C:\secure\vcenter.cred\'; '
. 'Connect-VIServer -Server \'' . $this->apiUri . '\' -Credential $cred; '
. $cmd . '; '
. 'Disconnect-VIServer -Confirm:$false" 2>&1';
Log::channel('Custom')->info('PowerShell Command: ' . substr($psCommand, 0, 200) . '...');
$out = $auth->exec($psCommand);
$errOut = $auth->getStdError();
Log::channel('Custom')->info('PowerShell Output length: ' . strlen($out));
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Exception $e) {
Log::channel('Custom')->error('Command Exception: ' . $e->getMessage());
return $e->getMessage();
}
}
/**
* Run a raw PowerShell command directly — no vCenter connect wrapper.
*/
public function runPsCommand(string $psCommand): string
{
$auth = $this->LoginAuth();
if (!$auth) {
return 'Login_Fail';
}
$action = 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass '
. '-Command "' . str_replace('"', '\"', $psCommand) . '"';
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return 'Error: ' . $e->getMessage();
}
}
/**
* Run a command using EncodedCommand — connects to vCenter with plain credentials.
* Safer for complex scripts that would break with inline quoting.
*/
protected function CommandRSP(string $cmd): string
{
$auth = $this->LoginAuth();
if (!$auth) {
return 'Login_Fail';
}
$esc = fn(string $v) => str_replace("'", "''", $v);
$script = strtr(
<<<'PS'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'
Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null
Connect-VIServer -Server '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
& {
<CMD>
} | Out-String
PS,
[
'<SERVER>' => $esc($this->apiUri ?? ''),
'<USER>' => $esc($this->username ?? ''),
'<PASS>' => $esc($this->password ?? ''),
'<CMD>' => $cmd,
]
);
$encoded = base64_encode(mb_convert_encoding($script, 'UTF-16LE', 'UTF-8'));
$action = "powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded";
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return 'Error: ' . $e->getMessage();
}
}
/**
* Run a command using EncodedCommand — variant used for RDP port changes.
* Optionally strips raw output down to the first JSON block found.
*/
protected function CommandChangePort(string $cmd, bool $expectJson = false): string
{
$auth = $this->LoginAuth();
if (!$auth) {
return 'Login_Fail';
}
$esc = fn(string $v) => str_replace("'", "''", $v);
$script = strtr(
<<<'PS'
$ProgressPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$false | Out-Null
Connect-VIServer -Server '<SERVER>' -User '<USER>' -Password '<PASS>' | Out-Null
& {
<CMD>
}
PS,
[
'<SERVER>' => $esc($this->apiUri ?? ''),
'<USER>' => $esc($this->username ?? ''),
'<PASS>' => $esc($this->password ?? ''),
'<CMD>' => $cmd,
]
);
$encoded = base64_encode(mb_convert_encoding($script, 'UTF-16LE', 'UTF-8'));
$action = "powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encoded";
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
if ($expectJson) {
$trimmed = trim($out);
if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $trimmed, $m)) {
return $m[1];
}
return $trimmed;
}
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return 'Error: ' . $e->getMessage();
}
}
}
+20 -22
View File
@@ -2,8 +2,9 @@
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Log;
use App\Support\Cache\CacheKey;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use App\Http\Facade\SignRSA;
class SnapshotService
@@ -68,10 +69,10 @@ class SnapshotService
} catch (\Exception $e) {
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
}
$vmware_name = (string) $product->name;
// In original code, it checks checkApiExits(), if true proceed.
if ($this->client->checkApiExits()) {
try {
$Snapshot = $vmware_name . "_" . date('d_m_Y_H_i_s');
@@ -102,24 +103,23 @@ class SnapshotService
private function clearSnapshotCache($product): void
{
$vmware_name = (string) $product->name;
$cacheKey = "snapshots_{$product->id}_{$vmware_name}";
$cacheKey = CacheKey::build('snapshot', 'list', ['vm_id' => $product->id]);
Cache::forget($cacheKey);
Log::channel('Custom')->info("🗑️ Cleared snapshot cache for VM: {$vmware_name}");
Log::channel('Custom')->info("🗑️ Cleared snapshot cache for VM: {$product->name}");
}
public function getSnapShot($product): string|array
{
// ... (Similar logic, using SOAP)
// Since SOAP logic is quite complex and self-contained in vCenter.php,
// Since SOAP logic is quite complex and self-contained in vCenter.php,
// I'll copy it here but need to adapt $this->callSoap etc.
// Actually, callSoap uses curl with manual implementation.
// I should probably simplify or copy that helper too.
// This method heavily relies on internal SOAP helpers.
// For simplicity in this 'splitting' task, I will implement the public method
// For simplicity in this 'splitting' task, I will implement the public method
// and add the private helpers it depends on.
try {
$vcenterModel = $this->resolveVCenterFromProduct($product);
@@ -132,10 +132,8 @@ class SnapshotService
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
}
$vmware_name = (string) $product->name;
$vmId = $product->vmware_id;
$cacheKey = "snapshots_{$product->id}_{$vmware_name}";
$vmId = $product->vmware_id;
$cacheKey = CacheKey::build('snapshot', 'list', ['vm_id' => $product->id]);
$cachedResult = Cache::get($cacheKey);
if ($cachedResult !== null) {
@@ -234,7 +232,7 @@ class SnapshotService
try {
$xml = simplexml_load_string($result['body']);
if ($xml === false) return [];
$xml->registerXPathNamespace('u', 'urn:vim25');
$json = json_encode($xml);
$array = json_decode($json, true);
@@ -271,14 +269,14 @@ class SnapshotService
if (isset($node['childSnapshotList'])) {
$children = $node['childSnapshotList'];
// Check if 'snapshot' key exists directly (single child wrapped)
if (isset($children['snapshot'])) {
// If it's an array of snapshots or single object?
// XML conversion: if strictly one child, might be direct. if multiple, array.
// But simplexml usually produces array if multiple same tags.
// However, structure is usually childSnapshotList -> snapshot (array or single)
$snaps = $children['snapshot'];
// If $snaps has numeric keys, it's array. Else single.
if (array_keys($snaps) !== range(0, count($snaps) - 1)) {
@@ -288,7 +286,7 @@ class SnapshotService
foreach ($snaps as $child) {
$list = array_merge($list, $this->flattenSnapshotTree($child));
}
}
}
}
return $list;
}
@@ -296,7 +294,7 @@ class SnapshotService
private function callSoap($soapBody, $cookie = null)
{
$curl = curl_init();
$host = $this->client->apiUri;
$host = $this->client->apiUri;
$headers = [
'Content-Type: text/xml',
@@ -327,11 +325,11 @@ class SnapshotService
));
$response = curl_exec($curl);
$header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $header_size);
$body = substr($response, $header_size);
curl_close($curl);
// Parse cookie
@@ -344,7 +342,7 @@ class SnapshotService
}
}
}
return ['body' => $body, 'cookie' => $cookieValue];
}
}