refactor: implement centralized cache key management and add utility traits for vCenter SSH operations.
This commit is contained in:
Vendored
BIN
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());
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\VirtualMachine;
|
||||
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Models\vCenter;
|
||||
use App\Services\VmProvisionService;
|
||||
use App\Services\VsphereHostSshService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* ChangeVmPasswordJob
|
||||
*
|
||||
* Customer-initiated VM guest OS password change.
|
||||
*
|
||||
* Flow:
|
||||
* [1] Load vCenter credentials
|
||||
* [2] SSH → jumphost → changePwdLinux.ps1 / changePwdWindows.ps1
|
||||
* [3] Parse SUCCESS / ERROR from output
|
||||
* [4] On SUCCESS → persist encrypted new password to DB
|
||||
* [5] On FAIL → retry (up to $tries times)
|
||||
*/
|
||||
class ChangeVmPasswordJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
public int $timeout = 180;
|
||||
public int $backoff = 30;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $moduleVmwareId,
|
||||
public readonly string $vmId,
|
||||
public readonly string $osFamily,
|
||||
public readonly string $newPassword,
|
||||
public readonly string $currentGuestPassword,
|
||||
) {}
|
||||
|
||||
public function handle(VmProvisionService $provision, VsphereHostSshService $ssh): void
|
||||
{
|
||||
Log::info('ChangeVmPasswordJob: starting', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
'os_family' => $this->osFamily,
|
||||
]);
|
||||
|
||||
$vcenter = vCenter::first();
|
||||
if (!$vcenter) {
|
||||
throw new \RuntimeException('No vCenter record found in module_vcenter');
|
||||
}
|
||||
|
||||
$output = $ssh->changePasswordCustomer(
|
||||
vmId: $this->vmId,
|
||||
newPassword: $this->newPassword,
|
||||
currentGuestPassword: $this->currentGuestPassword,
|
||||
osFamily: $this->osFamily,
|
||||
vcenterHost: $vcenter->host,
|
||||
vcenterUser: $vcenter->user,
|
||||
vcenterPassword: SignRSA::DecryptFunc($vcenter->password),
|
||||
);
|
||||
|
||||
$this->assertSuccess($output);
|
||||
|
||||
// Persist encrypted password to DB
|
||||
$provision->updatePassword($this->moduleVmwareId, $this->newPassword);
|
||||
|
||||
Log::info('ChangeVmPasswordJob: password changed successfully', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('ChangeVmPasswordJob: all retries exhausted', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function assertSuccess(string $output): void
|
||||
{
|
||||
if (str_contains(strtoupper(trim($output)), 'SUCCESS')) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
"Password change script failed for VM {$this->vmId}. Output: " . trim($output)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class WaitStatusVirtualMachine implements ShouldQueue
|
||||
|
||||
// Action Completed
|
||||
// 1. Clear Cache again (to ensure next fetch gets new state)
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $this->vmware_id);
|
||||
\Illuminate\Support\Facades\Cache::forget(\App\Support\Cache\CacheKey::build('vm', 'info', ['id' => $this->vmware_id]));
|
||||
|
||||
// 2. Update DB Status and Dispatch Event
|
||||
$product = \App\Models\ModuleVmware::where('vmware_id', $this->vmware_id)->first();
|
||||
|
||||
@@ -57,7 +57,7 @@ class RebootVmJob implements ShouldQueue
|
||||
Log::info("RebootVmJob: Command sent successfully for VM {$product->vmware_id}");
|
||||
|
||||
// Clear VM info cache
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
|
||||
\Illuminate\Support\Facades\Cache::forget(\App\Support\Cache\CacheKey::build('vm', 'info', ['id' => $product->vmware_id]));
|
||||
|
||||
// Dispatch Event to Frontend
|
||||
\App\Events\VirtualMachineStatusChanged::dispatch(
|
||||
|
||||
@@ -59,7 +59,7 @@ class StartVmJob implements ShouldQueue
|
||||
Log::info("StartVmJob: Command sent successfully for VM {$product->vmware_id}");
|
||||
|
||||
// Clear VM info cache
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
|
||||
\Illuminate\Support\Facades\Cache::forget(\App\Support\Cache\CacheKey::build('vm', 'info', ['id' => $product->vmware_id]));
|
||||
|
||||
// Dispatch Event to Frontend
|
||||
\App\Events\VirtualMachineStatusChanged::dispatch(
|
||||
|
||||
@@ -57,7 +57,7 @@ class StopVmJob implements ShouldQueue
|
||||
|
||||
if (is_array($result) && isset($result['error']) && $result['error'] === false) {
|
||||
// Clear VM info cache
|
||||
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
|
||||
\Illuminate\Support\Facades\Cache::forget(\App\Support\Cache\CacheKey::build('vm', 'info', ['id' => $product->vmware_id]));
|
||||
|
||||
// Dispatch Event to Frontend
|
||||
\App\Events\VirtualMachineStatusChanged::dispatch(
|
||||
|
||||
@@ -126,7 +126,7 @@ class UpdateVMInfoJob implements ShouldQueue
|
||||
];
|
||||
|
||||
// Cache the result for 5 minutes
|
||||
$cacheKey = "vm_info_{$this->vmId}";
|
||||
$cacheKey = \App\Support\Cache\CacheKey::build('vm', 'info', ['id' => $this->vmId]);
|
||||
Cache::put($cacheKey, $vmInfo, 300);
|
||||
|
||||
Log::info('VM info updated successfully', ['vm_id' => $this->vmId]);
|
||||
|
||||
@@ -4,13 +4,14 @@ namespace App\Providers;
|
||||
|
||||
use App\Http\Helpers\Helper;
|
||||
use App\Http\Services\SignRSA;
|
||||
use App\Services\Cache\CacheService;
|
||||
use App\Services\Cache\CacheServiceInterface;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\ModuleIpAddress;
|
||||
use App\Observers\ModuleVmwareObserver;
|
||||
use App\Observers\ModuleIpAddressObserver;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -22,6 +23,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(CacheServiceInterface::class, CacheService::class);
|
||||
|
||||
$this->app->bind('signrsa', function () {
|
||||
return new SignRSA();
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Providers;
|
||||
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Models\Setting;
|
||||
use App\Support\Cache\CacheKey;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
@@ -23,7 +24,7 @@ class MailConfigServiceProvider extends ServiceProvider
|
||||
return;
|
||||
}
|
||||
|
||||
$config = Cache::remember('mail_config', 3600, function () {
|
||||
$config = Cache::remember(CacheKey::build('settings', 'mail'), 3600, function () {
|
||||
if (!Schema::hasTable('setting')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Repositories\Addon;
|
||||
|
||||
use App\Repositories\BaseCachedRepository;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class AddonCachedRepository extends BaseCachedRepository implements AddonRepositoryInterface
|
||||
{
|
||||
@@ -12,10 +11,12 @@ class AddonCachedRepository extends BaseCachedRepository implements AddonReposit
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getModel()
|
||||
{
|
||||
return $this->addonRepository->getModel();
|
||||
}
|
||||
|
||||
public function filter(
|
||||
array $data = [],
|
||||
array $relationships = [],
|
||||
@@ -23,24 +24,13 @@ class AddonCachedRepository extends BaseCachedRepository implements AddonReposit
|
||||
array $select = ['*'],
|
||||
bool $returnQuery = false
|
||||
) {
|
||||
if (
|
||||
$returnQuery ||
|
||||
!empty($data['paginate'])
|
||||
) {
|
||||
return $this->addonRepository
|
||||
->filter($data, $relationships, $withCount, $select, $returnQuery);
|
||||
if ($returnQuery || !empty($data['paginate'])) {
|
||||
return $this->addonRepository->filter($data, $relationships, $withCount, $select, $returnQuery);
|
||||
}
|
||||
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'filter' => compact('data', 'relationships', 'withCount', 'select')
|
||||
])));
|
||||
|
||||
return Cache::remember(
|
||||
$cacheKey,
|
||||
$this->cacheTime,
|
||||
fn() =>
|
||||
$this->addonRepository
|
||||
->filter($data, $relationships, $withCount, $select, $returnQuery)
|
||||
return $this->rememberFilter(
|
||||
compact('data', 'relationships', 'withCount', 'select'),
|
||||
fn () => $this->addonRepository->filter($data, $relationships, $withCount, $select, $returnQuery)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,90 +2,102 @@
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Services\Cache\CacheServiceInterface;
|
||||
use App\Support\Cache\CacheKey;
|
||||
|
||||
abstract class BaseCachedRepository extends BaseRepository
|
||||
{
|
||||
protected $cacheTime = 172800; // Thời gian lưu cache )giây)
|
||||
protected int $cacheTime = 172800; // 48 hours (fallback for raw Cache calls)
|
||||
|
||||
protected CacheServiceInterface $cache;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->cache = app(CacheServiceInterface::class);
|
||||
}
|
||||
|
||||
public function find($id)
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($id);
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($id) {
|
||||
return parent::find($id);
|
||||
});
|
||||
$key = CacheKey::build($this->getModuleName(), 'find', ['id' => $id]);
|
||||
|
||||
return $this->cache->remember($key, fn () => parent::find($id), CacheServiceInterface::TTL_LONG);
|
||||
}
|
||||
|
||||
public function getAll()
|
||||
{
|
||||
|
||||
$cacheKey = $this->getCacheKey("getAll");
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () {
|
||||
return parent::getAll();
|
||||
});
|
||||
$key = CacheKey::build($this->getModuleName(), 'all');
|
||||
|
||||
return $this->cache->remember($key, fn () => parent::getAll(), CacheServiceInterface::TTL_LONG);
|
||||
}
|
||||
|
||||
public function getCount()
|
||||
{
|
||||
$key = CacheKey::build($this->getModuleName(), 'count');
|
||||
|
||||
return $this->cache->remember($key, fn () => parent::getCount(), CacheServiceInterface::TTL_LONG);
|
||||
}
|
||||
|
||||
public function create($data = [])
|
||||
{
|
||||
$result = parent::create($data);
|
||||
$this->clearCache();
|
||||
$this->cache->clearByModule($this->getModuleName());
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function insert(array $data = [])
|
||||
{
|
||||
$result = parent::insert($data);
|
||||
$this->clearCache();
|
||||
$this->cache->clearByModule($this->getModuleName());
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function update(int $id = 0, array $data = [])
|
||||
{
|
||||
$result = parent::update($id, $data);
|
||||
$this->clearCache();
|
||||
Cache::forget($this->getCacheKey($id));
|
||||
$this->cache->clearByModule($this->getModuleName());
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$result = parent::delete($id);
|
||||
$this->clearCache();
|
||||
Cache::forget($this->getCacheKey($id));
|
||||
$this->cache->clearByModule($this->getModuleName());
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getCacheKey($method)
|
||||
public function clearCache(): void
|
||||
{
|
||||
return sprintf('%s_%s', static::class, $method);
|
||||
$this->cache->clearByModule($this->getModuleName());
|
||||
}
|
||||
public function getCount()
|
||||
{
|
||||
$cacheKey = $this->getCacheKey("getCount");
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () {
|
||||
return parent::getCount();
|
||||
});
|
||||
}
|
||||
public function clearCache()
|
||||
{
|
||||
// Chỉ xóa cache của repository này, không flush toàn bộ cache app
|
||||
$keysToForget = [
|
||||
$this->getCacheKey('getAll'),
|
||||
$this->getCacheKey('getCount'),
|
||||
];
|
||||
|
||||
foreach ($keysToForget as $key) {
|
||||
Cache::forget($key);
|
||||
}
|
||||
}
|
||||
public function clearCacheByKey(string $key)
|
||||
public function clearCacheByKey(string $key): void
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($key);
|
||||
$this->cache->forget(CacheKey::build($this->getModuleName(), $key));
|
||||
}
|
||||
|
||||
Cache::forget($cacheKey);
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers for child repositories
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
protected function rememberFilter(array $params, callable $query): mixed
|
||||
{
|
||||
$key = CacheKey::build($this->getModuleName(), 'filter', [
|
||||
'hash' => md5(serialize($params)),
|
||||
]);
|
||||
|
||||
return $this->cache->remember($key, $query, CacheServiceInterface::TTL_LONG);
|
||||
}
|
||||
|
||||
protected function getModuleName(): string
|
||||
{
|
||||
$shortName = class_basename(static::class);
|
||||
return strtolower(str_replace('CachedRepository', '', $shortName));
|
||||
}
|
||||
|
||||
// Keep for backward compat — child classes that still call getCacheKey() directly.
|
||||
protected function getCacheKey(string $suffix): string
|
||||
{
|
||||
return CacheKey::build($this->getModuleName(), $suffix);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Repositories\Customer;
|
||||
|
||||
use App\Repositories\BaseCachedRepository;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Support\Cache\CacheKey;
|
||||
|
||||
class CustomerCachedRepository extends BaseCachedRepository implements CustomerRepositoryInterface
|
||||
{
|
||||
@@ -31,39 +31,22 @@ class CustomerCachedRepository extends BaseCachedRepository implements CustomerR
|
||||
->filter($data, $relationships, $withCount, $select, $returnQuery);
|
||||
}
|
||||
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'filter' => compact('data', 'relationships', 'withCount', 'select')
|
||||
])));
|
||||
|
||||
return Cache::remember(
|
||||
$cacheKey,
|
||||
$this->cacheTime,
|
||||
fn() =>
|
||||
$this->customerRepository
|
||||
->filter($data, $relationships, $withCount, $select, $returnQuery)
|
||||
return $this->rememberFilter(
|
||||
compact('data', 'relationships', 'withCount', 'select'),
|
||||
fn() => $this->customerRepository->filter($data, $relationships, $withCount, $select, $returnQuery)
|
||||
);
|
||||
}
|
||||
public function findWithRelationships(int $id = 0, array $relationships = [], array $withCount = [], array $withSum = [])
|
||||
{
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'findWithRelationships' => [
|
||||
'id' => $id,
|
||||
'relationships' => $relationships
|
||||
]
|
||||
])));
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($id, $relationships, $withCount, $withSum) {
|
||||
$cacheKey = CacheKey::build($this->getModuleName(), 'find-with-relationships', ['id' => $id]);
|
||||
return $this->cache->remember($cacheKey, function () use ($id, $relationships, $withCount, $withSum) {
|
||||
return $this->customerRepository->findWithRelationships($id, $relationships, $withCount, $withSum);
|
||||
});
|
||||
}
|
||||
public function findBy(string $key = '', string $value = '')
|
||||
{
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'findBy' => [
|
||||
'key' => $key,
|
||||
'value' => $value
|
||||
]
|
||||
])));
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($key, $value) {
|
||||
$cacheKey = CacheKey::build($this->getModuleName(), 'find-by', ['key' => $key, 'value' => $value]);
|
||||
return $this->cache->remember($cacheKey, function () use ($key, $value) {
|
||||
return $this->customerRepository->findBy($key, $value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Repositories\Discount;
|
||||
|
||||
use App\Repositories\BaseCachedRepository;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Support\Cache\CacheKey;
|
||||
|
||||
class DiscountCachedRepository extends BaseCachedRepository implements DiscountRepositoryInterface
|
||||
{
|
||||
@@ -18,32 +18,19 @@ class DiscountCachedRepository extends BaseCachedRepository implements DiscountR
|
||||
}
|
||||
public function filter(array $data = [], array $relationships = [], array $withCount = [], array $select = ['*'])
|
||||
{
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'filterItem' => [
|
||||
'data' => $data,
|
||||
'relationships' => $relationships,
|
||||
'select' => $select,
|
||||
'withCount' => $withCount
|
||||
]
|
||||
])));
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($data, $relationships, $select, $withCount) {
|
||||
return $this->discountRepository->filter($data, $relationships, $withCount, $select);
|
||||
});
|
||||
return $this->rememberFilter(
|
||||
compact('data', 'relationships', 'withCount', 'select'),
|
||||
fn() => $this->discountRepository->filter($data, $relationships, $withCount, $select)
|
||||
);
|
||||
}
|
||||
public function findBy(string $key = '', string $value = '')
|
||||
{
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'findBy' => [
|
||||
'key' => $key,
|
||||
'value' => $value
|
||||
]
|
||||
])));
|
||||
if (empty($value) || empty($key)) {
|
||||
return false;
|
||||
}
|
||||
return $this->model->where($key, $value)->first();
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($key, $value) {
|
||||
return $this->discountRepository->findBy($key, $value);
|
||||
$cacheKey = CacheKey::build($this->getModuleName(), 'find-by', ['key' => $key, 'value' => $value]);
|
||||
return $this->cache->remember($cacheKey, function () use ($key, $value) {
|
||||
return $this->model->where($key, $value)->first();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Repositories\Group;
|
||||
|
||||
use App\Repositories\BaseCachedRepository;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Support\Cache\CacheKey;
|
||||
|
||||
class GroupCachedRepository extends BaseCachedRepository implements GroupRepositoryInterface
|
||||
{
|
||||
@@ -21,28 +21,18 @@ class GroupCachedRepository extends BaseCachedRepository implements GroupReposit
|
||||
|
||||
public function getProduct(int $group_id = 0, int $type_id = 0)
|
||||
{
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'type_id' => $type_id,
|
||||
'group_id' => $group_id
|
||||
])));
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($type_id, $group_id) {
|
||||
$cacheKey = CacheKey::build($this->getModuleName(), 'product', ['group_id' => $group_id, 'type_id' => $type_id]);
|
||||
return $this->cache->remember($cacheKey, function () use ($type_id, $group_id) {
|
||||
return $this->groupRepository->getProduct($group_id, $type_id);
|
||||
});
|
||||
}
|
||||
|
||||
public function filter(array $data = [], array $relationships = [], array $withCount = [], array $select = ['*'])
|
||||
{
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'filterItem' => [
|
||||
'data' => $data,
|
||||
'relationships' => $relationships,
|
||||
'select' => $select,
|
||||
'withCount' => $withCount
|
||||
]
|
||||
])));
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($data, $relationships, $select, $withCount) {
|
||||
return $this->groupRepository->filter($data, $relationships, $withCount, $select);
|
||||
});
|
||||
return $this->rememberFilter(
|
||||
compact('data', 'relationships', 'withCount', 'select'),
|
||||
fn() => $this->groupRepository->filter($data, $relationships, $withCount, $select)
|
||||
);
|
||||
}
|
||||
|
||||
public function findBySlugWithProduct(string $slug)
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
namespace App\Repositories\Whois;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Services\Cache\CacheServiceInterface;
|
||||
use App\Support\Cache\CacheKey;
|
||||
|
||||
class CachedWhoisRepository implements WhoisRepositoryInterface
|
||||
{
|
||||
protected string $cachePrefix = 'whois';
|
||||
protected int $cacheTtl = 3600; // 1 hour
|
||||
|
||||
protected CacheServiceInterface $cache;
|
||||
|
||||
public function __construct(CacheServiceInterface $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain information from cache
|
||||
*
|
||||
@@ -17,10 +22,10 @@ class CachedWhoisRepository implements WhoisRepositoryInterface
|
||||
*/
|
||||
public function getDomainInfo(string $domain): ?array
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($domain);
|
||||
return Cache::get($cacheKey);
|
||||
$cacheKey = CacheKey::build('whois', 'domain', ['domain' => strtolower($domain)]);
|
||||
return $this->cache->get($cacheKey);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cache domain information
|
||||
*
|
||||
@@ -30,10 +35,11 @@ class CachedWhoisRepository implements WhoisRepositoryInterface
|
||||
*/
|
||||
public function cacheDomainInfo(string $domain, array $data): bool
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($domain);
|
||||
return Cache::put($cacheKey, $data, $this->cacheTtl);
|
||||
$cacheKey = CacheKey::build('whois', 'domain', ['domain' => strtolower($domain)]);
|
||||
$this->cache->put($cacheKey, $data);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear domain cache
|
||||
*
|
||||
@@ -42,10 +48,11 @@ class CachedWhoisRepository implements WhoisRepositoryInterface
|
||||
*/
|
||||
public function clearDomainCache(string $domain): bool
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($domain);
|
||||
return Cache::forget($cacheKey);
|
||||
$cacheKey = CacheKey::build('whois', 'domain', ['domain' => strtolower($domain)]);
|
||||
$this->cache->forget($cacheKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear all whois cache
|
||||
*
|
||||
@@ -53,17 +60,7 @@ class CachedWhoisRepository implements WhoisRepositoryInterface
|
||||
*/
|
||||
public function clearAllCache(): bool
|
||||
{
|
||||
return Cache::flush();
|
||||
$this->cache->clearByModule('whois');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key for domain
|
||||
*
|
||||
* @param string $domain
|
||||
* @return string
|
||||
*/
|
||||
protected function getCacheKey(string $domain): string
|
||||
{
|
||||
return "{$this->cachePrefix}.domain." . strtolower($domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Repositories\vCenter;
|
||||
|
||||
use App\Repositories\BaseCachedRepository;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Support\Cache\CacheKey;
|
||||
|
||||
class vCenterCachedRepository extends BaseCachedRepository implements vCenterRepositoryInterface
|
||||
{
|
||||
@@ -31,26 +31,15 @@ class vCenterCachedRepository extends BaseCachedRepository implements vCenterRep
|
||||
->filter($data, $relationships, $withCount, $select, $returnQuery);
|
||||
}
|
||||
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'filter' => compact('data', 'relationships', 'withCount', 'select')
|
||||
])));
|
||||
|
||||
return Cache::remember(
|
||||
$cacheKey,
|
||||
$this->cacheTime,
|
||||
fn() =>
|
||||
$this->vCenterRepository
|
||||
->filter($data, $relationships, $withCount, $select, $returnQuery)
|
||||
return $this->rememberFilter(
|
||||
compact('data', 'relationships', 'withCount', 'select'),
|
||||
fn() => $this->vCenterRepository->filter($data, $relationships, $withCount, $select, $returnQuery)
|
||||
);
|
||||
}
|
||||
public function findByName(string $name = '')
|
||||
{
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode([
|
||||
'findByName' => [
|
||||
'name' => $name
|
||||
]
|
||||
])));
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($name) {
|
||||
$cacheKey = CacheKey::build($this->getModuleName(), 'find-by-name', ['name' => $name]);
|
||||
return $this->cache->remember($cacheKey, function () use ($name) {
|
||||
return $this->vCenterRepository->findByName($name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,56 +2,127 @@
|
||||
|
||||
namespace App\Services\Cache;
|
||||
|
||||
|
||||
use App\Services\Cache\CacheServiceInterface;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use stdClass;
|
||||
|
||||
class CacheService implements CacheServiceInterface
|
||||
{
|
||||
protected int $ttl;
|
||||
private const TTL_SECONDS = [
|
||||
self::TTL_SHORT => 300, // 5 minutes
|
||||
self::TTL_MEDIUM => 3600, // 1 hour
|
||||
self::TTL_LONG => 86400, // 24 hours
|
||||
];
|
||||
|
||||
public function __construct(int $ttl = 60)
|
||||
{
|
||||
$this->ttl = $ttl;
|
||||
}
|
||||
private const TAGGABLE_DRIVERS = ['redis', 'memcached', 'dynamodb'];
|
||||
|
||||
public function putCache(string $prefix, string $key, mixed $value, ?int $ttl = null): void
|
||||
{
|
||||
$cacheKey = $this->makeKey($prefix, $key);
|
||||
Cache::put($cacheKey, $value, $ttl ?? $this->ttl);
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Interface implementation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function getCache(string $prefix, string $key, mixed $default = null): mixed
|
||||
{
|
||||
$cacheKey = $this->makeKey($prefix, $key);
|
||||
return Cache::get($cacheKey, $default);
|
||||
}
|
||||
public function remember(
|
||||
string $key,
|
||||
Closure $callback,
|
||||
string $ttl = self::TTL_MEDIUM,
|
||||
array $tags = [],
|
||||
): mixed {
|
||||
$store = $this->resolveStore($tags);
|
||||
$sentinel = new stdClass();
|
||||
$cached = $store->get($key, $sentinel);
|
||||
|
||||
public function clearCache(string $prefix, string $key): void
|
||||
{
|
||||
$cacheKey = $this->makeKey($prefix, $key);
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
public function clearCacheByPrefix(string $prefix): void
|
||||
{
|
||||
|
||||
$cachePrefix = config('cache.prefix');
|
||||
|
||||
$pattern = $cachePrefix . $prefix . '_*';
|
||||
$keys = Redis::connection('cache')->keys($pattern);
|
||||
|
||||
|
||||
foreach ($keys as $k) {
|
||||
Redis::connection('cache')->del($k);
|
||||
if ($cached !== $sentinel) {
|
||||
Log::debug("CACHE HIT: {$key}");
|
||||
return $cached;
|
||||
}
|
||||
|
||||
Log::debug("CACHE MISS: {$key}");
|
||||
$value = $callback();
|
||||
$store->put($key, $value, $this->resolveTtl($ttl));
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function makeKey(string $prefix, string $key = ''): string
|
||||
public function put(
|
||||
string $key,
|
||||
mixed $value,
|
||||
string $ttl = self::TTL_MEDIUM,
|
||||
array $tags = [],
|
||||
): void {
|
||||
$this->resolveStore($tags)->put($key, $value, $this->resolveTtl($ttl));
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return trim($prefix . '_' . $key, '_');
|
||||
return Cache::get($key, $default);
|
||||
}
|
||||
|
||||
public function forget(string $key): void
|
||||
{
|
||||
Cache::forget($key);
|
||||
}
|
||||
|
||||
public function clearByModule(string $module): void
|
||||
{
|
||||
if ($this->supportsTagging()) {
|
||||
Cache::tags([$module])->flush();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->scanAndDelete($module);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Internals
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function resolveStore(array $tags): CacheRepository
|
||||
{
|
||||
if (!empty($tags) && $this->supportsTagging()) {
|
||||
return Cache::tags($tags);
|
||||
}
|
||||
|
||||
return Cache::store();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all keys for a module using SCAN (safe for production).
|
||||
*
|
||||
* KEYS is O(N) and blocks Redis; SCAN iterates in batches without blocking.
|
||||
* The pattern matches: {cache_prefix}:{module}:*
|
||||
*/
|
||||
private function scanAndDelete(string $module): void
|
||||
{
|
||||
$redis = Redis::connection('cache');
|
||||
$prefix = config('cache.prefix');
|
||||
$pattern = "{$prefix}:{$module}:*";
|
||||
$cursor = '0';
|
||||
|
||||
do {
|
||||
$result = $redis->scan($cursor, 'MATCH', $pattern, 'COUNT', 100);
|
||||
|
||||
if ($result === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
[$cursor, $keys] = $result;
|
||||
|
||||
foreach ((array) $keys as $key) {
|
||||
$redis->del($key);
|
||||
}
|
||||
|
||||
} while ((string) $cursor !== '0');
|
||||
}
|
||||
|
||||
private function supportsTagging(): bool
|
||||
{
|
||||
return in_array(config('cache.default'), self::TAGGABLE_DRIVERS, strict: true);
|
||||
}
|
||||
|
||||
private function resolveTtl(string $ttl): int
|
||||
{
|
||||
return self::TTL_SECONDS[$ttl] ?? self::TTL_SECONDS[self::TTL_MEDIUM];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,48 @@
|
||||
|
||||
namespace App\Services\Cache;
|
||||
|
||||
use Closure;
|
||||
|
||||
interface CacheServiceInterface
|
||||
{
|
||||
public function putCache(string $prefix, string $key, mixed $value, ?int $ttl = null): void;
|
||||
public function getCache(string $prefix, string $key, mixed $default = null): mixed;
|
||||
public function clearCache(string $prefix, string $key): void;
|
||||
public function clearCacheByPrefix(string $prefix): void;
|
||||
}
|
||||
public const TTL_SHORT = 'short'; // 5 minutes
|
||||
public const TTL_MEDIUM = 'medium'; // 1 hour
|
||||
public const TTL_LONG = 'long'; // 24 hours
|
||||
|
||||
/**
|
||||
* Return cached value or execute callback and store the result.
|
||||
* Logs HIT / MISS to debug channel.
|
||||
*
|
||||
* @param string[] $tags Optional tag list (only applied when driver supports tagging)
|
||||
*/
|
||||
public function remember(
|
||||
string $key,
|
||||
Closure $callback,
|
||||
string $ttl = self::TTL_MEDIUM,
|
||||
array $tags = [],
|
||||
): mixed;
|
||||
|
||||
/**
|
||||
* Store a value unconditionally.
|
||||
*
|
||||
* @param string[] $tags
|
||||
*/
|
||||
public function put(
|
||||
string $key,
|
||||
mixed $value,
|
||||
string $ttl = self::TTL_MEDIUM,
|
||||
array $tags = [],
|
||||
): void;
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed;
|
||||
|
||||
public function forget(string $key): void;
|
||||
|
||||
/**
|
||||
* Invalidate all cache entries belonging to a module.
|
||||
*
|
||||
* Uses Cache::tags()->flush() when the driver supports tagging (Redis, Memcached),
|
||||
* falls back to a Redis SCAN-based deletion otherwise.
|
||||
*/
|
||||
public function clearByModule(string $module): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Services\Cache\CacheServiceInterface;
|
||||
use App\Support\Cache\CacheKey;
|
||||
|
||||
/**
|
||||
* Example service demonstrating the CacheService + CacheKey pattern.
|
||||
*
|
||||
* Cache key format: module:type[:param:value...]
|
||||
*
|
||||
* Examples produced:
|
||||
* students:list:page:1
|
||||
* students:list:page:2:status:active
|
||||
* students:detail:id:15
|
||||
*/
|
||||
class StudentService
|
||||
{
|
||||
private const MODULE = 'students';
|
||||
|
||||
public function __construct(
|
||||
private readonly CacheServiceInterface $cache,
|
||||
) {}
|
||||
|
||||
public function getList(int $page = 1, ?string $status = null): array
|
||||
{
|
||||
$params = ['page' => $page];
|
||||
|
||||
if ($status !== null) {
|
||||
$params['status'] = $status;
|
||||
}
|
||||
|
||||
$key = CacheKey::build(self::MODULE, 'list', $params);
|
||||
|
||||
return $this->cache->remember(
|
||||
key: $key,
|
||||
callback: fn () => $this->fetchList($page, $status),
|
||||
ttl: CacheServiceInterface::TTL_MEDIUM,
|
||||
tags: [self::MODULE],
|
||||
);
|
||||
}
|
||||
|
||||
public function getDetail(int $id): array
|
||||
{
|
||||
$key = CacheKey::build(self::MODULE, 'detail', ['id' => $id]);
|
||||
|
||||
return $this->cache->remember(
|
||||
key: $key,
|
||||
callback: fn () => $this->fetchDetail($id),
|
||||
ttl: CacheServiceInterface::TTL_LONG,
|
||||
tags: [self::MODULE],
|
||||
);
|
||||
}
|
||||
|
||||
public function update(int $id, array $_data): void
|
||||
{
|
||||
// ... update DB logic ...
|
||||
|
||||
// Forget only the stale detail entry
|
||||
$this->cache->forget(
|
||||
CacheKey::build(self::MODULE, 'detail', ['id' => $id])
|
||||
);
|
||||
|
||||
// Also bust all list caches for this module
|
||||
$this->cache->clearByModule(self::MODULE);
|
||||
}
|
||||
|
||||
public function getByIds(array $ids): array
|
||||
{
|
||||
// Array params are joined with "," — key: students:bulk:ids:1,2,3
|
||||
$key = CacheKey::build(self::MODULE, 'bulk', ['ids' => $ids]);
|
||||
|
||||
return $this->cache->remember(
|
||||
key: $key,
|
||||
callback: fn () => $this->fetchByIds($ids),
|
||||
ttl: CacheServiceInterface::TTL_SHORT,
|
||||
tags: [self::MODULE],
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private DB fetch methods (replace with real Eloquent/Repository calls)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function fetchList(int $_page, ?string $_status): array
|
||||
{
|
||||
return []; // replace with Eloquent/Repository query
|
||||
}
|
||||
|
||||
private function fetchDetail(int $_id): array
|
||||
{
|
||||
return []; // replace with Eloquent/Repository query
|
||||
}
|
||||
|
||||
private function fetchByIds(array $_ids): array
|
||||
{
|
||||
return []; // replace with Eloquent/Repository query
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,10 @@ final class VsphereHostSshService
|
||||
private const SCRIPT_VERIFY_LINUX = 'verifyIPLinux.ps1';
|
||||
private const SCRIPT_VERIFY_WINDOWS = 'verifyIPWindows.ps1';
|
||||
|
||||
private const CHANGE_PWD_DIR = 'C:\\CLISave\\changePassword';
|
||||
private const SCRIPT_CHANGE_PWD_LINUX = 'changePwdLinux.ps1';
|
||||
private const SCRIPT_CHANGE_PWD_WINDOWS = 'changePwdWindows.ps1';
|
||||
|
||||
private const CONNECT_TIMEOUT = 20;
|
||||
private const MAX_RETRIES = 5;
|
||||
private const RETRY_DELAY = 5;
|
||||
@@ -91,8 +95,8 @@ final class VsphereHostSshService
|
||||
* Assign a static IP to the VM guest OS via PS1 script.
|
||||
* Calls assignIPLinux.ps1 or assignIPWindows.ps1 in C:\CLISave\AssignIP\.
|
||||
*
|
||||
* Windows script params (7): VMName IP MaskBits Gateway DNS GuestUser GuestPassword
|
||||
* Linux script params (9): VCenter VCUser VCPass VMName IP Gateway DNS GuestUser GuestPassword
|
||||
* Windows script params (7): VMName IP MaskBits Gateway DNS GuestUser GuestPassword
|
||||
* Linux script params (10): VCenter VCUser VCPass VMName IP MaskBits Gateway DNS GuestUser GuestPassword
|
||||
*
|
||||
* Script output: JSON {"status":"ok"} or {"status":"error","message":"..."}
|
||||
*/
|
||||
@@ -127,9 +131,9 @@ final class VsphereHostSshService
|
||||
$this->warg($guestPassword),
|
||||
);
|
||||
} else {
|
||||
// assignIPLinux.ps1: VCenter VCUser VCPass VMName IP Gateway DNS GuestUser GuestPassword
|
||||
// assignIPLinux.ps1: VCenter VCUser VCPass VMName IP MaskBits Gateway DNS GuestUser GuestPassword
|
||||
$remoteCmd = sprintf(
|
||||
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s %s %s %s',
|
||||
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s %s %s %s %s',
|
||||
self::ASSIGN_DIR,
|
||||
$script,
|
||||
$this->warg($vcenterHost),
|
||||
@@ -137,6 +141,7 @@ final class VsphereHostSshService
|
||||
$this->warg($vcenterPassword),
|
||||
$this->warg($vmId),
|
||||
$this->warg($ip),
|
||||
$this->warg($cidr),
|
||||
$this->warg($gateway),
|
||||
$this->warg($dns),
|
||||
$this->warg($guestUser),
|
||||
@@ -272,12 +277,78 @@ final class VsphereHostSshService
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer-initiated VM password change.
|
||||
*
|
||||
* Calls changePwdLinux.ps1 / changePwdWindows.ps1 in C:\CLISave\changePassword\.
|
||||
*
|
||||
* Param order (matches PS1 declaration):
|
||||
* $vm vm_id / vmware display name double-quoted
|
||||
* $password new password single-quoted
|
||||
* $vcenter_host vCenter host double-quoted
|
||||
* $vcenter_user vCenter user double-quoted
|
||||
* $vcenter_password vCenter password single-quoted
|
||||
* $guest_password current guest password single-quoted
|
||||
*
|
||||
* PS1 outputs "SUCCESS" or "ERROR".
|
||||
*/
|
||||
public function changePasswordCustomer(
|
||||
string $vmId,
|
||||
string $newPassword,
|
||||
string $currentGuestPassword,
|
||||
string $osFamily,
|
||||
string $vcenterHost,
|
||||
string $vcenterUser,
|
||||
string $vcenterPassword,
|
||||
): string {
|
||||
$script = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS
|
||||
? self::SCRIPT_CHANGE_PWD_WINDOWS
|
||||
: self::SCRIPT_CHANGE_PWD_LINUX;
|
||||
|
||||
$remoteCmd = sprintf(
|
||||
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s',
|
||||
self::CHANGE_PWD_DIR,
|
||||
$script,
|
||||
$this->warg($vmId),
|
||||
$this->warg($newPassword),
|
||||
$this->warg($vcenterHost),
|
||||
$this->warg($vcenterUser),
|
||||
$this->warg($vcenterPassword),
|
||||
$this->warg($currentGuestPassword),
|
||||
);
|
||||
|
||||
Log::info('VsphereHostSshService: changePasswordCustomer', [
|
||||
'vm_id' => $vmId,
|
||||
'script' => $script,
|
||||
'os' => $osFamily,
|
||||
]);
|
||||
|
||||
$output = $this->sshExec($remoteCmd);
|
||||
|
||||
Log::info('VsphereHostSshService: changePasswordCustomer output', [
|
||||
'vm_id' => $vmId,
|
||||
'output' => trim($output),
|
||||
]);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Windows cmd.exe safe argument: wrap in double quotes, escape inner double quotes.
|
||||
* Password charset excludes " and % so wrapping is always safe.
|
||||
*/
|
||||
private function warg(string $value): string
|
||||
{
|
||||
return '"' . str_replace('"', '\\"', $value) . '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-quote wrap for passwords — prevents PowerShell variable expansion on $, %, ^ etc.
|
||||
* Inside a single-quoted PS string, a literal single quote is escaped as ''.
|
||||
*/
|
||||
private function sarg(string $value): string
|
||||
{
|
||||
return "'" . str_replace("'", "''", $value) . "'";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Cache;
|
||||
|
||||
final class CacheKey
|
||||
{
|
||||
/**
|
||||
* Build a canonical cache key.
|
||||
*
|
||||
* Format: module:type[:param:value[:param:value...]]
|
||||
*
|
||||
* Examples:
|
||||
* CacheKey::build('students', 'list', ['page' => 1])
|
||||
* => "students:list:page:1"
|
||||
*
|
||||
* CacheKey::build('students', 'list', ['status' => 'active', 'page' => 2])
|
||||
* => "students:list:page:2:status:active" (sorted by key)
|
||||
*
|
||||
* CacheKey::build('students', 'detail', ['id' => 15])
|
||||
* => "students:detail:id:15"
|
||||
*/
|
||||
public static function build(string $module, string $type, array $params = []): string
|
||||
{
|
||||
ksort($params);
|
||||
|
||||
$parts = [$module, $type];
|
||||
|
||||
foreach ($params as $param => $value) {
|
||||
$parts[] = (string) $param;
|
||||
$parts[] = is_array($value) ? implode(',', $value) : (string) $value;
|
||||
}
|
||||
|
||||
return implode(':', $parts);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user