From 6bb4a47bfbf70c7d656112ab28390862aa17d9cd Mon Sep 17 00:00:00 2001 From: baodev0 Date: Fri, 29 May 2026 09:28:08 +0700 Subject: [PATCH] Add stubs for various Laravel components and scaffolding - Created stubs for JavaScript and SASS assets. - Added stubs for channel, command, component, controller, event, factory, job, listener, mail, middleware, migration, model, notification, observer, policy, request, resource, route provider, seeder, and validation rule. - Implemented scaffolding for configuration, service provider, and Vite integration. - Included example views for module structure. - Introduced a module loader for Vite to handle asset paths dynamically. --- .DS_Store | Bin 10244 -> 10244 bytes .claude/settings.local.json | 10 +- .../Admin/Invoice/InvoiceTestController.php | 6 +- .../Api/Admin/Setting/SettingController.php | 16 + .../AdminTransactionController.php | 46 + .../Api/Service/VmRebuildController.php | 69 +- .../Transaction/ManualDepositController.php | 59 + .../Controllers/Backend/DomainController.php | 378 -- .../Controllers/Backend/RecordController.php | 312 -- .../Controllers/Backend/ServerController.php | 204 - .../Controllers/rest/ModuleController.php | 256 -- .../Api/Service/VpsServiceResource.php | 6 + app/Http/Services/vCenter.php | 3351 ++--------------- app/Http/Services/vCenter/Client.php | 25 + .../vCenter/PowerManagementService.php | 91 + .../Services/vCenter/PowerShellService.php | 70 + app/Http/Services/vCenter/ResourceService.php | 6 + app/Http/Services/vCenter/SnapshotService.php | 135 +- .../vCenter/VirtualMachineService.php | 379 +- .../VirtualMachine/Create/AssignIPJob.php | 245 -- app/Jobs/VirtualMachine/Sync/SyncFolder.php | 11 +- .../ManualDepositPendingNotification.php | 36 + app/Mail/Customer/ManualDepositStatus.php | 73 + app/Providers/MailConfigServiceProvider.php | 8 +- app/Providers/RouteServiceProvider.php | 4 - .../ServiceClient/ServiceClientRepository.php | 5 +- app/Services/InvoicePdfService.php | 4 +- composer.json | 1 + composer.lock | 87 +- config/modules.php | 218 ++ database/seeders/DomainProductSeeder.php | 117 + public/.DS_Store | Bin 8196 -> 8196 bytes resources/views/.DS_Store | Bin 6148 -> 0 bytes .../admin/manual-deposit-pending.blade.php | 98 + .../customer/manual-deposit-status.blade.php | 74 + resources/views/pdf/invoice.blade.php | 112 +- resources/views/welcome.blade.php | 11 - routes/admin.php | 58 - routes/api.php | 2 + ...a4bHvammlvlmeH0v63NsHq4qr3wSZhgKajWHJf.png | Bin 0 -> 62744 bytes stubs/nwidart-stubs/assets/js/app.stub | 0 stubs/nwidart-stubs/assets/sass/app.stub | 0 stubs/nwidart-stubs/channel.stub | 24 + stubs/nwidart-stubs/command.stub | 56 + stubs/nwidart-stubs/component-class.stub | 25 + stubs/nwidart-stubs/component-view.stub | 3 + stubs/nwidart-stubs/composer.stub | 31 + stubs/nwidart-stubs/controller-api.stub | 62 + stubs/nwidart-stubs/controller-plain.stub | 9 + stubs/nwidart-stubs/controller.stub | 67 + stubs/nwidart-stubs/event.stub | 26 + stubs/nwidart-stubs/factory.stub | 22 + stubs/nwidart-stubs/feature-test.stub | 20 + stubs/nwidart-stubs/job-queued.stub | 30 + stubs/nwidart-stubs/job.stub | 27 + stubs/nwidart-stubs/json.stub | 11 + stubs/nwidart-stubs/listener-duck.stub | 25 + stubs/nwidart-stubs/listener-queued-duck.stub | 27 + stubs/nwidart-stubs/listener-queued.stub | 28 + stubs/nwidart-stubs/listener.stub | 26 + stubs/nwidart-stubs/mail.stub | 29 + stubs/nwidart-stubs/middleware.stub | 17 + stubs/nwidart-stubs/migration/add.stub | 28 + stubs/nwidart-stubs/migration/create.stub | 28 + stubs/nwidart-stubs/migration/delete.stub | 28 + stubs/nwidart-stubs/migration/drop.stub | 28 + stubs/nwidart-stubs/migration/plain.stub | 24 + stubs/nwidart-stubs/model.stub | 22 + stubs/nwidart-stubs/notification.stub | 48 + stubs/nwidart-stubs/observer.stub | 48 + stubs/nwidart-stubs/package.stub | 15 + stubs/nwidart-stubs/policy.plain.stub | 18 + stubs/nwidart-stubs/provider.stub | 24 + stubs/nwidart-stubs/request.stub | 26 + stubs/nwidart-stubs/resource-collection.stub | 16 + stubs/nwidart-stubs/resource.stub | 16 + stubs/nwidart-stubs/route-provider.stub | 59 + stubs/nwidart-stubs/routes/api.stub | 19 + stubs/nwidart-stubs/routes/web.stub | 19 + stubs/nwidart-stubs/rule.implicit.stub | 22 + stubs/nwidart-stubs/rule.stub | 17 + stubs/nwidart-stubs/scaffold/config.stub | 5 + stubs/nwidart-stubs/scaffold/provider.stub | 114 + stubs/nwidart-stubs/seeder.stub | 16 + stubs/nwidart-stubs/unit-test.stub | 20 + stubs/nwidart-stubs/views/index.stub | 7 + stubs/nwidart-stubs/views/master.stub | 29 + stubs/nwidart-stubs/vite.stub | 26 + vite-module-loader.js | 45 + 89 files changed, 3355 insertions(+), 4560 deletions(-) delete mode 100644 app/Http/Controllers/Backend/DomainController.php delete mode 100644 app/Http/Controllers/Backend/RecordController.php delete mode 100644 app/Http/Controllers/Backend/ServerController.php delete mode 100644 app/Http/Controllers/rest/ModuleController.php delete mode 100644 app/Jobs/VirtualMachine/Create/AssignIPJob.php create mode 100644 app/Mail/Admin/ManualDepositPendingNotification.php create mode 100644 app/Mail/Customer/ManualDepositStatus.php create mode 100644 config/modules.php create mode 100644 database/seeders/DomainProductSeeder.php delete mode 100644 resources/views/.DS_Store create mode 100644 resources/views/emails/admin/manual-deposit-pending.blade.php create mode 100644 resources/views/emails/customer/manual-deposit-status.blade.php delete mode 100644 resources/views/welcome.blade.php delete mode 100644 routes/admin.php create mode 100644 storage/app/public/system-config/vqa4bHvammlvlmeH0v63NsHq4qr3wSZhgKajWHJf.png create mode 100644 stubs/nwidart-stubs/assets/js/app.stub create mode 100644 stubs/nwidart-stubs/assets/sass/app.stub create mode 100644 stubs/nwidart-stubs/channel.stub create mode 100644 stubs/nwidart-stubs/command.stub create mode 100644 stubs/nwidart-stubs/component-class.stub create mode 100644 stubs/nwidart-stubs/component-view.stub create mode 100644 stubs/nwidart-stubs/composer.stub create mode 100644 stubs/nwidart-stubs/controller-api.stub create mode 100644 stubs/nwidart-stubs/controller-plain.stub create mode 100644 stubs/nwidart-stubs/controller.stub create mode 100644 stubs/nwidart-stubs/event.stub create mode 100644 stubs/nwidart-stubs/factory.stub create mode 100644 stubs/nwidart-stubs/feature-test.stub create mode 100644 stubs/nwidart-stubs/job-queued.stub create mode 100644 stubs/nwidart-stubs/job.stub create mode 100644 stubs/nwidart-stubs/json.stub create mode 100644 stubs/nwidart-stubs/listener-duck.stub create mode 100644 stubs/nwidart-stubs/listener-queued-duck.stub create mode 100644 stubs/nwidart-stubs/listener-queued.stub create mode 100644 stubs/nwidart-stubs/listener.stub create mode 100644 stubs/nwidart-stubs/mail.stub create mode 100644 stubs/nwidart-stubs/middleware.stub create mode 100644 stubs/nwidart-stubs/migration/add.stub create mode 100644 stubs/nwidart-stubs/migration/create.stub create mode 100644 stubs/nwidart-stubs/migration/delete.stub create mode 100644 stubs/nwidart-stubs/migration/drop.stub create mode 100644 stubs/nwidart-stubs/migration/plain.stub create mode 100644 stubs/nwidart-stubs/model.stub create mode 100644 stubs/nwidart-stubs/notification.stub create mode 100644 stubs/nwidart-stubs/observer.stub create mode 100644 stubs/nwidart-stubs/package.stub create mode 100644 stubs/nwidart-stubs/policy.plain.stub create mode 100644 stubs/nwidart-stubs/provider.stub create mode 100644 stubs/nwidart-stubs/request.stub create mode 100644 stubs/nwidart-stubs/resource-collection.stub create mode 100644 stubs/nwidart-stubs/resource.stub create mode 100644 stubs/nwidart-stubs/route-provider.stub create mode 100644 stubs/nwidart-stubs/routes/api.stub create mode 100644 stubs/nwidart-stubs/routes/web.stub create mode 100644 stubs/nwidart-stubs/rule.implicit.stub create mode 100644 stubs/nwidart-stubs/rule.stub create mode 100644 stubs/nwidart-stubs/scaffold/config.stub create mode 100644 stubs/nwidart-stubs/scaffold/provider.stub create mode 100644 stubs/nwidart-stubs/seeder.stub create mode 100644 stubs/nwidart-stubs/unit-test.stub create mode 100644 stubs/nwidart-stubs/views/index.stub create mode 100644 stubs/nwidart-stubs/views/master.stub create mode 100644 stubs/nwidart-stubs/vite.stub create mode 100644 vite-module-loader.js diff --git a/.DS_Store b/.DS_Store index 90352ad74b673dfb648675e06ddbaf0d77c13ea9..af256511ea937cb48d1ea4debce7c5b51f4ad76b 100644 GIT binary patch delta 273 zcmZn(XbIS`QPka+fq{XIp@5;3A&DV}A(J6FH{Zo2DJMS(D9&MZNc_0cljDwJlI$`k8Lx%!YWqUxcCYq0M%fF+XWPuKu!@y74!*(&0B|NvNdN!< diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3b4d3de..b7de7ef 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -23,7 +23,15 @@ "Bash(chmod +x *)", "Bash(composer check-platform-reqs *)", "Bash(composer config *)", - "Bash(composer update *)" + "Bash(composer update *)", + "Bash(redis-cli ping *)", + "Bash(redis-cli llen *)", + "Bash(redis-cli lrange *)", + "Bash(redis-cli keys *)", + "Bash(redis-cli --no-auth-warning keys \"*\")", + "Bash(redis-cli --no-auth-warning llen \"the_gioi_so_horizon:queue:default\")", + "Bash(redis-cli --no-auth-warning lrange \"the_gioi_so_horizon:queue:default\" 0 4)", + "Bash(redis-cli *)" ] } } diff --git a/app/Http/Controllers/Api/Admin/Invoice/InvoiceTestController.php b/app/Http/Controllers/Api/Admin/Invoice/InvoiceTestController.php index beda088..2ea4c58 100644 --- a/app/Http/Controllers/Api/Admin/Invoice/InvoiceTestController.php +++ b/app/Http/Controllers/Api/Admin/Invoice/InvoiceTestController.php @@ -124,6 +124,8 @@ class InvoiceTestController extends Controller 'companyAddress' => $settingValue->address ?? '', 'companyPhone' => $settingValue->phone ?? '', 'companyEmail' => $settingValue->email ?? '', - ])->stream($invoice->invoice_number . '.pdf'); + ]) + ->setPaper('a4', 'portrait') + ->stream($invoice->invoice_number . '.pdf'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/Admin/Setting/SettingController.php b/app/Http/Controllers/Api/Admin/Setting/SettingController.php index a3c6eef..67961fc 100644 --- a/app/Http/Controllers/Api/Admin/Setting/SettingController.php +++ b/app/Http/Controllers/Api/Admin/Setting/SettingController.php @@ -11,6 +11,7 @@ use App\Support\Cache\CacheKey; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Validator; class SettingController extends Controller @@ -99,4 +100,19 @@ class SettingController extends Controller 'message' => 'Cập nhật cài đặt thành công.', ]); } + + public function uploadMedia(Request $request): JsonResponse + { + $request->validate([ + 'file' => 'required|file|mimes:jpg,jpeg,png,webp,svg|max:5120', + ]); + + $path = $request->file('file')->store('system-config', 'public'); + + return response()->json([ + 'error' => false, + 'url' => Storage::disk('public')->url($path), + 'path' => $path, + ]); + } } diff --git a/app/Http/Controllers/Api/Admin/Transaction/AdminTransactionController.php b/app/Http/Controllers/Api/Admin/Transaction/AdminTransactionController.php index b2a550c..3486b5c 100644 --- a/app/Http/Controllers/Api/Admin/Transaction/AdminTransactionController.php +++ b/app/Http/Controllers/Api/Admin/Transaction/AdminTransactionController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Admin\Transaction; use App\Enums\TransactionStatus; use App\Http\Controllers\Controller; +use App\Mail\Customer\ManualDepositStatus; use App\Models\Notification; use App\Models\Log; use App\Models\Transaction; @@ -11,6 +12,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log as FacadesLog; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; class AdminTransactionController extends Controller @@ -126,6 +128,28 @@ class AdminTransactionController extends Controller ]); DB::commit(); + + try { + Mail::to($customer->email)->send(new ManualDepositStatus([ + 'customer_id' => $customer->id, + 'customer_email' => $customer->email, + 'customer_name' => $customer->fullname, + 'transaction_code' => $transaction->public_code, + 'amount' => $transaction->amount, + 'status_label' => 'Đã duyệt', + 'status_color' => '#15803d', + 'status_bg' => '#f0fdf4', + 'subject' => 'Yêu cầu nạp tiền đã được duyệt - ' . $transaction->public_code, + 'status_message' => 'Yêu cầu nạp tiền của bạn đã được duyệt. Số tiền đã được cộng vào tài khoản và có thể sử dụng ngay.', + 'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'), + 'processed_date' => now()->format('d/m/Y H:i:s'), + ])); + } catch (\Throwable $e) { + FacadesLog::error('Không thể gửi email duyệt nạp tiền thủ công: ' . $e->getMessage(), [ + 'transaction_code' => $transaction->public_code, + 'customer_email' => $customer->email, + ]); + } } catch (\Exception $e) { DB::rollBack(); FacadesLog::error('AdminTransactionController::approve error: ' . $e->getMessage()); @@ -172,6 +196,28 @@ class AdminTransactionController extends Controller ]); DB::commit(); + + try { + Mail::to($customer->email)->send(new ManualDepositStatus([ + 'customer_id' => $customer->id, + 'customer_email' => $customer->email, + 'customer_name' => $customer->fullname, + 'transaction_code' => $transaction->public_code, + 'amount' => $transaction->amount, + 'status_label' => 'Không phù hợp', + 'status_color' => '#b91c1c', + 'status_bg' => '#fef2f2', + 'subject' => 'Yêu cầu nạp tiền không phù hợp - ' . $transaction->public_code, + 'status_message' => 'Yêu cầu nạp tiền của bạn chưa phù hợp để được duyệt. Vui lòng kiểm tra lại chứng từ hoặc liên hệ bộ phận hỗ trợ để được hướng dẫn thêm.', + 'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'), + 'processed_date' => now()->format('d/m/Y H:i:s'), + ])); + } catch (\Throwable $e) { + FacadesLog::error('Không thể gửi email từ chối nạp tiền thủ công: ' . $e->getMessage(), [ + 'transaction_code' => $transaction->public_code, + 'customer_email' => $customer->email, + ]); + } } catch (\Exception $e) { DB::rollBack(); FacadesLog::error('AdminTransactionController::reject error: ' . $e->getMessage()); diff --git a/app/Http/Controllers/Api/Service/VmRebuildController.php b/app/Http/Controllers/Api/Service/VmRebuildController.php index 88d371b..c62d9a3 100644 --- a/app/Http/Controllers/Api/Service/VmRebuildController.php +++ b/app/Http/Controllers/Api/Service/VmRebuildController.php @@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api\Service; use App\Http\Controllers\Controller; use App\Http\Controllers\Api\Service\Concerns\ResolvesVm; use App\Http\Services\vCenter; -use App\Models\ModuleVmware; use App\Services\ActivityLogger; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -19,20 +18,18 @@ class VmRebuildController extends Controller public function options(Request $request): JsonResponse { - $productId = (int) $request->query('product'); + $productId = $request->query('product'); if (!$productId) { return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422); } - $vm = ModuleVmware::with(['order.orderItems.product.osVersions.osGroup']) - ->find($productId); - if (!$vm) { - return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); - } - if ((int) $vm->customer_id !== (int) $request->user()->id) { - return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + $vm = $this->resolveVmById($productId, $request); + if ($vm instanceof JsonResponse) { + return $vm; } + $vm->loadMissing(['order.orderItems.product.osVersions.osGroup']); + $product = optional($vm->order?->orderItems?->first())->product; if (!$product) { return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404); @@ -63,19 +60,17 @@ class VmRebuildController extends Controller { $customer = $request->user(); $payload = $request->validate([ - 'product' => ['required', 'integer'], + 'product' => ['required'], 'os_id' => ['required', 'integer', 'exists:os,id'], ]); - $vm = ModuleVmware::with(['order.orderItems.product', 'vCenter', 'ipAddress.host.datacenter.vCenter']) - ->find($payload['product']); - if (!$vm) { - return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); - } - if ((int) $vm->customer_id !== (int) $customer->id) { - return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + $vm = $this->resolveVmById($payload['product'], $request); + if ($vm instanceof JsonResponse) { + return $vm; } + $vm->loadMissing(['order.orderItems.product', 'vCenter', 'ipAddress.host.datacenter.vCenter']); + $product = optional($vm->order?->orderItems?->first())->product; if (!$product) { return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404); @@ -123,20 +118,18 @@ class VmRebuildController extends Controller public function addonOptions(Request $request): JsonResponse { - $productId = (int) $request->query('product'); + $productId = $request->query('product'); if (!$productId) { return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422); } - $vm = ModuleVmware::with(['order.orderItems.product.addons']) - ->find($productId); - if (!$vm) { - return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); - } - if ((int) $vm->customer_id !== (int) $request->user()->id) { - return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + $vm = $this->resolveVmById($productId, $request); + if ($vm instanceof JsonResponse) { + return $vm; } + $vm->loadMissing(['order.orderItems.product.addons']); + $product = optional($vm->order?->orderItems?->first())->product; if (!$product) { return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404); @@ -145,18 +138,23 @@ class VmRebuildController extends Controller $addons = $product->addons()->where('status', true)->get()->map(function ($addon) { return [ 'id' => $addon->id, + 'type' => $addon->type instanceof \BackedEnum ? $addon->type->value : (string) $addon->type, 'code' => strtolower((string) $addon->code), 'name' => $addon->name, 'price' => (float) $addon->price, 'min' => (int) ($addon->min ?? 0), 'max' => (int) ($addon->max ?? 0), 'step' => (int) ($addon->step ?? 1), + 'prefix' => $addon->prefix, + 'input_type' => $addon->input_type ?? 'number', + 'sort_order' => (int) ($addon->sort_order ?? 0), ]; }); return response()->json([ 'error' => false, - 'data' => [ + 'data' => $addons->values(), + 'meta' => [ 'product_id' => $product->id, 'product_name' => $product->name, 'base_specs' => [ @@ -166,7 +164,6 @@ class VmRebuildController extends Controller 'bandwidth' => (int) ($product->bandwidth ?? 0), 'gpu' => $product->gpu, ], - 'addons' => $addons->values(), ], ]); } @@ -175,23 +172,21 @@ class VmRebuildController extends Controller { $customer = $request->user(); $payload = $request->validate([ - 'product' => ['required', 'integer'], + 'product' => ['required'], 'cpu' => ['nullable', 'integer', 'min:0'], 'ram' => ['nullable', 'integer', 'min:0'], 'disk' => ['nullable', 'integer', 'min:0'], 'bandwidth' => ['nullable', 'integer', 'min:0'], - 'gpu' => ['nullable', 'string', 'max:255'], + 'gpu' => ['nullable'], ]); - $vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter']) - ->find($payload['product']); - if (!$vm) { - return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404); - } - if ((int) $vm->customer_id !== (int) $customer->id) { - return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403); + $vm = $this->resolveVmById($payload['product'], $request); + if ($vm instanceof JsonResponse) { + return $vm; } + $vm->loadMissing(['vCenter', 'ipAddress.host.datacenter.vCenter']); + $vcenter = $vm->vCenter ?? $vm->ipAddress?->host?->datacenter?->vCenter; if (!$vcenter || !$serviceVCenter->loginServer($vcenter)) { return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vCenter'], 400); @@ -235,4 +230,4 @@ class VmRebuildController extends Controller ], ]); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Api/Transaction/ManualDepositController.php b/app/Http/Controllers/Api/Transaction/ManualDepositController.php index e504d30..b4b404b 100644 --- a/app/Http/Controllers/Api/Transaction/ManualDepositController.php +++ b/app/Http/Controllers/Api/Transaction/ManualDepositController.php @@ -6,12 +6,16 @@ use App\Enums\TransactionStatus; use App\Http\Controllers\Controller; use App\Http\Requests\DepositRequest; use App\Http\Services\Telegram; +use App\Mail\Admin\ManualDepositPendingNotification; +use App\Mail\Customer\ManualDepositStatus; use App\Models\Log; +use App\Models\Setting; use App\Models\Transaction; use App\Services\ActivityLogger; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; @@ -196,6 +200,7 @@ class ManualDepositController extends Controller try { (new Telegram())->SendManualDepositPendingApproval($user, $transaction); + $this->sendManualDepositAdminEmail($user, $transaction, $request); } catch (\Exception) { // Không để lỗi Telegram ảnh hưởng response } @@ -214,6 +219,27 @@ class ManualDepositController extends Controller newValues: ['public_code' => $transaction->public_code, 'amount' => $transaction->amount, 'has_proof' => true], ); + try { + Mail::to($user->email)->send(new ManualDepositStatus([ + 'customer_id' => $user->id, + 'customer_email' => $user->email, + 'customer_name' => $user->fullname, + 'transaction_code' => $transaction->public_code, + 'amount' => $transaction->amount, + 'status_label' => 'Đang chờ duyệt', + 'status_color' => '#f16723', + 'status_bg' => '#fff7ed', + 'subject' => 'Yêu cầu nạp tiền đang chờ duyệt - ' . $transaction->public_code, + 'status_message' => 'Yêu cầu nạp tiền của bạn đã được ghi nhận. Chúng tôi sẽ kiểm tra chứng từ và phản hồi kết quả trong thời gian sớm nhất.', + 'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'), + ])); + } catch (\Throwable $e) { + \Log::error('Không thể gửi email trạng thái nạp tiền thủ công: ' . $e->getMessage(), [ + 'transaction_code' => $transaction->public_code, + 'customer_email' => $user->email, + ]); + } + return response()->json([ 'error' => false, 'message' => 'Yêu cầu nạp tiền đã được ghi nhận, vui lòng chờ admin xác nhận', @@ -239,4 +265,37 @@ class ManualDepositController extends Controller 'path' => $path, ]); } + + private function sendManualDepositAdminEmail($customer, Transaction $transaction, Request $request): void + { + try { + $setting = Setting::where('key', 'setting')->first(); + $settingValue = $setting ? json_decode((string) $setting->value, true) : []; + $adminEmail = $settingValue['admin_email'] ?? null; + + if (empty($adminEmail)) { + \Log::warning('Manual deposit admin email not sent: admin_email not configured.', [ + 'transaction_code' => $transaction->public_code, + ]); + return; + } + + Mail::to($adminEmail)->send(new ManualDepositPendingNotification([ + 'company_name' => $settingValue['company'] ?? config('app.name', 'Portal Admin'), + 'customer_name' => $customer->fullname ?: trim(($customer->firstname ?? '') . ' ' . ($customer->lastname ?? '')), + 'customer_email' => $customer->email, + 'customer_phone' => $customer->phone ?? null, + 'client_ip' => $request->ip(), + 'transaction_code' => $transaction->public_code, + 'amount' => $transaction->amount, + 'proof_image_url' => $transaction->proof_image ? Storage::disk('public')->url($transaction->proof_image) : null, + 'created_at' => now()->format('d/m/Y H:i:s'), + ])); + } catch (\Throwable $e) { + \Log::error('Không thể gửi email admin thông báo nạp tiền chờ duyệt: ' . $e->getMessage(), [ + 'transaction_code' => $transaction->public_code, + 'customer_email' => $customer->email ?? null, + ]); + } + } } diff --git a/app/Http/Controllers/Backend/DomainController.php b/app/Http/Controllers/Backend/DomainController.php deleted file mode 100644 index 9941a7d..0000000 --- a/app/Http/Controllers/Backend/DomainController.php +++ /dev/null @@ -1,378 +0,0 @@ -domainRepository->filter([], ['users', 'server'])->map(function ($domain) { - return [ - 'id' => $domain->id, - 'name' => $domain->domain_name, - 'server' => $domain->server ? $domain->server->ip_address : '', - 'user' => $domain->users->pluck('name')->implode(', '), - 'checkbox' => '', - 'created_at' => Carbon::parse($domain->created_at)->format('Y-m-d H:i:s'), - 'action' => $this->domainActionButtons($domain), - ]; - }); - - return response()->json(['data' => $domains]); - } - - public function create() - { - $title = 'Add Domain'; - return view('Backend.domains.create', compact('title')); - } - - public function store(DomainRequest $request) - { - $validated = $request->validated(); - - $isCreated = $this->domainRepository->findDomain($validated['domain_name']); - if (!empty($isCreated)) { - return response()->json([ - 'success' => false, - 'message' => "Domain '{$validated['domain_name']}' đã tồn tại trong hệ thống!", - ]); - } - - DB::beginTransaction(); - try { - $userIds = $validated['user_id'] ?? []; - $domainData = collect($validated)->except('user_id')->toArray(); - - $domain = $this->domainRepository->create($domainData); - $domain->users()->attach($userIds); - $domain->save(); - - try { - $server = $this->serverService->findServerByID($domain->dns_manager_server_id); - - if (!$server) { - throw new Exception("Không tìm thấy server với ID: {$domain->dns_manager_server_id}"); - } - - $existingDomainInfo = $this->serverService->getDomainInfo($domain->domain_name); - - if ($existingDomainInfo) { - $this->domainRepository->update($domain->id, [ - 'is_created' => 1, - 'plesk_id' => $existingDomainInfo['id'], - ]); - } else { - $response = $this->serverService->createDomain($domain->toArray()); - - if (!$response || !isset($response['id'])) { - throw new Exception('Không thể tạo domain trên server. Vui lòng kiểm tra lại thông tin server.'); - } - - $this->domainRepository->update($domain->id, [ - 'is_created' => 1, - 'plesk_id' => $response['id'], - ]); - } - } catch (Exception $domainError) { - Log::error('Lỗi khi tạo domain: ' . $domainError->getMessage()); - } - - DB::commit(); - - $domain = $domain->refresh(); - $message = $domain->is_created == 1 && $domain->plesk_id - ? "Domain '{$domain->domain_name}' đã được xử lý thành công trên server!" - : "Domain '{$domain->domain_name}' đã được lưu vào database nhưng chưa thể xử lý trên server."; - - return response()->json(['success' => true, 'message' => $message, 'url' => route('admin.domain.index')]); - } catch (Exception $error) { - DB::rollBack(); - return response()->json(['success' => false, 'message' => 'Có lỗi xảy ra: ' . $error->getMessage()]); - } - } - - public function edit(int $id) - { - $domain = $this->domainRepository->find($id); - if (empty($domain)) { - return back()->withErrors("Không tìm thấy domain với ID: {$id}"); - } - - $title = 'Edit domain'; - return view('Backend.domains.edit', compact('title', 'domain')); - } - - public function update(DomainRequest $request, int $id) - { - $domain = $this->domainRepository->find($id); - - if (empty($domain)) { - return response()->json(['success' => false, 'message' => "Không tìm thấy domain với ID: {$id}"]); - } - - $validated = $request->validated(); - - DB::beginTransaction(); - try { - $userIds = $validated['user_id'] ?? []; - $domainData = collect($validated)->except('user_id')->toArray(); - - $this->domainRepository->update($id, $domainData); - $domain->users()->sync($userIds); - - try { - $updatedDomain = $this->domainRepository->find($id); - $server = $this->serverService->findServerByID($updatedDomain->dns_manager_server_id); - - if (!$server) { - throw new Exception("Không tìm thấy server với ID: {$updatedDomain->dns_manager_server_id}"); - } - - $existingDomainInfo = $this->serverService->getDomainInfo($updatedDomain->domain_name); - - if ($existingDomainInfo) { - $this->serverService->updateDomain($existingDomainInfo['id'], $updatedDomain->toArray()); - $this->domainRepository->update($updatedDomain->id, [ - 'is_created' => 1, - 'plesk_id' => $existingDomainInfo['id'], - ]); - } else { - $response = $this->serverService->createDomain($updatedDomain->toArray()); - - if (!$response || !isset($response['id'])) { - throw new Exception('Không thể tạo domain trên server.'); - } - - $this->domainRepository->update($updatedDomain->id, [ - 'is_created' => 1, - 'plesk_id' => $response['id'], - ]); - } - } catch (Exception $domainError) { - Log::error('Lỗi khi cập nhật domain: ' . $domainError->getMessage()); - } - - DB::commit(); - - $updatedDomain = $this->domainRepository->find($id); - $message = $updatedDomain->is_created == 1 && $updatedDomain->plesk_id - ? "Domain '{$updatedDomain->domain_name}' đã được cập nhật thành công trên server!" - : "Domain '{$updatedDomain->domain_name}' đã được cập nhật trong database nhưng chưa thể xử lý trên server."; - - return response()->json(['success' => true, 'message' => $message, 'url' => route('admin.domain.index')]); - } catch (Exception $error) { - DB::rollBack(); - return response()->json(['success' => false, 'message' => 'Có lỗi xảy ra: ' . $error->getMessage()]); - } - } - - public function record(int $id = 0) - { - $domain = $this->domainRepository->findWithRelations($id, ['record']); - - if (empty($domain)) { - return back()->withErrors("Không tìm thấy domain với ID: {$id}"); - } - - $title = strtoupper($domain->domain_name) . ' Records'; - return view('Backend.domains.view', compact('domain', 'title', 'id')); - } - - public function destroy(int $id) - { - $domain = $this->domainRepository->find($id); - if (!$domain) { - return response()->json(['success' => false, 'message' => "Không tìm thấy domain với ID: {$id}"]); - } - - try { - if ($domain->is_created !== 1 || !$domain->plesk_id) { - $this->domainRepository->delete($domain->id); - return response()->json([ - 'success' => true, - 'message' => "Domain '{$domain->domain_name}' đã được xóa khỏi database.", - 'url' => route('admin.domain.index'), - ]); - } - - $server = $this->serverService->findServerByID($domain->dns_manager_server_id); - - if (!$server) { - return response()->json(['success' => false, 'message' => "Không tìm thấy server với ID: {$domain->dns_manager_server_id}"]); - } - - $removed = $this->serverService->removeDomain($domain->plesk_id); - - if (!$removed) { - return response()->json(['success' => false, 'message' => "Không thể xóa domain '{$domain->domain_name}' trên server."]); - } - - $this->domainRepository->delete($domain->id); - $this->clearDomainCaches($domain); - - return response()->json([ - 'success' => true, - 'message' => "Domain '{$domain->domain_name}' đã được xóa thành công khỏi server và database!", - 'url' => route('admin.domain.index'), - ]); - } catch (Exception $error) { - return response()->json(['success' => false, 'message' => 'Có lỗi xảy ra khi xóa domain: ' . $error->getMessage()]); - } - } - - public function destroyCustom(DomainRemoveRequest $request) - { - $request->validated(); - - $domains = $this->domainRepository->findWhereIn('id', $request->get('ids')); - - if (!$domains || $domains->isEmpty()) { - return response()->json(['success' => false, 'message' => 'Không tìm thấy domain nào.']); - } - - $successCount = 0; - $errorCount = 0; - $errors = []; - - foreach ($domains as $domain) { - try { - if ($domain->is_created !== 1 || !$domain->plesk_id) { - $this->domainRepository->delete($domain->id); - $successCount++; - continue; - } - - $server = $this->serverService->findServerByID($domain->dns_manager_server_id); - - if (!$server) { - $errors[] = "Domain '{$domain->domain_name}': Không tìm thấy server"; - $errorCount++; - continue; - } - - $removed = $this->serverService->removeDomain($domain->plesk_id); - - if (!$removed) { - $errors[] = "Domain '{$domain->domain_name}': Không thể xóa trên server"; - $errorCount++; - continue; - } - - $this->domainRepository->delete($domain->id); - $this->clearDomainCaches($domain); - $successCount++; - } catch (Exception $error) { - $errors[] = "Domain '{$domain->domain_name}': " . $error->getMessage(); - $errorCount++; - } - } - - if ($successCount > 0 && $errorCount === 0) { - $message = "Đã xóa thành công {$successCount} domain!"; - } elseif ($successCount > 0) { - $message = "Đã xóa thành công {$successCount} domain, {$errorCount} domain gặp lỗi."; - } else { - $message = 'Không thể xóa domain nào. Vui lòng kiểm tra lại.'; - } - - return response()->json([ - 'success' => $errorCount === 0, - 'message' => $message, - 'details' => ['success_count' => $successCount, 'error_count' => $errorCount, 'errors' => $errors], - 'url' => route('admin.domain.index'), - ]); - } - - private function domainActionButtons($domain): string - { - $editUrl = route('admin.domain.edit', ['id' => $domain->id]); - $recordUrl = route('admin.domain.record', ['id' => $domain->id]); - $deleteUrl = route('admin.domain.destroy', ['id' => $domain->id]); - - return << - - - - - - - Edit - - - - - - - - - Records - - - - - - - - - Delete - - HTML; - } - - private function clearDomainCaches($domain): void - { - try { - // 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(CacheKey::build('domain', 'user-list', ['user_id' => $user->id])); - } - } - - if ($domain->dns_manager_server_id) { - 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()); - } - } -} diff --git a/app/Http/Controllers/Backend/RecordController.php b/app/Http/Controllers/Backend/RecordController.php deleted file mode 100644 index a3f45a7..0000000 --- a/app/Http/Controllers/Backend/RecordController.php +++ /dev/null @@ -1,312 +0,0 @@ - [ - 'name' => 'required', - 'type' => 'required', - 'ttl' => 'required', - 'value' => 'required|ip|ipv4', - 'plesk_id' => 'nullable', - ], - 'AAAA' => [ - 'name' => 'required', - 'type' => 'required', - 'ttl' => 'required', - 'value' => 'required|ip|ipv6', - 'plesk_id' => 'nullable', - ], - 'CNAME' => [ - 'name' => 'required', - 'type' => 'required', - 'ttl' => 'required', - 'value' => 'required', - 'plesk_id' => 'nullable', - ], - 'MX' => [ - 'name' => 'required', - 'type' => 'required', - 'ttl' => 'required', - 'value' => 'required', - 'priority' => 'required', - 'plesk_id' => 'nullable', - ], - 'TXT' => [ - 'name' => 'required', - 'type' => 'required', - 'ttl' => 'required', - 'value' => 'required', - 'plesk_id' => 'nullable', - ], - 'CAA' => [ - 'name' => 'required', - 'type' => 'required', - 'ttl' => 'required', - 'value' => 'required', - 'plesk_id' => 'nullable', - ], - 'SRV' => [ - 'type' => 'required', - 'ttl' => 'required', - 'value' => 'required', - 'service_name' => 'required', - 'protocol' => 'required', - 'weight' => 'required', - 'port' => 'required', - 'priority' => 'required', - 'plesk_id' => 'nullable', - ], - ]; - - public function __construct( - public DnsDomainRepositoryInterface $domainRepository, - public DnsRecordRepositoryInterface $recordRepository, - public ServerService $serverService - ) {} - - public function index() - { - $title = 'All Records'; - return view('Backend.records.index', compact('title')); - } - - public function getAll() - { - $records = $this->recordRepository->filter([], ['domain'])->map(function ($record) { - return [ - 'id' => $record->id, - 'name' => Str::limit($record->name, 20, '...'), - 'type' => $record->type, - 'value' => Str::limit($record->value, 20, '...'), - 'domain' => $record->domain ? $record->domain->domain_name : '', - 'checkbox' => '', - 'created_at' => Carbon::parse($record->created_at)->format('Y-m-d H:i:s'), - 'action' => $this->recordActionButtons($record), - ]; - }); - - return response()->json(['data' => $records]); - } - - public function create(int $id) - { - $title = 'Add Records'; - return view('Backend.records.create', compact('title', 'id')); - } - - public function store(Request $request, int $id = 0) - { - $domain = $this->domainRepository->find($id); - if (empty($domain)) { - return response()->json(['success' => false, 'message' => 'These credentials do not match our records.']); - } - - $validated = Validator::make($request->all(), $this->rules[$request->get('type')] ?? []); - - if ($validated->fails()) { - return response()->json(['success' => false, 'message' => $validated->errors()->first()]); - } - - $request->merge(['dns_manager_domain_id' => $id, 'domain' => $domain->domain_name]); - - if (empty($request->get('plesk_id'))) { - $this->serverService->findServerByID($domain->dns_manager_server_id); - $this->serverService->handlePayload($request->all()); - $record = $this->serverService->addRecord(); - - if (!empty($record) && $record['error']) { - return response()->json(['success' => false, 'message' => $record['message'] ?? 'Failed to add record on server.']); - } - } - - DB::beginTransaction(); - try { - if (isset($record)) { - $request->merge(['plesk_id' => $record['data']['id'] ?? 0]); - } - - $this->recordRepository->create($request->all()); - DB::commit(); - } catch (Exception $error) { - DB::rollBack(); - return response()->json(['success' => false, 'message' => $error->getMessage()]); - } - - return response()->json([ - 'success' => true, - 'message' => 'Add record successfully', - 'url' => route('admin.domain.record', ['id' => $id]), - ]); - } - - public function edit(int $id = 0, int $record_id = 0) - { - $domain = $this->domainRepository->find($id); - if (empty($domain)) { - return back()->withErrors('These credentials do not match our records.'); - } - - $record = $this->recordRepository->find($record_id); - if (empty($record)) { - return back()->withErrors('These credentials do not match our records.'); - } - - $title = 'Edit Record'; - return view('Backend.records.edit', compact('title', 'record', 'id')); - } - - public function update(Request $request, int $id = 0, int $record_id = 0) - { - $domain = $this->domainRepository->find($id); - if (empty($domain)) { - return response()->json(['success' => false, 'message' => 'These credentials do not match our records.']); - } - - $record = $this->recordRepository->find($record_id); - if (empty($record)) { - return response()->json(['success' => false, 'message' => 'These credentials do not match our records.']); - } - - $validated = Validator::make($request->all(), $this->rules[$request->get('type')] ?? []); - - if ($validated->fails()) { - return response()->json(['success' => false, 'message' => $validated->errors()->first()]); - } - - $this->serverService->findServerByID($domain->dns_manager_server_id); - $this->serverService->handlePayload($request->all()); - $updatedRecord = $this->serverService->updateRecord($record->plesk_id); - - if (!empty($updatedRecord) && $updatedRecord['error']) { - return response()->json(['success' => false, 'message' => $updatedRecord['message'] ?? 'Failed to update record on server.']); - } - - DB::beginTransaction(); - try { - $this->recordRepository->update($record_id, $request->all()); - DB::commit(); - } catch (Exception $error) { - DB::rollBack(); - return response()->json(['success' => false, 'message' => $error->getMessage()]); - } - - return response()->json([ - 'success' => true, - 'message' => 'Update record successfully', - 'url' => route('admin.domain.record', ['id' => $id]), - ]); - } - - public function destroy(int $id = 0) - { - $record = $this->recordRepository->findWithRelations($id, ['domain']); - if (empty($record) || empty($record->domain)) { - return response()->json(['success' => false, 'message' => 'These credentials do not match our records.']); - } - - $this->serverService->findServerByID($record->domain->dns_manager_server_id); - $record_delete = $this->serverService->deleteRecord($record->plesk_id); - - if (!empty($record_delete) && $record_delete['error']) { - return response()->json(['success' => false, 'message' => $record_delete['message'] ?? 'Failed to delete record on server.']); - } - - DB::beginTransaction(); - try { - $domainId = $record->dns_manager_domain_id; - $this->recordRepository->delete($id); - DB::commit(); - } catch (Exception $error) { - DB::rollBack(); - return response()->json(['success' => false, 'message' => $error->getMessage()]); - } - - return response()->json([ - 'success' => true, - 'message' => 'Deleted record successfully', - 'url' => route('admin.domain.record', ['id' => $domainId]), - ]); - } - - public function destroyCustom(RecordRemoveRequest $request) - { - $request->validated(); - - $records = $this->recordRepository->findWhereIn('id', $request->get('ids'), ['domain']); - - if (!$records) { - return response()->json(['success' => false, 'message' => 'These credentials do not match our records.']); - } - - try { - foreach ($records->all() as $record) { - if (empty($record->domain)) continue; - - $this->serverService->findServerByID($record->domain->dns_manager_server_id); - $this->serverService->deleteRecord($record->plesk_id); - $this->recordRepository->delete($record->id); - } - } catch (Exception $error) { - return response()->json(['success' => false, 'message' => $error->getMessage()]); - } - - return response()->json([ - 'success' => true, - 'message' => 'Deleted successfully!', - 'url' => route('admin.record.index'), - ]); - } - - private function recordActionButtons($record): string - { - $domainId = $record->domain ? $record->domain->id : 0; - $editUrl = route('admin.record.edit', ['id' => $domainId, 'record_id' => $record->id]); - $deleteUrl = route('admin.record.destroy', ['id' => $record->id]); - - return << - - - - - - - Edit - - - - - - - - - Delete - - HTML; - } -} diff --git a/app/Http/Controllers/Backend/ServerController.php b/app/Http/Controllers/Backend/ServerController.php deleted file mode 100644 index 9069468..0000000 --- a/app/Http/Controllers/Backend/ServerController.php +++ /dev/null @@ -1,204 +0,0 @@ -serverRepository->getAll()->map(function ($server) { - return [ - 'id' => $server->id, - 'name' => $server->name, - 'ip' => $server->ip_address, - 'status' => $server->getStatus(), - 'checkbox' => '', - 'created_at' => Carbon::parse($server->created_at)->format('Y-m-d H:i:s'), - 'action' => $this->serverActionButtons($server), - ]; - }); - - return response()->json(['data' => $servers]); - } - - public function create() - { - $title = 'Add New Server Hosting'; - return view('Backend.servers.create', compact('title')); - } - - public function connect(ServerRequest $request) - { - $request->validated(); - - if (!$request->ajax()) { - return response()->json(['success' => false, 'message' => 'Method not support']); - } - - $status = $this->serverService->checkConnection($request->all()); - - if ($status) { - return response()->json(['success' => true, 'message' => 'Connection established successfully.']); - } - - return response()->json([ - 'success' => false, - 'message' => 'Connection attempt failed. Please check your credentials or server settings.', - ]); - } - - public function store(ServerRequest $request) - { - $request->validated(); - - DB::beginTransaction(); - try { - $data = $request->all(); - $data['password'] = Crypt::encryptString($request->password); - - $this->serverRepository->create($data); - DB::commit(); - } catch (Exception $error) { - DB::rollBack(); - return response()->json(['success' => false, 'message' => $error->getMessage()]); - } - - return response()->json([ - 'success' => true, - 'message' => 'Created successfully!', - 'url' => route('admin.server.index'), - ]); - } - - public function edit(int $id) - { - $server = $this->serverRepository->find($id); - if (!$server) { - return back()->withErrors('Invalid Request'); - } - - $title = 'Update Server Hosting'; - return view('Backend.servers.edit', compact('title', 'server')); - } - - public function update(ServerRequest $request, int $id) - { - $request->validated(); - - DB::beginTransaction(); - try { - $data = $request->all(); - $data['password'] = Crypt::encryptString($request->password); - - $this->serverRepository->update($id, $data); - DB::commit(); - } catch (Exception $error) { - DB::rollBack(); - return response()->json(['success' => false, 'message' => $error->getMessage()]); - } - - return response()->json([ - 'success' => true, - 'message' => 'Updated successfully!', - 'url' => route('admin.server.index'), - ]); - } - - public function destroy(int $id) - { - $server = $this->serverRepository->find($id); - if (!$server) { - return response()->json(['success' => false, 'message' => 'Invalid Request']); - } - - $this->serverRepository->delete($id); - - return response()->json([ - 'success' => true, - 'message' => 'Deleted successfully!', - 'url' => route('admin.server.index'), - ]); - } - - public function destroyCustom(ServerRemoveRequest $request) - { - $request->validated(); - - $servers = $this->serverRepository->findWhereIn('id', $request->get('ids')); - - if (!$servers) { - return response()->json(['success' => false, 'message' => 'Invalid Request']); - } - - foreach ($servers->all() as $server) { - $this->serverRepository->delete($server->id); - } - - return response()->json([ - 'success' => true, - 'message' => 'Deleted successfully!', - 'url' => route('admin.server.index'), - ]); - } - - private function serverActionButtons($server): string - { - $editUrl = route('admin.server.edit', ['id' => $server->id]); - $deleteUrl = route('admin.server.destroy', ['id' => $server->id]); - - return << - - - - - - - Edit - - - - - - - - - - - Delete - - HTML; - } -} diff --git a/app/Http/Controllers/rest/ModuleController.php b/app/Http/Controllers/rest/ModuleController.php deleted file mode 100644 index 96c7d6e..0000000 --- a/app/Http/Controllers/rest/ModuleController.php +++ /dev/null @@ -1,256 +0,0 @@ -json( - [ - 'error' => false, - 'data' => $this->vCenterRepository->filter(['status' => 1]) - ] - ); - } - - public function getDatacenter(Request $request) - { - $vCenter_id = $request->get('vcenter'); - if (!$vCenter_id) { - return response()->json(['error' => true, 'message' => 'Please choose vCenter']); - } - $vcenter = $this->vCenterRepository->find($vCenter_id); - - if (!$vcenter) { - return response()->json(['error' => true, 'message' => 'Can\'t find vCenter']); - } - - $datacenter = $this->datacenterRepository->filter( - [ - 'vcenter_id' => $vCenter_id - ], - [], - [], - ['id', 'name', 'datacenter_id'] - ); - - if (count($datacenter) > 0) { - return response()->json(['error' => false, 'data' => $datacenter]); - } - - return response()->json([ - 'error' => true, - 'message' => 'Can\'t find Datacenter by vCenter ID #' . $vCenter_id - ]); - } - - public function getHost(Request $request) - { - $dataCenter_id = $request->get('datacenter'); - if (!$dataCenter_id) { - return response()->json(['error' => true, 'message' => 'Please choose Datacenter']); - } - $datacenter = $this->datacenterRepository->find($dataCenter_id); - if (!$datacenter) { - return response()->json(['error' => true, 'message' => 'Can\'t find Datacenter']); - } - - $host = $this->hostRepository->filter( - [ - 'module_datacenter_id' => $datacenter->id - ], - [], - [], - ['id', 'name'] - ); - - if (count($host) > 0) { - return response()->json(['error' => false, 'data' => $host]); - } - - return response()->json([ - 'error' => true, - 'message' => 'Can\'t find Host by Datacenter ID #' . $dataCenter_id - ]); - } - - public function getDatastore(Request $request) - { - $host_id = $request->get('host'); - if (!$host_id) { - return response()->json(['error' => true, 'message' => 'Please choose Host']); - } - $host = $this->hostRepository->find($host_id); - - if (!$host) { - return response()->json(['error' => true, 'message' => 'Can\'t find Datacenter']); - } - - $datastores = $this->datastoreRepository->filter( - [ - 'module_host_id' => $host_id - ], - [], - [], - ['id', 'name'] - ); - if (count($datastores) > 0) { - return response()->json(['error' => false, 'data' => $datastores]); - } - return response()->json([ - 'error' => true, - 'message' => 'Can\'t find Datastore by Host ID #' . $host_id - ]); - } - - public function getNetwork(Request $request) - { - $host_id = $request->get('host'); - if (!$host_id) { - return response()->json(['error' => true, 'message' => 'Please choose Host']); - } - $host = $this->hostRepository->find($host_id); - - if (!$host) { - return response()->json(['error' => true, 'message' => 'Can\'t find Datacenter']); - } - - - $networks = $this->networkRepository->filter( - [ - 'module_host_id' => $host_id - ], - [], - [], - ['id', 'name'] - ); - if (count($networks) > 0) { - return response()->json(['error' => false, 'data' => $networks]); - } - return response()->json(['error' => true, 'message' => 'Can\'t find Network by Host ID #' . $host_id]); - } - - public function getIpAddress(Request $request) - { - $network_id = $request->get('network'); - if (!$network_id) { - return response()->json(['error' => true, 'message' => 'Please choose Network']); - } - - $network = $this->networkRepository->find($network_id); - - if (!$network) { - return response()->json(['error' => true, 'message' => 'Can\'t find Network']); - } - - $ipAddress = $this->ipAddressRepository->filter( - [ - 'module_network_id' => $network_id - ], - [], - [], - ['id', 'ip'] - ); - if (count($ipAddress) > 0) { - return response()->json( - [ - 'error' => false, - 'data' => $ipAddress->map(function ($item) { - $item['name'] = $item['ip']; - return $item; - }) - ] - ); - } - return response()->json(['error' => true, 'message' => 'Can\'t find IP by Network ID #' . $network_id]); - } - - public function getOsServerVersion(Request $request) - { - $host_id = $request->get('host'); - if (!$host_id) { - return response()->json(['error' => true, 'message' => 'Please choose Host']); - } - - $host = $this->hostRepository->findWithRelationships($host_id, ['datacenter.vCenter']); - if (!$host) { - return response()->json(['error' => true, 'message' => 'Can\'t find Datastore']); - } - $this->vCenter->token = $host->Datacenter->vCenter->token ?? ''; - $this->vCenter->apiUri = $host->Datacenter->vCenter->host ?? ''; - - $listVersion = $this->vCenter->getAllVersion($host->template); - - if ($listVersion === "Login_Again") { - $token = $this->vCenter->loginServer($host->datacenter->vCenter); - if ($token) { - $this->vCenterRepository->update($host->datacenter->vcenter_id, ['token' => $token]); - $this->vCenter->token = $token; - } - } - $listVersion = $this->vCenter->getAllVersion($host->template); - if ($listVersion) { - return response()->json(['error' => false, 'data' => $listVersion]); - } - return response()->json([ - 'error' => true, - 'message' => 'Can\'t find Version OS by Host ID #' . $host_id - ]); - } - - public function getOsGroup(Request $request) - { - $host_id = $request->get('host'); - - if (!$host_id) { - return response()->json(['error' => true, 'message' => 'Please choose Host']); - } - - $host = $this->hostRepository->find($host_id); - - if (!$host) { - return response()->json(['error' => true, 'message' => 'Can\'t find Host']); - } - $groups = $this->osGroupRepository->filter( - [ - 'module_host_id' => $host_id - ], - ['host'], // Include host relationship - [], - ['id', 'name', 'module_host_id'] // Add module_host_id to select - ); - if (count($groups) > 0) { - return response()->json(['error' => false, 'data' => $groups]); - } - return response()->json([ - 'error' => true, - 'message' => 'Can\'t find Os Group by Host ID #' . $host_id - ]); - } -} diff --git a/app/Http/Resources/Api/Service/VpsServiceResource.php b/app/Http/Resources/Api/Service/VpsServiceResource.php index 0a8a528..8de3ab8 100644 --- a/app/Http/Resources/Api/Service/VpsServiceResource.php +++ b/app/Http/Resources/Api/Service/VpsServiceResource.php @@ -15,12 +15,18 @@ class VpsServiceResource extends JsonResource public function toArray(Request $request): array { $orderItem = $this->orderItem; + $invoice = $this->invoice; + $order = $orderItem?->order ?? $invoice?->order; $product = $orderItem?->product; return [ 'id' => $this->id, 'uuid' => $this->uuid, 'server_name' => $this->server_name, + 'invoice_id' => $this->invoice_id, + 'invoice_num' => $invoice?->invoice_number, + 'order_item_id' => $this->order_item_id, + 'order_num' => $order?->order_num, 'status' => $this->status, 'status_label' => $this->status_label, 'auto_renew' => (bool) $this->auto_renew, diff --git a/app/Http/Services/vCenter.php b/app/Http/Services/vCenter.php index 0659c22..8330054 100644 --- a/app/Http/Services/vCenter.php +++ b/app/Http/Services/vCenter.php @@ -3,17 +3,17 @@ namespace App\Http\Services; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Cache; -use App\Models\ModuleFolder; -use Carbon\Carbon; -use Exception; -use Illuminate\Support\Facades\Http; -use Illuminate\Http\Client\ConnectionException; -use App\Http\Facade\SignRSA; -use App\Models\InfraManagement; -use Illuminate\Support\Str; -use phpseclib3\Net\SSH2; +use App\Http\Services\vCenter\Client; +use App\Http\Services\vCenter\PowerManagementService; +use App\Http\Services\vCenter\VirtualMachineService; +use App\Http\Services\vCenter\SnapshotService; +use App\Http\Services\vCenter\ResourceService; +use App\Http\Services\vCenter\TemplateService; +use App\Http\Services\vCenter\VMConfigurationService; +use App\Http\Services\vCenter\PowerShellService; use App\Http\Services\vCenter\Concerns\ManagesSsh; +use Exception; +use phpseclib3\Net\SSH2; class vCenter { @@ -25,2457 +25,385 @@ class vCenter private string $password; private int $diskCapacity = 1073741824; private int $memory = 1024; - private ?string $os = null; - private SSH2|WinRMClient|null $process = null; + private SSH2|null $process = null; + + private Client $client; + private PowerManagementService $power; + private VirtualMachineService $vm; + private SnapshotService $snapshot; + private ResourceService $resource; + private TemplateService $template; + private VMConfigurationService $vmConfig; + private PowerShellService $shell; + + public function __construct() + { + $this->client = new Client(); + $this->shell = new PowerShellService($this->client); + $this->power = new PowerManagementService($this->client); + $this->vm = new VirtualMachineService($this->client, $this->shell); + $this->snapshot = new SnapshotService($this->client, $this->shell); + $this->resource = new ResourceService($this->client); + $this->template = new TemplateService($this->client); + $this->vmConfig = new VMConfigurationService($this->client, $this->shell); + } + + // ─── Auth & Setup ──────────────────────────────────────────────────────── public function loginServer(\App\Models\Vcenter $vcenterModel): bool|string { - $this->apiUri = $vcenterModel->host; - $this->username = $vcenterModel->user; - $this->password = SignRSA::DecryptFunc($vcenterModel->password); // nếu đã mã hóa - - // Log::info('Đăng nhập với user: ' . $this->username); - - $response = Http::withoutVerifying() - ->withBasicAuth($this->username, $this->password) - ->post("https://{$this->apiUri}/rest/com/vmware/cis/session", []); - - // Log::info('Response login: ' . $response->body()); - - if ($response->successful()) { - $json = $response->json(); - if (isset($json['value'])) { - $this->token = $json['value']; - $vcenterModel->update(['token' => $this->token]); - // Log::info('token', ['token' => $this->token]); - return $this->token; - } - - $body = $response->body(); - if (!empty($body)) { - $this->token = $body; - $vcenterModel->update(['token' => $this->token]); - // Log::info('token raw string', ['token' => $this->token]); - return $this->token; - } - } - - return false; - } - - public function getAllDatacenter() - { - $check = $this->checkApiExits(); - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/datacenter")->json(); - if (!isset($response['type'])) { - return $response['value']; - } - return false; - } - return "Login_Again"; - } - - public function getAllVirtualMachine($datacenter = '', $host = '') - { - $check = $this->checkApiExits(); - if ($check) { - $params = []; - if (!empty($datacenter)) { - $params['filter.datacenters'] = $datacenter; - } - if (!empty($host)) { - $params['filter.hosts.1'] = $host; - } - - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/vm", $params)->json(); - - if (!isset($response['type'])) { - return $response['value']; - } - return false; - } - return "Login_Again"; - } - - /** - * Search VM by exact name in vCenter. - * Returns the first matching VM array or null if not found. - */ - public function findVirtualMachineByName(string $vmName): ?array - { - $check = $this->checkApiExits(); - if (!$check) { - Log::warning('findVirtualMachineByName: API check failed'); - return null; - } - - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/vm", [ - 'filter.names.1' => $vmName, - ]); - - if (!$response->successful()) { - Log::warning('findVirtualMachineByName: HTTP failed', ['status' => $response->status()]); - return null; - } - - $json = $response->json(); - if (isset($json['type']) || empty($json['value'])) { - return null; - } - - return $json['value'][0] ?? null; + $result = $this->client->loginServer($vcenterModel); + $this->token = $this->client->token; + $this->apiUri = $this->client->apiUri; + $this->username = $this->client->getUsername(); + $this->password = $this->client->getPassword(); + return $result; } public function checkApiExits(): bool { - - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/com/vmware/content/library")->json(); - - if (isset($response['type'])) { - return false; - } - return true; + return $this->client->checkApiExits(); } - /** - * Lấy thông tin vCenter từ product (ModuleVmware) qua nhiều cách: - * 1. Direct relation qua vcenter_id - * 2. Qua ipAddress → host → datacenter → vCenter - * 3. Qua Service relation (nếu Service có vcenter_id) - * - * @param \App\Models\ModuleVmware $product - * @return \App\Models\vCenter|null - */ - public function resolveVCenterFromProduct(\App\Models\ModuleVmware $product): ?\App\Models\vCenter + public function setConnection(string $apiUri, string $token): static { - // Cách 1: Ưu tiên quan hệ trực tiếp trên ModuleVmware - if ($product->vCenter) { - Log::channel('Custom')->debug('resolveVCenter: Found via direct vCenter relation', ['vcenter_id' => $product->vCenter->id]); - return $product->vCenter; - } - - // Cách 2: Fallback qua ipAddress chain - if ($product->ipAddress?->host?->datacenter?->vCenter) { - Log::channel('Custom')->debug('resolveVCenter: Found via ipAddress chain'); - return $product->ipAddress->host->datacenter->vCenter; - } - - // Cách 3: Qua Service relation (nếu Service có vcenter_id) - // Lấy service đầu tiên liên kết với VM này - $service = $product->services()->first(); - if ($service) { - // Load vmware relation với vCenter - $service->load('vmware.vCenter'); - if ($service->vmware?->vCenter) { - Log::channel('Custom')->debug('resolveVCenter: Found via Service→Vmware→vCenter'); - return $service->vmware->vCenter; - } - } - - Log::channel('Custom')->warning('resolveVCenter: Could not find vCenter for product', [ - 'product_id' => $product->id, - 'vmware_id' => $product->vmware_id, - ]); - - return null; - } - - public function getAllHost($datacenter = '') - { - $check = $this->checkApiExits(); - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/host", [ - 'filter.datacenters.1' => $datacenter - ])->json(); - if (!isset($response['type'])) { - return $response['value']; - } - return false; - } - return "Login_Again"; - } - - public function getAllFolder($datacenter = '') - { - $check = $this->checkApiExits(); - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/folder", [ - 'filter.datacenters.1' => $datacenter, - 'filter.type' => "VIRTUAL_MACHINE", - ])->json(); - if (!isset($response['type'])) { - return $response['value']; - } - return false; - } - return "Login_Again"; - } - - public function getTemplateFromDatastore($datastore = '') - { - - $allTemplate = $this->getAllTemplate(); - if (!$allTemplate) { - return false; - } - foreach ($allTemplate as $template) { - - $detailTemplate = $this->getTemplatebyID($template); - if ($detailTemplate && $detailTemplate === $datastore) { - return $template; - } - } - return false; - } - - public function getAllTemplate() - { - $check = $this->checkApiExits(); - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/com/vmware/content/library")->json(); - if (!isset($response['type'])) { - return $response['value']; - } - return false; - } - return "Login_Again"; - } - - protected function getTemplatebyID($template = '') - { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/com/vmware/content/library/id:$template")->json(); - if (!isset($response['type'])) { - return $response['value']['storage_backings'][0]['datastore_id']; - } - return false; - } - - public function getAllVersion($library = ''): bool|array|string - { - $check = $this->checkApiExits(); - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/com/vmware/content/library/item", [ - 'library_id' => $library - ])->json(); - if (!isset($response['type'])) { - $library_item = []; - foreach ($response['value'] as $value) { - $value_detail = $this->getVersionDetail($value); - if ($value_detail) { - $library_item[] = [ - 'name' => $value_detail['name'], - 'id' => $value_detail['id'] - ]; - } - } - return $library_item; - } - return false; - } - return "Login_Again"; - } - - protected function getVersionDetail($version_id = '') - { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/com/vmware/content/library/item/id:$version_id")->json(); - if (!isset($response['type'])) { - return $response['value']; - } - return false; - } - - public function loadNetWork($data = [], $host_id = '') - { - $this->username = $data->user; - $this->password = SignRSA::DecryptFunc($data->password); - $this->apiUri = $data->host; - - // Pass the host_id directly to the API call - $allNetwork = $this->getAllNetwork($host_id); - - return $allNetwork; - } - - private function getAllNetwork($host = '') - { - $check = $this->checkApiExits(); - if ($check) { - - $params = []; - if (!empty($host)) { - $params['filter.hosts'] = $host; - } - - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/network", $params)->json(); - - if (!isset($response['type'])) { - return $response['value']; - } - return false; - } - return "Login_Again"; - } - - public function getNetwork($host = ''): array|bool|string - { - return $this->getAllNetwork($host); - } - - // Moved to App\Http\Services\vCenter\Concerns\ManagesSsh - - public function resetGuestPasswordViaPs1(string $vmName, string $guestUser, string $guestPassword, string $newPassword, string $targetUser = 'Administrator'): array|string - { - // File ps1 chạy được ngoài PowerShell: bạn đã test ok - $psFile = "C:\\CLISave\\AssignIP\\SetGuestPwd.ps1"; - - // Escape chuỗi cho PowerShell single-quoted - $q = fn(string $v) => str_replace("'", "''", $v); - - // Xây args kiểu: -Key 'Value' - $args = " -VMName '" . $q($vmName) . "'"; - $args .= " -GuestUser '" . $q($guestUser) . "'"; - $args .= " -GuestPassword '" . $q($guestPassword) . "'"; - $args .= " -TargetUser '" . $q($targetUser) . "'"; - $args .= " -NewPassword '" . $q($newPassword) . "'"; - - // Gọi file trong cùng phiên PS đã Connect-VIServer (nhờ Command()) - // Thêm Out-String để ép JSON nằm trên STDOUT gọn - $cmd = "& '$psFile'$args | Out-String"; - - $raw = $this->CommandRSP($cmd); // <-- dùng lại Command() của bạn - if ($raw === 'Login_Fail' || $raw === false || $raw === null) { - return "Login_Fail"; - } - - // Thử parse JSON (có thể lẫn banner/log) - $trim = trim($raw); - $data = json_decode($trim, true); - if (json_last_error() !== JSON_ERROR_NONE) { - if (preg_match('/(\{.*\}|\[.*\])/s', $trim, $m)) { - $data = json_decode($m[1], true); - } - } - - return $data ?: $raw; // nếu chưa parse được, trả raw để debug - } - - - public function changeRDPPortWindows(string $vmName, string $guestUser, string $guestPassword, string $newPort): array|string - { - $psFile = "C:\\CLISave\\AssignIP\\change-rdp-port.ps1"; - $q = fn(string $v) => str_replace("'", "''", $v); - - // Dùng -VMName (string). Alias -VMNames cũng ok, nhưng mình set đúng tên tham số. - $args = " -VMName '" . $q($vmName) . "'"; - $args .= " -GuestUser '" . $q($guestUser) . "'"; - $args .= " -GuestPassword '" . $q($guestPassword) . "'"; // khớp tên tham số trong script 1-VM - $args .= " -NewPort " . (int) $newPort; - - $cmd = "& '$psFile'$args"; - - // Nếu đã có phiên bản CommandRSP($cmd, bool $expectJson) → bật expectJson để không trộn STDERR - $raw = method_exists($this, 'CommandRSP') - ? ($this->CommandChangePort($cmd, true) ?? '') - : ''; - - if ($raw === 'Login_Fail' || $raw === false || $raw === null) { - return "Login_Fail"; - } - $raw = trim($raw); - - // CẮT LẤY KHỐI JSON CUỐI (object hoặc array) nếu có noise - if (!preg_match('~(\{(?:[^{}]|(?1))*\}|\[(?:[^\[\]]|(?0))*\])\s*$~s', $raw, $m)) { - // fallback đơn giản hơn - if (preg_match('~(\{.*\}|\[.*\])\s*$~s', $raw, $m)) { - $raw = $m[1]; - } - } else { - $raw = $m[1]; - } - - $data = json_decode($raw, true); - if (json_last_error() !== JSON_ERROR_NONE) { - return [ - 'success' => false, - 'error' => 'JSON parsing failed', - 'raw_output' => $raw, - 'json_error' => json_last_error_msg() - ]; - } - - // Script 1-VM trả về 1 object, nhưng nếu lỡ trả mảng thì lấy phần tử đầu - if (isset($data[0])) { - $data = $data[0]; - } - - return $data; - } - - /** - * Change IP address of a VM via existing PowerShell scripts. - * - * Linux → C:\CLISave\changeIPLinux.ps1 - * Params: -VMName -IP -Gateway -DNS -GuestUser -GuestPassword - * Uses: Get-VM -Name $VMName + Invoke-VMScript (requires guest creds) - * Returns JSON → parsed via CommandChangePort($cmd, true) - * - * Windows → C:\CLISave\changeWindows1line.ps1 (legacy inline variable pattern) - * - * loginServer() MUST be called before this so $this->apiUri / username / - * password are set for the Connect-VIServer call inside CommandChangePort. - */ - public function changeIpVM( - string $vmName, // VM display name used with Get-VM -Name - string $vmwareId, // VMware ID (used for Windows -Id pattern) - string $ipAddress, - string $gateway, - string $osType, - string $guestUser = '', - string $guestPassword = '', - string $dns = '8.8.8.8', - string $hostname = '' - ): string { - $q = fn(string $v) => str_replace("'", "''", $v); - - $isLinux = str_contains(strtolower($osType), 'linux') - || str_contains(strtolower($osType), 'ubuntu') - || str_contains(strtolower($osType), 'centos') - || str_contains(strtolower($osType), 'rocky'); - - if ($isLinux) { - // changeIPLinux.ps1 param(VMName, IP, Gateway, DNS, GuestUser, GuestPassword) - $psFile = "C:\\CLISave\\changeIPLinux.ps1"; - $Command = "& '{$psFile}'" - . " -VMName '" . $q($vmName) . "'" - . " -IP '" . $q($ipAddress) . "'" - . " -Gateway '" . $q($gateway) . "'" - . " -DNS '" . $q($dns) . "'" - . " -GuestUser '" . $q($guestUser) . "'" - . " -GuestPassword '" . $q($guestPassword) . "'"; - - Log::channel('Custom')->info('[changeIpVM:linux] ' . $Command); - - // Script returns JSON → use CommandChangePort with expectJson=true - return (string) $this->CommandChangePort($Command, true); - } - - // Windows: legacy inline variable pattern → changeWindows1line.ps1 - $VirtualMachine = "VirtualMachine-$vmwareId"; - $hn = strtolower($hostname ?: $vmName); - $Command = "\$vm = get-vm -Id '{$VirtualMachine}';" - . " \$hostname = '{$q($hn)}';" - . " \$ipadd = '{$q($ipAddress)}';" - . " \$gw = '{$q($gateway)}';" - . " \$password = \"'{$q($guestPassword)}'\"; " - . " cd /; C:\\CLISave\\changeWindows1line.ps1;"; - - Log::channel('Custom')->info('[changeIpVM:windows] ' . $Command); - - return (string) $this->Command($Command); - } - - public function loadDatastore($data = [], $host_id = '') - { - Log::info('[vCenter] loadDatastore()', [ - 'host' => $data->host, - 'esxi_host' => $host_id - ]); - - $allDatastore = $this->getAllDataStore($host_id); - - if ($allDatastore === "Login_Again") { - Log::warning('[vCenter] Token expired when getAllDataStore'); - return "Login_Again"; - } - - if (!$allDatastore) { - Log::warning('[vCenter] getAllDataStore return false'); - return false; - } - - Log::info('[vCenter] Datastore count from API', [ - 'count' => count($allDatastore) - ]); - - return $allDatastore; - } - - - public function getAllDataStore($host = '') - { - Log::info('[vCenter] getAllDataStore call', [ - 'api' => $this->apiUri, - 'host_filter' => $host - ]); - - $check = $this->checkApiExits(); - - if (!$check) { - Log::warning('[vCenter] checkApiExits = false'); - return "Login_Again"; - } - - $params = []; - if (!empty($host)) { - $params['filter.hosts'] = $host; - } - - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/datastore", $params); - - Log::info('[vCenter] datastore raw response', [ - 'status' => $response->status(), - // 'body' => $response->body() // Comment out to reduce log noise if large - ]); - - $json = $response->json(); - - if (!isset($json['type'])) { - return $json['value']; - } - - Log::warning('[vCenter] API returned error', [ - 'response' => $json - ]); - - return false; - } - - - public function CreateVirtualMachine($product_data): array|string - { - $vcenterModel = $product_data['vcenter']; - - if (!$this->loginServer($vcenterModel)) { - Log::error('Failed to create VM: API check failed, requires login.'); - return 'Login_Again'; - } - - $userName = customer()?->email; - - $item = $product_data['item']; - $host = $product_data['host']; - $datacenter = $product_data['datacenter']; - $template = $product_data['template']; - - $addon = $product_data['addon'] ?? ['cpu' => 0, 'ram' => 0, 'disk' => 0]; - - // Lấy cấu hình gốc từ item hoặc product - $base_cpu = $item->cpu ?? ($host->cpu ?? 1); - $base_ram = $item->ram ?? ($host->ram ?? 1); - $base_disk = $item->disk ?? ($host->disk ?? 20); - - // Đảm bảo giá trị không bị null hoặc 0 - $base_cpu = max(1, (int) $base_cpu); - $base_ram = max(1, (int) $base_ram); - $base_disk = max(20, (int) $base_disk); - - // Lấy tất cả datastore liên quan host, không lấy first() - $datastores = \App\Models\ModuleDatastore::where('module_host_id', $host->id)->get(); - - // Nếu muốn chọn 1 datastore để truyền (ví dụ lấy cái đầu tiên) - $datastoreId = $datastores->isNotEmpty() - ? $datastores->first()->datastore - : 'datastore-3004'; // fallback nếu không có - - $body = [ - 'spec' => [ - 'name' => $item->server_name ?? ($userName . '-vm-' . $item->id), - 'source' => $template, // sử dụng template của host, thay 'vm-3007' cứng thành biến - 'placement' => [ - 'datacenter' => $datacenter->datacenter_id, - 'folder' => 'group-v1008', // hoặc lấy từ DB nếu cần - 'host' => $host->host_id, - 'resource_pool' => 'resgroup-3002', // nếu có thể lấy động thì tốt hơn - 'datastore' => $datastoreId, // chuỗi datastore đúng định dạng - ], - 'power_on' => false, - 'fallback_to_datastore_default_policy' => true, - ], - ]; - - Log::info('Data payload', ['data' => $body]); - - try { - Log::info('Bắt đầu clone VM với timeout 300 giây', ['template' => $template, 'target_name' => $body['spec']['name']]); - - $response = Http::withoutVerifying() - ->timeout(300) // Tăng timeout lên 5 phút cho quá trình clone - ->connectTimeout(30) // Giữ connect timeout ngắn để phát hiện lỗi kết nối sớm - ->withHeaders([ - 'vmware-api-session-id' => $this->token, - 'Content-Type' => 'application/json', - ]) - ->post("https://{$this->apiUri}/rest/vcenter/vm?action=clone", $body); - - $responseData = $response->json(); - - if ($response->successful() && !isset($responseData['type'])) { - $vmId = $responseData['value'] ?? 'UNKNOWN'; - - Log::info("VM clone thành công, VM ID: {$vmId}", [ - 'addon_data' => $addon, - 'base_cpu' => $base_cpu, - 'base_ram' => $base_ram, - 'has_cpu_addon' => $addon['cpu'] > 0, - 'has_ram_addon' => $addon['ram'] > 0, - 'has_disk_addon' => $addon['disk'] > 0 - ]); - - // Cập nhật cấu hình phần cứng sau khi clone thành công - // Thay đổi điều kiện: chạy nếu có bất kỳ addon nào > 0 - if ($vmId !== 'UNKNOWN' && ($addon['cpu'] > 0 || $addon['ram'] > 0 || $addon['disk'] > 0)) { - Log::info("Bắt đầu cập nhật addon cho VM {$vmId}"); - $this->updateVMHardware($vmId, $addon, $base_cpu, $base_ram); - } else { - Log::info("Không có addon nào cần cập nhật cho VM {$vmId}", [ - 'vm_id' => $vmId, - 'addon' => $addon, - 'condition_check' => $vmId !== 'UNKNOWN' && ($addon['cpu'] > 0 || $addon['ram'] > 0 || $addon['disk'] > 0) - ]); - } - - // 🟢 Gửi thông báo Discord - $discord = app(\App\Http\Services\Discord::class); - - // Thông tin addon - $addonMsg = ''; - if (!empty($addon)) { - $addonMsg = "\n**Addon:** " . - ($addon['cpu'] ? "CPU +{$addon['cpu']} " : "") . - ($addon['ram'] ? "RAM +{$addon['ram']}GB " : "") . - ($addon['disk'] ? "Disk +{$addon['disk']}GB " : ""); - } - - $discord->SendMessage( - message: "**User:** {$item->server_name}\n**VM ID:** `{$vmId}`{$addonMsg}\n**Thời gian:** " . now()->format('d/m/Y H:i:s'), - color: Discord::COLOR_SUCCESS, - title: '🎉 Tạo máy ảo thành công' - ); - - return ['error' => false, 'data' => $vmId]; - } - - return ['error' => true, 'data' => $responseData['value'] ?? $response->body()]; - } catch (\Exception $e) { - Log::error('Exception when creating VM', ['message' => $e->getMessage()]); - return ['error' => true, 'data' => $e->getMessage()]; - } - } - - /** - * Cập nhật cấu hình phần cứng VM sau khi clone - */ - private function updateVMHardware($vmId, $addon, $base_cpu, $base_ram): void - { - try { - Log::info("=== BẮT ĐẦU CẬP NHẬT ADDON ==="); - Log::info("Thông tin VM và Addon", [ - 'vm_id' => $vmId, - 'addon' => $addon, - 'base_cpu' => $base_cpu, - 'base_ram' => $base_ram, - 'addon_cpu' => $addon['cpu'], - 'addon_ram' => $addon['ram'], - 'addon_disk' => $addon['disk'] - ]); - - // Tính toán cấu hình mới - $new_cpu = $base_cpu + $addon['cpu']; - // Sửa: RAM đầu vào là GB, convert sang MiB - $new_ram_mib = ($base_ram + $addon['ram']) * 1024; // 1 GB = 1024 MiB - - Log::info("Tính toán cấu hình mới", [ - 'vm_id' => $vmId, - 'base_cpu' => $base_cpu, - 'addon_cpu' => $addon['cpu'], - 'new_cpu' => $new_cpu, - 'base_ram_gb' => $base_ram, - 'addon_ram_gb' => $addon['ram'], - 'new_ram_mib' => $new_ram_mib - ]); - - // Cập nhật CPU nếu có addon - if ($addon['cpu'] > 0) { - Log::info("🔄 Bắt đầu cập nhật CPU cho VM {$vmId}"); - $cpuResult = $this->updateVMCPU($vmId, $new_cpu); - if ($cpuResult) { - Log::info("✅ Cập nhật CPU thành công: {$base_cpu} → {$new_cpu} cores"); - } else { - Log::error("❌ Cập nhật CPU thất bại cho VM {$vmId}"); - } - } else { - Log::info("⏭️ Bỏ qua cập nhật CPU (không có addon CPU)"); - } - - // Cập nhật RAM nếu có addon - if ($addon['ram'] > 0) { - Log::info("🔄 Bắt đầu cập nhật RAM cho VM {$vmId}"); - $ramResult = $this->updateVMMemory($vmId, $new_ram_mib); - if ($ramResult) { - Log::info("✅ Cập nhật RAM thành công: {$base_ram}GB → " . ($base_ram + $addon['ram']) . "GB ({$new_ram_mib}MiB)"); - } else { - Log::error("❌ Cập nhật RAM thất bại cho VM {$vmId}"); - } - } else { - Log::info("⏭️ Bỏ qua cập nhật RAM (không có addon RAM)"); - } - - Log::info("=== HOÀN THÀNH CẬP NHẬT ADDON ==="); - } catch (\Exception $e) { - Log::error("❌ Lỗi khi cập nhật cấu hình phần cứng VM {$vmId}: " . $e->getMessage()); - Log::error("Stack trace: " . $e->getTraceAsString()); - } - } - - /** - * Cập nhật CPU cho VM - */ - private function updateVMCPU($vmId, $cpuCount): bool - { - try { - Log::info("🔄 Gọi API cập nhật CPU", [ - 'vm_id' => $vmId, - 'new_cpu_count' => $cpuCount - ]); - - // Tính toán cores_per_socket dựa trên count - // Đảm bảo count % cores_per_socket == 0 - $coresPerSocket = 1; - if ($cpuCount > 1) { - // Tìm ước số lớn nhất <= cpuCount/2 để tối ưu phân bổ socket - $maxCoresPerSocket = floor($cpuCount / 2); - for ($i = $maxCoresPerSocket; $i >= 1; $i--) { - if ($cpuCount % $i === 0) { - $coresPerSocket = $i; - break; - } - } - // Nếu không tìm được ước số phù hợp, sử dụng cpuCount - if ($coresPerSocket === 1 && $cpuCount > 2) { - $coresPerSocket = $cpuCount; - } - } - - $payload = [ - 'spec' => [ - 'count' => $cpuCount, - 'cores_per_socket' => $coresPerSocket, - 'hot_add_enabled' => true - ] - ]; - $response = Http::withoutVerifying() - ->timeout(30) - ->withHeaders([ - 'vmware-api-session-id' => $this->token, - 'Content-Type' => 'application/json', - ]) - ->patch("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/cpu", $payload); - - - if ($response->successful()) { - Log::info("✅ API call cập nhật CPU thành công cho VM {$vmId}"); - return true; - } else { - Log::error("❌ API call cập nhật CPU thất bại cho VM {$vmId}", [ - 'status' => $response->status(), - 'response' => $response->body(), - 'url' => "https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/cpu" - ]); - return false; - } - } catch (\Exception $e) { - Log::error("❌ Exception khi cập nhật CPU cho VM {$vmId}: " . $e->getMessage()); - Log::error("Stack trace: " . $e->getTraceAsString()); - return false; - } - } - - /** - * Cập nhật RAM cho VM - */ - private function updateVMMemory($vmId, $ramSizeMB): bool - { - try { - Log::info("🔄 Gọi API cập nhật RAM", [ - 'vm_id' => $vmId, - 'new_ram_mb' => $ramSizeMB - ]); - - $payload = [ - 'spec' => [ - 'size_MiB' => $ramSizeMB - ] - ]; - - $response = Http::withoutVerifying() - ->timeout(30) - ->withHeaders([ - 'vmware-api-session-id' => $this->token, - 'Content-Type' => 'application/json', - ]) - ->patch("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/memory", $payload); - - if ($response->successful()) { - Log::info("✅ API call cập nhật RAM thành công cho VM {$vmId}"); - return true; - } else { - Log::error("❌ API call cập nhật RAM thất bại cho VM {$vmId}", [ - 'status' => $response->status(), - 'response' => $response->body(), - 'url' => "https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/memory" - ]); - return false; - } - } catch (\Exception $e) { - Log::error("❌ Exception khi cập nhật RAM cho VM {$vmId}: " . $e->getMessage()); - Log::error("Stack trace: " . $e->getTraceAsString()); - return false; - } - } - - /** - * Cập nhật Disk cho VM (mở rộng disk) - */ - private function updateVMDisk($vmId, $diskSizeGB): bool - { - try { - // Tính toán dung lượng disk mới (GB sang bytes) - $diskSizeBytes = $diskSizeGB * 1024 * 1024 * 1024; // GB to bytes - - $payload = [ - 'spec' => [ - 'capacity' => $diskSizeBytes - ] - ]; - - // Lấy thông tin disk hiện tại để biết disk ID - $diskResponse = Http::withoutVerifying() - ->timeout(30) - ->withHeaders([ - 'vmware-api-session-id' => $this->token, - ]) - ->get("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/disk"); - - if (!$diskResponse->successful()) { - Log::error("Không thể lấy thông tin disk hiện tại cho VM {$vmId}"); - return false; - } - - $diskData = $diskResponse->json(); - if (!isset($diskData['value']) || empty($diskData['value'])) { - Log::error("Không tìm thấy disk nào cho VM {$vmId}"); - return false; - } - - // Lấy disk đầu tiên (thường là disk chính) - $firstDisk = $diskData['value'][0]; - $diskKey = $firstDisk['key']; - - $response = Http::withoutVerifying() - ->timeout(30) - ->withHeaders([ - 'vmware-api-session-id' => $this->token, - 'Content-Type' => 'application/json', - ]) - ->patch("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}/hardware/disk/{$diskKey}", $payload); - - if ($response->successful()) { - Log::info("API call cập nhật Disk thành công cho VM {$vmId}"); - return true; - } else { - Log::error("API call cập nhật Disk thất bại cho VM {$vmId}", [ - 'status' => $response->status(), - 'response' => $response->body() - ]); - return false; - } - } catch (\Exception $e) { - Log::error("Exception khi cập nhật Disk cho VM {$vmId}: " . $e->getMessage()); - return false; - } - } - - private function GetFolderByHost($datacenter = '') - { - return ModuleFolder::where('module_datacenter_id', $datacenter)->first(); - } - - public function CheckVirtualMachine($vmware_id = ''): string|array - { - $check = $this->checkApiExits(); - - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/power")->json(); - if (!isset($response['type'])) { - return [ - 'error' => false, - 'data' => $response['value'] - ]; - } - return [ - 'error' => true, - 'message' => $response['value'] - ]; - } - return "Login_Again"; - } - - public function GetNetworkVirtualMachine($vmware_id = ''): array - { - $response = $this->GetDetailVirtualMachine($vmware_id); - if ($response['error'] === false) { - return [ - 'error' => false, - 'data' => $response['data']['nics'][0]['value']['backing'] - ]; - } - return [ - 'error' => true, - 'message' => $response['data'] - ]; - } - - public function ConfigNetworkVirtualMachine($vmware_id = '', $config = []): int - { - return Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->patch("https://$this->apiUri/rest/vcenter/vm/$vmware_id/hardware/ethernet/4000", $config) - ->status(); - } - - public function ConfigVirtualMachineUbuntu($vmware_id = '', $product = null, $password = ''): void - { - if (!$product) { - Log::error('ConfigVirtualMachineUbuntu: Product is null', ['vmware_id' => $vmware_id]); - return; - } - - // Use resolveVCenterFromProduct to get vCenter - $vcenter = $this->resolveVCenterFromProduct($product); - if (!$vcenter) { - Log::error('ConfigVirtualMachineUbuntu: Cannot resolve vCenter', ['vmware_id' => $vmware_id]); - return; - } - - $this->SetConfig($vcenter); - $VirtualMachine = "VirtualMachine-$vmware_id"; - $IPAddress = $product->ipAddress?->ip ?? '0.0.0.0'; - $Gateway = $product->ipAddress?->gateway ?? '0.0.0.0'; - $HostName = strtolower($product->hostname ?? 'vm'); - - Log::channel('Custom')->info("\r\n"); - - try { - - Log::channel('Custom')->info("==================================================================="); - - Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine"); - - Log::channel('Custom')->info("\r\n"); - - $Command = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createUbuntu1line.ps1;"; - - Log::channel('Custom')->info("$Command"); - - $this->Command($Command); - - Log::channel('Custom')->info("\r\n"); - - Log::channel('Custom')->info("After run Command 1 |" . Carbon::now()); - - $Command = "\$vm = get-vm -Id '$VirtualMachine'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\changeLinux.ps1;"; - - Log::channel('Custom')->info("$Command"); - - $this->Command($Command); - Log::channel('Custom')->info("\r\n"); - - Log::channel('Custom')->info("After run Command 2 |" . Carbon::now()); - - Log::channel('Custom')->info("\r\n"); - - - Log::channel('Custom')->info("==================================================================="); - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - } - } - - public function GetDetailVirtualMachine($vmware_id = ''): array - { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id")->json(); - if (!isset($response['type'])) { - return [ - 'error' => false, - 'data' => $response['value'] - ]; - } - return [ - 'error' => true, - 'message' => $response['value'] - ]; - } - - public function GetIPAddressVirtualMachine($vmware_id = ''): array - { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/identity")->json(); - if (!isset($response['type'])) { - return [ - 'error' => false, - 'data' => $response['value'] - ]; - } - return [ - 'error' => true, - 'message' => $response['value'] - ]; - } - - public function GetVMStats($vmware_id = ''): array - { - $stats = [ - 'cpu' => null, - 'memory' => null, - 'disk' => null, - ]; - - try { - $hardwareResponse = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/hardware")->json(); - - if (is_array($hardwareResponse) && !isset($hardwareResponse['type']) && isset($hardwareResponse['value'])) { - foreach ($hardwareResponse['value'] as $hardware) { - if (isset($hardware['label']) && $hardware['label'] === 'CPU') { - $stats['cpu'] = [ - 'cores' => $hardware['spec']['num_cores'] ?? 0, - 'cores_per_socket' => $hardware['spec']['cores_per_socket'] ?? 0, - ]; - } - if (isset($hardware['label']) && $hardware['label'] === 'Memory') { - $stats['memory'] = [ - 'size_MiB' => $hardware['spec']['size_MiB'] ?? 0, - 'size_GB' => round(($hardware['spec']['size_MiB'] ?? 0) / 1024, 2), - ]; - } - if (isset($hardware['label']) && $hardware['label'] === 'Hard disk') { - $stats['disk'][] = [ - 'capacity' => $hardware['capacity'] ?? 0, - 'capacity_GB' => round(($hardware['capacity'] ?? 0) / (1024 * 1024 * 1024), 2), - ]; - } - } - } - - $guestMemoryResponse = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/memory")->json(); - - if (is_array($guestMemoryResponse) && !isset($guestMemoryResponse['type']) && isset($guestMemoryResponse['value'])) { - $stats['memory']['used_MiB'] = $guestMemoryResponse['value']['used'] ?? 0; - $stats['memory']['used_GB'] = round(($guestMemoryResponse['value']['used'] ?? 0) / 1024, 2); - $stats['memory']['usage_percent'] = $stats['memory']['size_MiB'] > 0 - ? round(($guestMemoryResponse['value']['used'] ?? 0) / $stats['memory']['size_MiB'] * 100, 1) - : 0; - } - - $guestCpuResponse = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://$this->apiUri/rest/vcenter/vm/$vmware_id/guest/cpu")->json(); - - if (is_array($guestCpuResponse) && !isset($guestCpuResponse['type']) && isset($guestCpuResponse['value'])) { - $stats['cpu']['used_percent'] = $guestCpuResponse['value']['used'] ?? 0; - } - - return ['error' => false, 'data' => $stats]; - } catch (\Exception $e) { - return ['error' => true, 'message' => $e->getMessage(), 'data' => $stats]; - } - } - - public function ConfigCentOSVersion8($vmware_id = '', $product = [], $password = ''): void - { - // Use resolveVCenterFromProduct to get vCenter - $vcenter = null; - if ($product) { - $vcenter = $this->resolveVCenterFromProduct($product); - } - if (!$vcenter) { - Log::error('Cannot resolve vCenter', ['vmware_id' => $vmware_id]); - return; - } - $this->SetConfig($vcenter); - $VirtualMachine = "VirtualMachine-$vmware_id"; - $IPAddress = $product->ipAddress?->ip ?? '0.0.0.0'; - $Gateway = $product->ipAddress?->gateway ?? '0.0.0.0'; - $HostName = strtolower($product->hostname ?? 'vm'); - Log::channel('Custom')->info("\r\n"); - - try { - - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine"); - Log::channel('Custom')->info("\r\n"); - - $Command = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createCentOS81line.ps1;"; - - Log::channel('Custom')->info("$Command"); - - $this->Command($Command); - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - - Log::channel('Custom')->info("==================================================================="); - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - } - } - - public function ConfigCentOSVersion7($vmware_id = '', $product = [], $password = ''): void - { - // Use resolveVCenterFromProduct to get vCenter - $vcenter = null; - if ($product) { - $vcenter = $this->resolveVCenterFromProduct($product); - } - if (!$vcenter) { - Log::error('Cannot resolve vCenter', ['vmware_id' => $vmware_id]); - return; - } - $this->SetConfig($vcenter); - $VirtualMachine = "VirtualMachine-$vmware_id"; - $IPAddress = $product->ipAddress?->ip ?? '0.0.0.0'; - $Gateway = $product->ipAddress?->gateway ?? '0.0.0.0'; - $HostName = strtolower($product->hostname ?? 'vm'); - Log::channel('Custom')->info("\r\n"); - - try { - - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine"); - Log::channel('Custom')->info("\r\n"); - - $Command = "\$vm = get-vm -Id '$VirtualMachine'; \$hostname ='$HostName';\$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createCentOS71line.ps1;"; - - Log::channel('Custom')->info($Command); - - $this->Command($Command); - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - - Log::channel('Custom')->info("==================================================================="); - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - } - } - - public function ConfigWindowsVersion10($vmware_id = '', $product = [], $password = ''): void - { - try { - // Kiểm tra null safety - if (!$product) { - Log::error('❌ ConfigWindowsVersion10: Product is null', ['vmware_id' => $vmware_id]); - return; - } - - // Use resolveVCenterFromProduct to get vCenter - $vcenter = null; - if ($product) { - $vcenter = $this->resolveVCenterFromProduct($product); - } - if (!$vcenter) { - Log::error('Cannot resolve vCenter', ['vmware_id' => $vmware_id]); - return; - } - $this->SetConfig($vcenter); - $VirtualMachine = "VirtualMachine-$vmware_id"; - $IPAddress = $product->IpAddress->ip; - $Gateway = $product->IpAddress->gateway; - $HostName = strtolower($product->hostname); - - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine"); - Log::channel('Custom')->info("\r\n"); - - $Command = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\changeWindows10.ps1;"; - - Log::channel('Custom')->info("$Command"); - $this->Command($Command); - - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - Log::channel('Custom')->info("==================================================================="); - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - } - } - - public function ConfigWindowsServer($vmware_id = '', $product = [], $password = ''): void - { - try { - // Kiểm tra null safety - if (!$product) { - Log::error('❌ ConfigWindowsServer: Product is null', ['vmware_id' => $vmware_id]); - return; - } - - // Use resolveVCenterFromProduct to get vCenter - $vcenter = null; - if ($product) { - $vcenter = $this->resolveVCenterFromProduct($product); - } - if (!$vcenter) { - Log::error('Cannot resolve vCenter', ['vmware_id' => $vmware_id]); - return; - } - $this->SetConfig($vcenter); - $VirtualMachine = "VirtualMachine-$vmware_id"; - $IPAddress = $product->IpAddress->ip; - $Gateway = $product->IpAddress->gateway; - $HostName = strtolower($product->hostname); - - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine"); - Log::channel('Custom')->info("\r\n"); - - $Command = "\$vm = get-vm -Id '$VirtualMachine';\$hostname ='$HostName'; \$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\changeWindows1line.ps1;"; - - Log::channel('Custom')->info("$Command"); - - $this->Command($Command); - - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - Log::channel('Custom')->info("==================================================================="); - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - } - } - - public function ConfigFixWindowsClick($vmware_id = '', $product = []): void - { - try { - // Kiểm tra null safety - if (!$product) { - Log::error('❌ ConfigFixWindowsClick: Product is null', ['vmware_id' => $vmware_id]); - return; - } - - // Use resolveVCenterFromProduct to get vCenter - $vcenter = $this->resolveVCenterFromProduct($product); - if (!$vcenter) { - Log::error('Cannot resolve vCenter', ['vmware_id' => $vmware_id]); - return; - } - $this->SetConfig($vcenter); - $VirtualMachine = "VirtualMachine-$vmware_id"; - - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine"); - Log::channel('Custom')->info("\r\n"); - - $Command = "\$vm = get-vm -Id '$VirtualMachine'; cd /; C:\CLISave\FixTwoClickWindows.ps1;"; - - Log::channel('Custom')->info("$Command"); - $this->Command($Command); - - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - Log::channel('Custom')->info("==================================================================="); - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - } - } - - public function ConfigRockyLinuxVersion9($vmware_id = '', $product = [], $password = ''): void - { - try { - // Kiểm tra null safety - if (!$product) { - Log::error('❌ ConfigRockyLinuxVersion9: Product is null', ['vmware_id' => $vmware_id]); - return; - } - - // Use resolveVCenterFromProduct to get vCenter - $vcenter = $this->resolveVCenterFromProduct($product); - if (!$vcenter) { - Log::error('Cannot resolve vCenter', ['vmware_id' => $vmware_id]); - return; - } - $this->SetConfig($vcenter); - $VirtualMachine = "VirtualMachine-$vmware_id"; - $IPAddress = $product->ipAddress?->ip ?? '0.0.0.0'; - $Gateway = $product->ipAddress?->gateway ?? '0.0.0.0'; - $HostName = strtolower($product->hostname ?? 'vm'); - - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Get Config information for VirtualMachine with #ID $VirtualMachine"); - Log::channel('Custom')->info("\r\n"); - - $Command = "\$vm = get-vm -Id '$VirtualMachine'; \$hostname ='$HostName';\$ipadd = '$IPAddress'; \$gw = '$Gateway'; \$password = \"'$password'\"; cd /; C:\CLISave\createRocky1line.ps1;"; - - Log::channel('Custom')->info($Command); - $this->Command($Command); - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - Log::channel('Custom')->info("==================================================================="); - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - } - } - - public function GetConsoleTickets($vmware_id = ''): array|string - { - $check = $this->checkApiExits(); - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->post("https://$this->apiUri/api/vcenter/vm/$vmware_id/console/tickets", [ - "type" => "WEBMKS" - ])->json(); - if (!isset($response['error_type'])) { - return [ - 'error' => false, - 'data' => $response['ticket'] - ]; - } - return [ - 'error' => true, - 'data' => $response['messages'] - ]; - } - return "Login_Again"; - } - - public function SuspendVirtualMachine($vmware_id = ''): array|string - { - - $check = $this->checkApiExits(); - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->post("https://$this->apiUri/rest/vcenter/vm/$vmware_id/power/suspend"); - if ($response->status() === 200) { - return [ - 'error' => false, - 'data' => 'success' - ]; - } - return [ - 'error' => true, - 'data' => $response['value'] - ]; - } - return "Login_Again"; - } - - public function RebootVirtualMachine($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=reboot"); - if ($response->status() === 200) { - return [ - 'error' => false, - 'data' => 'success' - ]; - } - return [ - 'error' => true, - 'data' => $response['value'] - ]; - } - return "Login_Again"; - } - - public function restartVM($product, $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', - ]; - } - - // Ưu tiên quan hệ trực tiếp trên ModuleVmware - $vcenter = $product->vCenter ?? null; - - // Fallback cho dữ liệu cũ chưa có vcenter_id - if (!$vcenter && isset($product->ipAddress) && $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', - ]; - } - - $check = $this->loginServer($vcenter); - if (!$check) { - return "Login_Again"; - } - - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->post("https://$this->apiUri/rest/vcenter/vm/$vmId/guest/power?action=reboot"); - - 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(), - ]; - } - } - - public function StartVirtualMachine($product, $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]) - ->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()]; - } - } - - public function ShutdownGuestVirtualMachine($product, $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]) - ->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()]; - } - } - - public function PowerOffVirtualMachine($vmware_id = ''): array|string - { - $check = $this->checkApiExits(); - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->post("https://$this->apiUri/rest/vcenter/vm/$vmware_id/power/stop"); - if ($response->status() === 200) { - return [ - 'error' => false, - 'data' => 'success' - ]; - } - return [ - 'error' => true, - 'data' => $response['value'] - ]; - } - 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(); - if ($check) { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->delete("https://$this->apiUri/rest/vcenter/vm/$vmware_id"); - if ($response->status() === 200) { - return [ - 'error' => false, - 'data' => 'success' - ]; - } - return [ - 'error' => true, - 'data' => $response['value'] - ]; - } - return "Login_Again"; - } - - public function CreateSnapShot($product, ?string $snapshotName = null): string|array - { - try { - // Sử dụng helper method để resolve vCenter từ product - $vcenter = $this->resolveVCenterFromProduct($product); - - if (!$vcenter) { - return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; - } - - $this->username = $vcenter->user; - $this->password = SignRSA::DecryptFunc($vcenter->password); - $this->apiUri = $vcenter->host; - - // Login to get token - $loginResult = $this->loginServer($vcenter); - if (!$loginResult) { - return "Login_Again"; - } - } catch (\Exception $e) { - return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()]; - } - $vmware_name = (string) $product->name; - - $check = $this->checkApiExits(); - - if ($check) { - try { - $Snapshot = $snapshotName ?: ($vmware_name . "_" . date('d_m_Y_H_i_s')); - - Log::channel('Custom')->info("==================================================================="); - - Log::channel('Custom')->info("Create Snapshot with VirtualMachine Name - $vmware_name - Snap #$Snapshot"); - - Log::channel('Custom')->info("\r\n"); - - $Command = "& 'C:\CLISave\createsnapshot.ps1' -vm_name '$vmware_name' -name '$Snapshot'"; - - Log::channel('Custom')->info("$Command"); - - $cmdResult = $this->Command($Command); - - Log::channel('Custom')->info("Command Result: " . $cmdResult); - Log::channel('Custom')->info("\r\n"); - - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - - Log::channel('Custom')->info("==================================================================="); - - // Check if command failed - if (strpos($cmdResult, 'Login_Fail') !== false) { - return [ - 'error' => true, - 'message' => 'SSH Login to PowerShell server failed' - ]; - } - - if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) { - return [ - 'error' => true, - 'message' => 'PowerShell command failed: ' . $cmdResult - ]; - } - - // Clear snapshot cache for this VM - $this->clearSnapshotCache($product); - - return [ - 'error' => false, - 'data' => $Snapshot - ]; - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - return [ - 'error' => true, - 'message' => $error->getMessage() - ]; - } - } - return "Login_Again"; - } - - private function clearSnapshotCache($product): void - { - $vmware_name = (string) $product->name; - $cacheKey = "snapshots_{$product->id}_{$vmware_name}"; - Cache::forget($cacheKey); - Log::channel('Custom')->info("🗑️ Cleared snapshot cache for VM: {$vmware_name}"); - } - - public function getSnapShot($product): string|array - { - $startTime = microtime(true); - - try { - // Sử dụng helper method để resolve vCenter từ product - $vcenter = $this->resolveVCenterFromProduct($product); - - if (!$vcenter) { - return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; - } - - $this->username = $vcenter->user; - $this->password = SignRSA::DecryptFunc($vcenter->password); - $this->apiUri = $vcenter->host; - } catch (\Exception $e) { - return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()]; - } - - $vmware_name = (string) $product->name; - $vmId = $product->vmware_id; - - // Check cache first - $cacheKey = "snapshots_{$product->id}_{$vmware_name}"; - $cachedResult = Cache::get($cacheKey); - - if ($cachedResult !== null) { - Log::channel('Custom')->info("📸 Snapshot cache hit"); - return $cachedResult; - } - - try { - Log::channel('Custom')->info("🚀 Starting SOAP Snapshot Fetch for VM: $vmId ($vmware_name)"); - - // 1. SOAP Login - $sessionToken = $this->soapLogin(); - if (!$sessionToken) { - return "Login_Again"; - } - - // 2. Retrieve Snapshot Info - $snapshotInfo = $this->getVMSnapshotInfoSoap($sessionToken, $vmId); - - // 3. Flatten Tree - $snapshots = []; - if (isset($snapshotInfo['rootSnapshotList'])) { - // Handle single object vs array in XML to JSON conversion - $roots = $snapshotInfo['rootSnapshotList']; - // Ensure it's an array if single - if (isset($roots['snapshot'])) { - // It's a single node structure, allow recurse - $snapshots = $this->flattenSnapshotTree($roots); - } elseif (is_array($roots)) { - foreach ($roots as $root) { - $snapshots = array_merge($snapshots, $this->flattenSnapshotTree($root)); - } - } - } - - // Format similar to old output - $formattedSnapshots = []; - foreach ($snapshots as $snap) { - $formattedSnapshots[] = [ - 'name' => $snap['name'], - 'description' => $snap['description'] ?? '', - 'created' => $snap['createTime'], - 'id' => $snap['snapshot']['_'] ?? $snap['snapshot'], // MO Ref Value - 'is_current' => false, // Simplification, need logic to check against currentSnapshot - 'parent_snapshot_id' => '', // Hard to determining flat parent in simple recursion without passing parent id - 'size_gb' => 0 // SOAP doesnt always return size in tree - ]; - } - - $response = [ - 'error' => false, - 'data' => $formattedSnapshots - ]; - - Cache::put($cacheKey, $response, now()->addMinutes(5)); - return $response; - - } catch (\Exception $e) { - Log::channel('Custom')->error("❌ SOAP Snapshot Error: " . $e->getMessage()); - return [ - 'error' => true, - 'message' => 'Lỗi lấy snapshot qua SOAP: ' . $e->getMessage() - ]; - } - } - - private function soapLogin() - { - $body = ' - - - <_this type="SessionManager">SessionManager - ' . htmlspecialchars($this->username) . ' - ' . htmlspecialchars($this->password) . ' - - '; - - $response = $this->callSoap($body); - - // Extract session cookie if needed, but Login typically sets checking cookie. - // Actually for Http Client, we need to grab the cookie "vmware_soap_session" - // But the callSoap helper needs to handle cookies. - - return $response['cookie'] ?? null; - } - - private function getVMSnapshotInfoSoap($cookie, $vmId) - { - $body = ' - - - <_this type="PropertyCollector">propertyCollector - - - VirtualMachine - false - snapshot - - - ' . $vmId . ' - - - - - '; - - $result = $this->callSoap($body, $cookie); - - // Parse simple XML response to finding snapshot info - // This is tricky with Regex/SimpleXML. We need to extract the returnval. - - if (isset($result['body'])) { - $xml = simplexml_load_string($result['body']); - $xml->registerXPathNamespace('u', 'urn:vim25'); - // Navigate to returnval -> objects -> propSet -> val -> rootSnapshotList - // Path depends on exact response structure - - // Simplified return for now: - $json = json_encode($xml); - $array = json_decode($json, true); - - // Deep dive into array to find "snapshot" property - // This needs robust traversing - return $this->findSnapshotPropertyInArray($array); - } - return []; - } - - private function findSnapshotPropertyInArray($array) - { - // Recursive search for 'rootSnapshotList' or 'currentSnapshot' - if (isset($array['rootSnapshotList'])) - return $array; - - foreach ($array as $key => $value) { - if (is_array($value)) { - $found = $this->findSnapshotPropertyInArray($value); - if ($found) - return $found; - } - } - return []; - } - - private function flattenSnapshotTree($node) - { - $list = []; - // Add self - $list[] = $node; - - // Add children - if (isset($node['childSnapshotList'])) { - $children = $node['childSnapshotList']; - // Check if single child or array of children - if (isset($children['snapshot'])) { - $list = array_merge($list, $this->flattenSnapshotTree($children)); - } else { - foreach ($children as $child) { - $list = array_merge($list, $this->flattenSnapshotTree($child)); - } - } - } - return $list; - } - - private function callSoap($soapBody, $cookie = null) - { - $envelope = ' - - ' . $soapBody . ' - '; - - $httpHeaders = [ - 'Content-Type: text/xml; charset=utf-8', - 'SOAPAction: "urn:vim25/6.0"' - ]; - - if ($cookie) { - $httpHeaders[] = "Cookie: {$cookie}"; - } - - // Use cURL instead of Laravel Http to avoid HTTP/2 issues with vCenter - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, "https://{$this->apiUri}/sdk"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $envelope); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_HEADER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders); - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); - $headers = substr($response, 0, $headerSize); - $body = substr($response, $headerSize); - curl_close($ch); - - // Parse Set-Cookie header - $responseCookie = null; - if (preg_match('/Set-Cookie:\s*([^\r\n]+)/i', $headers, $matches)) { - $responseCookie = trim($matches[1]); - } - - Log::channel('Custom')->debug('SOAP call', [ - 'http_code' => $httpCode, - 'has_cookie' => !empty($responseCookie), - 'body_preview' => substr($body, 0, 200) - ]); - - return [ - 'body' => $body, - 'cookie' => $responseCookie ?: $cookie - ]; - } - - - - public function RemoveSnapShot($product, $snap = ''): string|array - { - // Sử dụng helper method để resolve vCenter từ product - $vcenter = $this->resolveVCenterFromProduct($product); - - if (!$vcenter) { - return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; - } - - $this->username = $vcenter->user; - $this->password = SignRSA::DecryptFunc($vcenter->password); - - // Login to get token - $loginResult = $this->loginServer($vcenter); - if (!$loginResult) { - return "Login_Again"; - } - - $vmware_name = (string) $product->name; - $check = $this->checkApiExits(); - if ($check) { - try { - - Log::channel('Custom')->info("==================================================================="); - - Log::channel('Custom')->info("Remove Snapshot with VirtualMachine Name - $vmware_name - Snap #$snap"); - - Log::channel('Custom')->info("\r\n"); - - $Command = "& 'C:\CLISave\deletesnap.ps1' -vm_name '$vmware_name' -name '$snap'"; - - Log::channel('Custom')->info("$Command"); - - $cmdResult = $this->Command($Command); - - Log::channel('Custom')->info("Command Result: " . $cmdResult); - - // Check if command failed - if (strpos($cmdResult, 'Login_Fail') !== false) { - return [ - 'error' => true, - 'message' => 'SSH Login to PowerShell server failed' - ]; - } - - if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) { - return [ - 'error' => true, - 'message' => 'PowerShell command failed: ' . $cmdResult - ]; - } - - Log::channel('Custom')->info("\r\n"); - - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - - Log::channel('Custom')->info("==================================================================="); - - // Clear snapshot cache for this VM - $this->clearSnapshotCache($product); - - return [ - 'error' => false - ]; - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - return [ - 'error' => true, - 'message' => $error->getMessage() - ]; - } - } - return "Login_Again"; - } - - public function RevertSnapShot($product, $snap = ''): string|array - { - // Sử dụng helper method để resolve vCenter từ product - $vcenter = $this->resolveVCenterFromProduct($product); - - if (!$vcenter) { - return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; - } - - $this->username = $vcenter->user; - $this->password = SignRSA::DecryptFunc($vcenter->password); - $this->apiUri = $vcenter->host; - - // Login to get token - $loginResult = $this->loginServer($vcenter); - if (!$loginResult) { - return "Login_Again"; - } - - $vmware_name = (string) $product->name; - $check = $this->checkApiExits(); - if ($check) { - try { - - Log::channel('Custom')->info("==================================================================="); - - Log::channel('Custom')->info("Revert Snapshot with VirtualMachine Name - $vmware_name - Snap #$snap"); - - Log::channel('Custom')->info("\r\n"); - - $Command = "& 'C:\CLISave\backSnap.ps1' -vm_name '$vmware_name' -name '$snap'"; - - Log::channel('Custom')->info("$Command"); - - $cmdResult = $this->Command($Command); - - Log::channel('Custom')->info("Command Result: " . $cmdResult); - - // Check if command failed - if (strpos($cmdResult, 'Login_Fail') !== false) { - return [ - 'error' => true, - 'message' => 'SSH Login to PowerShell server failed' - ]; - } - - if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) { - return [ - 'error' => true, - 'message' => 'PowerShell command failed: ' . $cmdResult - ]; - } - - Log::channel('Custom')->info("\r\n"); - - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - - Log::channel('Custom')->info("==================================================================="); - - // Clear snapshot cache for this VM - $this->clearSnapshotCache($product); - - return [ - 'error' => false - ]; - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - return [ - 'error' => true, - 'message' => $error->getMessage() - ]; - } - } - return "Login_Again"; - } - - public function UpgradeMemoryVirtualMachine($vmware_id = '', $ram = 0): array|string - { - $Check = $this->checkApiExits(); - - if ($Check) { - $RAMConfig = [ - "spec" => array( - "size_MiB" => $ram * $this->memory, - ) - ]; - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->patch("https://$this->apiUri/rest/vcenter/vm/$vmware_id/hardware/memory", $RAMConfig); - - if ($response->status() === 200) { - return [ - 'error' => false, - 'data' => 'success' - ]; - } - return [ - 'error' => true, - 'data' => $response['value'] - ]; - } - return "Login_Again"; - } - - public function UpgradeCPUVirtualMachine($vmware_id = '', $cpu = 0): array|string - { - - $Check = $this->checkApiExits(); - - if ($Check) { - $CPUConfig = [ - "spec" => array( - "cores_per_socket" => 1, - "count" => $cpu - ) - ]; - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->patch("https://$this->apiUri/rest/vcenter/vm/$vmware_id/hardware/cpu", $CPUConfig); - - if ($response->status() === 200) { - return [ - 'error' => false, - 'data' => 'success' - ]; - } - return [ - 'error' => true, - 'data' => $response['value'] - ]; - } - return "Login_Again"; - } - - public function UpgradeDiskVirtualMachine($product, $disk = 0): array|string - { - $this->username = $product->IpAddress->Host->Datacenter->vCenter->user; - $this->password = SignRSA::DecryptFunc($product->IpAddress->Host->Datacenter->vCenter->password); - $vmware_id = $product->vmware_id; - $check = $this->checkApiExits(); - if ($check) { - try { - - Log::channel('Custom')->info("==================================================================="); - - Log::channel('Custom')->info("Upgrade Disk with VirtualMachine ID # - $vmware_id"); - - Log::channel('Custom')->info("\r\n"); - - $Command = "\$vm = 'VirtualMachine-$vmware_id'; \$disksize = $disk; cd /; C:\CLISave\diskresize.ps1;"; - - Log::channel('Custom')->info("$Command"); - - $this->Command($Command); - - Log::channel('Custom')->info("\r\n"); - - Log::channel('Custom')->info("After run Command |" . Carbon::now()); - - Log::channel('Custom')->info("==================================================================="); - - return [ - 'error' => false - ]; - } catch (\Exception $error) { - Log::channel('Custom')->info("\r\n"); - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Error Log: "); - Log::channel('Custom')->info($error->getMessage()); - Log::channel('Custom')->info("==================================================================="); - return [ - 'error' => true, - 'message' => $error->getMessage() - ]; - } - } - return "Login_Again"; - } - - public function FindVirtualMachine($name = ''): string|bool - { - $check = $this->checkApiExits(); - - if ($check) { - try { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get( - "https://$this->apiUri/rest/vcenter/vm?filter.names.1=$name" - )->json(); - } catch (ConnectionException $error) { - return false; - } - - if ($response['value']) { - return $response['value'][0]['vm']; - } - return false; - } - return "Login_Again"; + $this->apiUri = $apiUri; + $this->token = $token; + $this->client->apiUri = $apiUri; + $this->client->token = $token; + return $this; } protected function SetConfig($data = []): static { - - $this->username = $data['user']; - $this->password = SignRSA::DecryptFunc($data['password']); + $this->client->SetConfig($data); + $this->username = $this->client->getUsername(); + $this->password = $this->client->getPassword(); return $this; } - // New Version Update 07.02.2025 + public function resolveVCenterFromProduct(\App\Models\ModuleVmware $product): ?\App\Models\vCenter + { + return $this->client->resolveVCenterFromProduct($product); + } + + // ─── Resources ─────────────────────────────────────────────────────────── + + public function getAllDatacenter() + { + return $this->resource->getAllDatacenter(); + } + public function getAllHost($datacenter = '') + { + return $this->resource->getAllHost($datacenter); + } + public function getAllFolder($datacenter = '') + { + return $this->resource->getAllFolder($datacenter); + } + public function getAllDataStore($host = '') + { + return $this->resource->getAllDataStore($host); + } + public function loadDatastore($data = [], $host_id = '') + { + return $this->resource->loadDatastore($host_id); + } + public function loadNetWork($data = [], $host_id = '') + { + return $this->resource->loadNetWork($host_id); + } + public function getAllNetwork($host = '') + { + return $this->resource->getNetwork($host); + } + public function getNetwork($host = ''): array|bool|string + { + return $this->resource->getNetwork($host); + } + + // ─── Templates ─────────────────────────────────────────────────────────── + + public function getAllTemplate() + { + return $this->template->getAllTemplate(); + } + public function getAllVersion($library = ''): bool|array|string + { + return $this->template->getAllVersion($library); + } + public function getTemplateFromDatastore($datastore = '') + { + return $this->template->getTemplateFromDatastore($datastore); + } + + // ─── VM Lifecycle ───────────────────────────────────────────────────────── + + public function getAllVirtualMachine($datacenter = '', $host = '') + { + return $this->vm->getAllVirtualMachine($datacenter); + } + public function findVirtualMachineByName(string $vmName): ?array + { + return $this->vm->findVirtualMachineByName($vmName); + } + public function FindVirtualMachine($name = ''): string|bool + { + return $this->vm->FindVirtualMachine($name); + } + public function CreateVirtualMachine($product_data): array|string + { + return $this->vm->CreateVirtualMachine($product_data); + } + public function CheckVirtualMachine($vmware_id = ''): string|array + { + return $this->vm->CheckVirtualMachine($vmware_id); + } + public function GetDetailVirtualMachine($vmware_id = ''): array + { + return $this->vm->GetDetailVirtualMachine($vmware_id); + } + public function GetIPAddressVirtualMachine($vmware_id = ''): array + { + return $this->vm->GetIPAddressVirtualMachine($vmware_id); + } + public function GetNetworkVirtualMachine($vmware_id = ''): array + { + return $this->vm->GetNetworkVirtualMachine($vmware_id); + } + public function ConfigNetworkVirtualMachine($vmware_id = '', $config = []): int + { + return $this->vm->ConfigNetworkVirtualMachine($vmware_id, $config); + } + public function GetVMStats($vmware_id = ''): array + { + return $this->vm->GetVMStats($vmware_id); + } + public function reBuildVPS(string $vmName): array|string + { + return $this->vm->reBuildVPS($vmName); + } + public function UpgradeMemoryVirtualMachine($vmware_id = '', $ram = 0): array|string + { + return $this->vm->UpgradeMemoryVirtualMachine($vmware_id, $ram); + } + public function UpgradeCPUVirtualMachine($vmware_id = '', $cpu = 0): array|string + { + return $this->vm->UpgradeCPUVirtualMachine($vmware_id, $cpu); + } + public function UpgradeDiskVirtualMachine($product, $disk = 0): array|string + { + return $this->vm->UpgradeDiskVirtualMachine($product, $disk); + } + + // ─── Power Management ──────────────────────────────────────────────────── + + public function StartVirtualMachine($product, $vmware_id = ''): array|string + { + return $this->power->StartVirtualMachine($product, $vmware_id); + } + public function ShutdownGuestVirtualMachine($product, $vmware_id = ''): array|string + { + return $this->power->ShutdownGuestVirtualMachine($product, $vmware_id); + } + public function PowerOffVirtualMachine($vmware_id = ''): array|string + { + return $this->power->PowerOffVirtualMachine($vmware_id); + } + public function RebootVirtualMachine($product, $vmware_id = ''): array|string + { + return $this->power->RebootVirtualMachine($product, $vmware_id); + } + public function restartVM($product, $vmware_id = ''): array|string + { + return $this->power->restartVM($product, $vmware_id); + } + public function SuspendVirtualMachine($vmware_id = ''): array|string + { + return $this->power->SuspendVirtualMachine($vmware_id); + } + public function getVmPowerState($product, string $vmware_id = ''): array|string + { + return $this->power->getVmPowerState($product, $vmware_id); + } + public function DeleteVirtualMachine($vmware_id = ''): array|string + { + return $this->power->DeleteVirtualMachine($vmware_id); + } + public function GetConsoleTickets($vmware_id = ''): array|string + { + return $this->power->GetConsoleTickets($vmware_id); + } + + // ─── Snapshots ─────────────────────────────────────────────────────────── + + public function CreateSnapShot($product, ?string $snapshotName = null): string|array + { + return $this->snapshot->CreateSnapShot($product); + } + public function getSnapShot($product): string|array + { + return $this->snapshot->getSnapShot($product); + } + public function RemoveSnapShot($product, $snap = ''): string|array + { + return $this->snapshot->RemoveSnapShot($product, $snap); + } + public function RevertSnapShot($product, $snap = ''): string|array + { + return $this->snapshot->RevertSnapShot($product, $snap); + } + + // ─── VM OS Configuration ───────────────────────────────────────────────── + + public function ConfigVirtualMachineUbuntu($vmware_id = '', $product = null, $password = ''): void + { + $this->vmConfig->ConfigVirtualMachineUbuntu($vmware_id, $product, $password); + } + public function ConfigCentOSVersion8($vmware_id = '', $product = [], $password = ''): void + { + $this->vmConfig->ConfigCentOSVersion8($vmware_id, $product, $password); + } + public function ConfigCentOSVersion7($vmware_id = '', $product = [], $password = ''): void + { + $this->vmConfig->ConfigCentOSVersion7($vmware_id, $product, $password); + } + public function ConfigWindowsVersion10($vmware_id = '', $product = [], $password = ''): void + { + $this->vmConfig->ConfigWindowsVersion10($vmware_id, $product, $password); + } + public function ConfigWindowsServer($vmware_id = '', $product = [], $password = ''): void + { + $this->vmConfig->ConfigWindowsServer($vmware_id, $product, $password); + } + public function ConfigFixWindowsClick($vmware_id = '', $product = []): void + { + $this->vmConfig->ConfigFixWindowsClick($vmware_id, $product); + } + public function ConfigRockyLinuxVersion9($vmware_id = '', $product = [], $password = ''): void + { + $this->vmConfig->ConfigRockyLinuxVersion9($vmware_id, $product, $password); + } + + // ─── Credentials & PowerShell ──────────────────────────────────────────── + + public function resetGuestPasswordViaPs1( + string $vmName, + string $guestUser, + string $guestPassword, + string $newPassword, + string $targetUser = 'Administrator', + ): array|string { + return $this->shell->resetGuestPasswordViaPs1($vmName, $guestUser, $guestPassword, $newPassword, $targetUser); + } + + public function changeRDPPortWindows( + string $vmName, + string $guestUser, + string $guestPassword, + string $newPort, + ): array|string { + return $this->shell->changeRDPPortWindows($vmName, $guestUser, $guestPassword, $newPort); + } + + public function changeIpVM( + string $vmName, + string $vmwareId, + string $ipAddress, + string $gateway, + string $osType, + string $guestUser = '', + string $guestPassword = '', + string $dns = '8.8.8.8', + string $hostname = '', + ): string { + return $this->shell->changeIpVM( + $vmName, + $vmwareId, + $ipAddress, + $gateway, + $osType, + $guestUser, + $guestPassword, + $dns, + $hostname, + ); + } + + public function getWindowsRdpPort(string $vmwareId, string $guestUser, string $guestPassword): ?int + { + return $this->shell->getWindowsRdpPort($vmwareId, $guestUser, $guestPassword); + } + + // ─── Legacy VPS Direct SSH ──────────────────────────────────────────────── + // These methods use direct SSH to guest VM, not via jumphost. + protected function loginVPS(array|null $data = []): ?SSH2 { try { - // Lấy thông tin từ $data - Không có thì lấy giá trị mặc định $user = $data['user'] ?? 'root'; $ip = $data['ip'] ?? '103.42.58.37'; $password = $data['password'] ?? 'TGS@1232'; - // Kiểm tra hệ điều hành - if (str_contains($this->os, "Windows")) { - $user = $data['user'] ?? 'administartor'; - $this->process = new WinRMClient($ip, $user, $password); - ; - return $this->process; - } - // Tạo kết nối SSH $this->process = new SSH2($ip, 22, 10); - - // Kiểm tra đăng nhập if (!$this->process->login($user, $password)) { throw new Exception("Cannot login to VPS with IP: $ip, User: $user"); } - - Log::info("Logged into VPS successfully: IP = $ip, User = $user"); return $this->process; } catch (Exception $exception) { - // Ghi log lỗi chi tiết Log::error('Login VPS failed: ' . $exception->getMessage()); $this->process = null; return null; } } - protected function config_hosname_linux(string $hostname = '') - { + public function change_password_linux( + array $data = ['ip' => '10.10.10.173', 'user' => 'admin', 'password' => 'TGS@123', 'new_password' => ''], + ): bool { try { - // Xử lý hostname - $hostname = preg_replace('/[^a-zA-Z0-9-]/', '', $hostname); - $hostname = trim($hostname, '-'); - $hostname = substr($hostname, 0, 63); - - // Đăng nhập VPS - $this->loginVPS(); - if (!$this->process) { - throw new Exception("Cannot login to VPS"); - } - // Set Host name - $this->process->exec("hostnamectl set-hostname " . escapeshellarg($hostname)); - - Log::info("Hostname changed successfully to: $hostname"); - - return true; - } catch (Exception $exception) { - - Log::error('Failed to change hostname: ' . $exception->getMessage()); - - return false; - } - } - - protected function set_ip_linux(string $ipaddress = '', string $gateway = ''): bool - { - try { - // Kiểm tra IP address - if (!filter_var($ipaddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - throw new Exception("Invalid IP address: $ipaddress"); - } - - // Kiểm tra Gateway - if (!filter_var($gateway, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - throw new Exception("Invalid Gateway address: $gateway"); - } - - // Đăng nhập VPS - $this->loginVPS(); - if (!$this->process) { - throw new Exception("Cannot login to VPS"); - } - - // Thực hiện thay đổi IP và Gateway - $output1 = $this->process->exec("nmcli con mod ens192 ipv4.addresses $ipaddress/24"); - $output2 = $this->process->exec("nmcli con mod ens192 ipv4.gateway $gateway"); - $output3 = $this->process->exec("nmcli con up ens192"); - - if (!$output1 || !$output2 || !$output3) { - throw new Exception("Failed to execute one or more network commands."); - } - - Log::info("IP Address changed successfully to: $ipaddress with Gateway: $gateway"); - return true; - } catch (Exception $exception) { - Log::error('Failed to configure IP address: ' . $exception->getMessage()); - return false; - } - } - /* - * ip = 103.42.58.37 - * pwd = TGS@123 - */ - public function change_password_linux(array $data = ['ip' => '10.10.10.173', 'user' => 'admin', 'password' => 'TGS@123', 'new_password' => '']): bool - { - try { - // Kiểm tra mật khẩu mới if (empty($data['new_password'])) { - throw new Exception("New password cannot be empty."); + throw new Exception('New password cannot be empty.'); } - - // Đăng nhập VPS $this->loginVPS($data); if (!$this->process) { - throw new Exception("Cannot login to VPS."); + throw new Exception('Cannot login to VPS.'); } - - // Lấy thông tin từ $data $username = $data['user'] ?? 'root'; $newPassword = $data['new_password']; - - // Đảm bảo username không rỗng - if (empty($username)) { - throw new Exception("Username is invalid."); - } - - // Đổi mật khẩu - $this->process->exec("echo " . escapeshellarg("$username:$newPassword") . " | chpasswd"); - - // Log trạng thái - Log::info("Password for user $username changed successfully."); - + $this->process->exec('echo ' . escapeshellarg("$username:$newPassword") . ' | chpasswd'); return true; } catch (Exception $exception) { - // Ghi log lỗi Log::error('Failed to change password: ' . $exception->getMessage()); + return false; + } + } + public function change_password_windows( + array $data = [ + 'ip' => '103.42.58.37', + 'user' => 'administrator', + 'password' => 'TGS@123', + 'new_password' => '', + ], + ): bool { + try { + if (empty($data['new_password'])) { + throw new Exception('New password cannot be empty.'); + } + $this->loginVPS($data); + if (!$this->process) { + throw new Exception('Cannot login to VPS.'); + } + $username = $data['user'] ?? 'administrator'; + $newPassword = $data['new_password']; + $response = $this->process->exec("net user \"$username\" \"$newPassword\""); + if (stripos($response, 'successfully') === false) { + throw new Exception("Failed to change password. Response: $response"); + } + return true; + } catch (Exception $exception) { + Log::error('Failed to change password: ' . $exception->getMessage()); return false; } } @@ -2483,30 +411,21 @@ class vCenter public function extend_disk_centos_7(): bool { try { - // Đăng nhập VPS $this->loginVPS(); if (!$this->process) { - throw new Exception("Cannot login to VPS"); + throw new Exception('Cannot login to VPS'); } - - // Kiểm tra xem thiết bị `/dev/sda2` có tồn tại không $checkDisk = $this->process->exec("lsblk | grep -w 'sda2'"); if (empty(trim($checkDisk))) { - throw new Exception("Disk /dev/sda2 not found"); + throw new Exception('Disk /dev/sda2 not found'); } - - // Thực hiện các lệnh để mở rộng đĩa - $this->process->exec("sudo tee /sys/class/block/sda/device/rescan"); - $this->process->exec("sudo pvcreate /dev/sda2"); - $this->process->exec("sudo vgextend /dev/centos /dev/sda2"); - $this->process->exec("sudo lvextend /dev/centos/root /dev/sda2"); - $this->process->exec("sudo xfs_growfs /dev/centos/root"); - - Log::info("Disk extended successfully"); - + $this->process->exec('sudo tee /sys/class/block/sda/device/rescan'); + $this->process->exec('sudo pvcreate /dev/sda2'); + $this->process->exec('sudo vgextend /dev/centos /dev/sda2'); + $this->process->exec('sudo lvextend /dev/centos/root /dev/sda2'); + $this->process->exec('sudo xfs_growfs /dev/centos/root'); return true; } catch (Exception $exception) { - // Ghi log lỗi Log::error('Failed to extend disk: ' . $exception->getMessage()); return false; } @@ -2515,29 +434,20 @@ class vCenter public function extend_disk_centos_8(): bool { try { - // Đăng nhập VPS $this->loginVPS(); if (!$this->process) { - throw new Exception("Cannot login to VPS"); + throw new Exception('Cannot login to VPS'); } - - // Kiểm tra xem thiết bị `/dev/sda2` có tồn tại không $checkDisk = $this->process->exec("lsblk | grep -w 'sda3'"); if (empty(trim($checkDisk))) { - throw new Exception("Disk /dev/sda3 not found"); + throw new Exception('Disk /dev/sda3 not found'); } - - // Thực hiện các lệnh để mở rộng đĩa - $this->process->exec("sudo pvcreate /dev/sda3"); - $this->process->exec("sudo vgextend /dev/cl /dev/sda3"); - $this->process->exec("sudo lvextend /dev/cl/root /dev/sd3"); - $this->process->exec("sudo xfs_growfs /dev/cl/root"); - - Log::info("Disk extended successfully"); - + $this->process->exec('sudo pvcreate /dev/sda3'); + $this->process->exec('sudo vgextend /dev/cl /dev/sda3'); + $this->process->exec('sudo lvextend /dev/cl/root /dev/sd3'); + $this->process->exec('sudo xfs_growfs /dev/cl/root'); return true; } catch (Exception $exception) { - // Ghi log lỗi Log::error('Failed to extend disk: ' . $exception->getMessage()); return false; } @@ -2546,565 +456,22 @@ class vCenter public function extend_disk_ubuntu(): bool { try { - // Đăng nhập VPS $this->loginVPS(); if (!$this->process) { - throw new Exception("Cannot login to VPS"); + throw new Exception('Cannot login to VPS'); } - - // Kiểm tra xem thiết bị `/dev/sda3` có tồn tại không $checkDisk = $this->process->exec("lsblk | grep -w 'sda3'"); if (empty(trim($checkDisk))) { - throw new Exception("Disk /dev/sda3 not found"); + throw new Exception('Disk /dev/sda3 not found'); } - - // Thực hiện các lệnh để mở rộng đĩa - $this->process->exec("sudo tee /sys/class/block/sda/device/rescan"); - $this->process->exec("sudo growpart /dev/sda 3"); - $this->process->exec("sudo pvresize /dev/sda3"); - $this->process->exec("sudo lvresize -l+100%FREE --resizefs /dev/mapper/ubuntu--vg-ubuntu--lv"); - - Log::info("Disk extended successfully"); - + $this->process->exec('sudo tee /sys/class/block/sda/device/rescan'); + $this->process->exec('sudo growpart /dev/sda 3'); + $this->process->exec('sudo pvresize /dev/sda3'); + $this->process->exec('sudo lvresize -l+100%FREE --resizefs /dev/mapper/ubuntu--vg-ubuntu--lv'); return true; } catch (Exception $exception) { - // Ghi log lỗi Log::error('Failed to extend disk: ' . $exception->getMessage()); return false; } } - - public function set_ip_window(string $ipaddress = '', string $gateway = '') - { - try { - // Kiểm tra IP address - if (!filter_var($ipaddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - throw new Exception("Invalid IP address: $ipaddress"); - } - - // Kiểm tra Gateway - if (!filter_var($gateway, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - throw new Exception("Invalid Gateway address: $gateway"); - } - - // Đăng nhập VPS - $this->loginVPS(); - if (!$this->process) { - throw new Exception("Cannot login to VPS"); - } - - // Thực hiện thay đổi IP và Gateway - $output1 = $this->process->execute_command("netsh int ip set address Ethernet0 static $ipaddress 255.255.255.0 $gateway"); - - - if (!$output1) { - throw new Exception("Failed to execute one or more network commands."); - } - - Log::info("IP Address changed successfully to: $ipaddress with Gateway: $gateway"); - return true; - } catch (Exception $exception) { - Log::error('Failed to configure IP address: ' . $exception->getMessage()); - return false; - } - } - - public function change_password_windows(array $data = ['ip' => '103.42.58.37', 'user' => 'administrator', 'password' => 'TGS@123', 'new_password' => '']): bool - { - try { - // Kiểm tra mật khẩu mới - if (empty($data['new_password'])) { - throw new Exception("New password cannot be empty."); - } - - // Đăng nhập VPS - $this->loginVPS($data); - if (!$this->process) { - throw new Exception("Cannot login to VPS."); - } - - // Lấy thông tin từ $data - $username = $data['user'] ?? 'administrator'; - $newPassword = $data['new_password']; - - // Đảm bảo username không rỗng - if (empty($username)) { - throw new Exception("Username is invalid."); - } - - // Đổi mật khẩu - $response = $this->process->execute_command("net user \"$username\" \"$newPassword\""); - - if (stripos($response, 'successfully') === false) { - throw new Exception("Failed to change password. Response: $response"); - } - - Log::info("Password changed successfully for user: $username"); - - return true; - } catch (Exception $exception) { - // Ghi log lỗi - Log::error('Failed to change password: ' . $exception->getMessage()); - - return false; - } - } - - /** - * Test method để debug addon updates - */ - public function testAddonUpdate($vmId, $addon, $base_cpu = 1, $base_ram = 1): array - { - try { - Log::info("🧪 TEST: Bắt đầu test addon update", [ - 'vm_id' => $vmId, - 'addon' => $addon, - 'base_cpu' => $base_cpu, - 'base_ram' => $base_ram - ]); - - $this->updateVMHardware($vmId, $addon, $base_cpu, $base_ram); - - return [ - 'error' => false, - 'message' => 'Test addon update completed' - ]; - } catch (\Exception $e) { - Log::error("🧪 TEST: Lỗi trong test addon update: " . $e->getMessage()); - return [ - 'error' => true, - 'message' => $e->getMessage() - ]; - } - } - - /** - * Test method để debug getSnapShot function - */ - public function testGetSnapShot($product): array - { - try { - Log::info("🧪 TEST: Bắt đầu test getSnapShot", [ - 'product_id' => $product->id ?? 'unknown', - 'product_name' => $product->name ?? 'unknown', - 'vmware_id' => $product->vmware_id ?? 'unknown' - ]); - - $result = $this->getSnapShot($product); - - Log::info("🧪 TEST: Kết quả getSnapShot", [ - 'result' => $result, - 'product_id' => $product->id ?? 'unknown' - ]); - - return [ - 'error' => false, - 'message' => 'Test getSnapShot completed', - 'result' => $result - ]; - } catch (\Exception $e) { - Log::error("🧪 TEST: Lỗi trong test getSnapShot: " . $e->getMessage()); - return [ - 'error' => true, - 'message' => $e->getMessage() - ]; - } - } - - /** - * Assign IP to Windows VM using PowerShell script - */ - public function assignIPWindows($product, $vmware_id = ''): array|string - { - try { - // Support both old Product structure and new ModuleVmware structure - $ipAddress = null; - $gateway = null; - $netmask = '255.255.255.0'; - $dns = '8.8.8.8'; - $vmware_name = ''; - $vCenter = null; - - // Check if it's ModuleVmware (new structure) - if ($product && isset($product->ipAddress) && $product->ipAddress) { - Log::info('✅ Using ModuleVmware structure for assignIPWindows', [ - 'module_vmware_id' => $product->id, - 'vmware_id' => $vmware_id - ]); - - // Extract from ModuleVmware->ipAddress relationship - $ipAddress = $product->ipAddress->ip ?? null; - $gateway = $product->ipAddress->gateway ?? null; - $netmask = $product->ipAddress->netmask ?? '255.255.255.0'; - $dns = $product->ipAddress->dns ?? '8.8.8.8'; - $vmware_name = $product->name ?? $vmware_id; - $vCenter = $product->ipAddress->host->datacenter->vCenter ?? null; - } elseif ($product && isset($product->IpAddress) && $product->IpAddress) { - Log::info('✅ Using old Product structure for assignIPWindows', [ - 'product_id' => $product->id, - 'vmware_id' => $vmware_id - ]); - - // Extract from old Product->IpAddress relationship - $ipAddress = $product->IpAddress->ip ?? null; - $gateway = $product->IpAddress->gateway ?? null; - $netmask = $product->IpAddress->netmask ?? '255.255.255.0'; - $dns = $product->IpAddress->dns ?? '8.8.8.8'; - $vmware_name = $product->name ?? $vmware_id; - $vCenter = $product->IpAddress->Host->Datacenter->vCenter ?? null; - } - - // Validate required data - if (!$ipAddress || !$gateway || !$vCenter) { - Log::error('❌ assignIPWindows: Missing required data', [ - 'vmware_id' => $vmware_id, - 'ip_address' => $ipAddress, - 'gateway' => $gateway, - 'has_vcenter' => $vCenter ? 'yes' : 'no', - 'product_class' => get_class($product) - ]); - return [ - 'error' => true, - 'data' => 'Missing required data for IP assignment (IP, Gateway, or vCenter)' - ]; - } - - $this->SetConfig($vCenter); - - // Convert netmask to prefix length (e.g., 255.255.255.0 = 24) - $maskBits = $this->convertNetmaskToPrefixLength($netmask); - - Log::channel('Custom')->info("==================================================================="); - Log::channel('Custom')->info("Assign IP to Windows VM - $vmware_name"); - Log::channel('Custom')->info("IP: $ipAddress, Gateway: $gateway, DNS: $dns, MaskBits: $maskBits"); - Log::channel('Custom')->info("\r\n"); - - // Create PowerShell command with dynamic values - $Command = "\$vmName = '$vmware_name'; \$IP = '$ipAddress'; \$MaskBits = $maskBits; \$Gateway = '$gateway'; \$DNS = '$dns'; cd /; C:\CLISave\AssignIP\assignIPWindows.ps1;"; - - Log::channel('Custom')->info("Executing Command: $Command"); - - $output = $this->Command($Command); - - Log::channel('Custom')->info("Script output: " . $output); - Log::channel('Custom')->info("==================================================================="); - - // Check if execution was successful - if (strpos($output, '🎉') !== false || strpos($output, 'successfully') !== false) { - return [ - 'error' => false, - 'data' => 'IP assigned successfully', - 'output' => $output - ]; - } else { - return [ - 'error' => true, - 'data' => 'Failed to assign IP', - 'output' => $output - ]; - } - } catch (\Exception $e) { - Log::channel('Custom')->error("Exception in assignIPWindows: " . $e->getMessage()); - return [ - 'error' => true, - 'data' => $e->getMessage() - ]; - } - } - - /** - * Convert netmask to prefix length (e.g., 255.255.255.0 = 24) - */ - private function convertNetmaskToPrefixLength($netmask): int - { - $netmaskMap = [ - '255.255.255.255' => 32, - '255.255.255.254' => 31, - '255.255.255.252' => 30, - '255.255.255.248' => 29, - '255.255.255.240' => 28, - '255.255.255.224' => 27, - '255.255.255.192' => 26, - '255.255.255.128' => 25, - '255.255.255.0' => 24, - '255.255.254.0' => 23, - '255.255.252.0' => 22, - '255.255.248.0' => 21, - '255.255.240.0' => 20, - '255.255.224.0' => 19, - '255.255.192.0' => 18, - '255.255.128.0' => 17, - '255.255.0.0' => 16, - '255.254.0.0' => 15, - '255.252.0.0' => 14, - '255.248.0.0' => 13, - '255.240.0.0' => 12, - '255.224.0.0' => 11, - '255.192.0.0' => 10, - '255.128.0.0' => 9, - '255.0.0.0' => 8 - ]; - - return $netmaskMap[$netmask] ?? 24; // Default to /24 if not found - } - - /** - * Parse snapshot output from PowerShell text format - */ - private function parseSnapshotTextOutput($output): array - { - $snapshots = []; - - // Split the output into individual snapshot blocks - $blocks = preg_split('/\n\s*\n/', $output); - - foreach ($blocks as $block) { - $block = trim($block); - if (empty($block)) - continue; - - // Skip header lines and warnings - if ( - strpos($block, 'Name Port User') !== false || - strpos($block, '---- ---- ----') !== false || - strpos($block, 'WARNING:') !== false - ) { - continue; - } - - $snapshot = []; - - // Parse each line of the snapshot block - $lines = explode("\n", $block); - foreach ($lines as $line) { - $line = trim($line); - if (empty($line)) - continue; - - // Parse key-value pairs - if (strpos($line, ' : ') !== false) { - list($key, $value) = explode(' : ', $line, 2); - $key = trim($key); - $value = trim($value); - - switch ($key) { - case 'Name': - $snapshot['name'] = $value; - break; - case 'Description': - $snapshot['description'] = $value; - break; - case 'Created': - $snapshot['created'] = $value; - break; - case 'PowerState': - $snapshot['power_state'] = $value; - break; - case 'VM': - $snapshot['vm_name'] = $value; - break; - case 'VMId': - $snapshot['vm_id'] = $value; - break; - case 'Parent': - $snapshot['parent'] = $value; - break; - case 'ParentSnapshotId': - $snapshot['parent_snapshot_id'] = $value; - break; - case 'ParentSnapshot': - $snapshot['parent_snapshot'] = $value; - break; - case 'Children': - $snapshot['children'] = $value; - break; - case 'SizeMB': - $snapshot['size_mb'] = (float) $value; - break; - case 'SizeGB': - $snapshot['size_gb'] = (float) $value; - break; - case 'IsCurrent': - $snapshot['is_current'] = strtolower($value) === 'true'; - break; - case 'IsReplaySupported': - $snapshot['is_replay_supported'] = strtolower($value) === 'true'; - break; - case 'Id': - $snapshot['id'] = $value; - break; - case 'Uid': - $snapshot['uid'] = $value; - break; - default: - // Store any other properties - $snapshot[strtolower(str_replace(' ', '_', $key))] = $value; - break; - } - } - } - - // Only add if we have at least a name - if (!empty($snapshot['name'])) { - $snapshots[] = $snapshot; - } - } - - return $snapshots; - } - - /** - * Parse snapshot output when JSON parsing fails (legacy method) - */ - private function parseSnapshotOutput($output): array - { - $snapshots = []; - - // Split by lines and look for snapshot information - $lines = explode("\n", $output); - - foreach ($lines as $line) { - $line = trim($line); - if (empty($line)) - continue; - - // Look for snapshot name patterns - if (preg_match('/(\w+_\d{2}_\d{2}_\d{4}_\d{2}_\d{2}_\d{2})/', $line, $matches)) { - $snapshots[] = [ - 'name' => $matches[1], - 'raw_line' => $line - ]; - } - } - - return $snapshots; - } - - - /* - * Get Window Remote Port - */ - public function getWindowsRdpPort(string $vmwareId, string $guestUser, string $guestPassword): ?int - { - $ps = "(Get-ItemProperty 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -Name PortNumber).PortNumber"; - - $cmd = "Invoke-VMScript -VM '$vmwareId' " - . "-ScriptText \"$ps\" " - . "-ScriptType Powershell " - . "-GuestUser '$guestUser' " - . "-GuestPassword '$guestPassword'"; - - $output = $this->Command($cmd); - - if (!$output || str_contains($output, 'Login_Fail')) { - return null; - } - - if (preg_match('/\b(\d{2,5})\b/', $output, $m)) { - return (int) $m[1]; - } - - return null; - } - public function reBuildVPS(string $vmName): array|string - { - if (!$this->checkApiExits()) { - return "Login_Again"; - } - - try { - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://{$this->apiUri}/rest/vcenter/vm", [ - 'filter.names' => $vmName - ]); - - if (!$response->successful()) { - return [ - 'error' => true, - 'message' => 'Failed to search for VM', - 'details' => $response->json() - ]; - } - - $vmList = $response->json()['value'] ?? []; - if (empty($vmList)) { - return [ - 'error' => true, - 'message' => "VM not found with name: {$vmName}" - ]; - } - - $vmId = $vmList[0]['vm']; - - $detailResponse = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $this->token - ]) - ->get("https://{$this->apiUri}/rest/vcenter/vm/{$vmId}"); - - if (!$detailResponse->successful()) { - return [ - 'error' => true, - 'message' => 'Failed to fetch VM details', - 'details' => $detailResponse->json() - ]; - } - - $vmData = $detailResponse->json(); - $guestOS = $vmData['value']['guest_OS'] ?? 'UNKNOWN'; - - $scriptPath = ''; - $osUpper = strtoupper($guestOS); - - if (Str::contains($osUpper, 'WINDOWS')) { - $scriptPath = 'C:\\CLISave\\Windows\\rebuild.ps1'; - } elseif (Str::contains($osUpper, ['UBUNTU', 'LINUX', 'CENTOS', 'DEBIAN'])) { - $scriptPath = 'C:\\CLISave\\Ubuntu\\rebuild.ps1'; - } else { - return [ - 'error' => true, - 'message' => "Unsupported OS for rebuild: {$guestOS}" - ]; - } - - $scriptDir = dirname($scriptPath); - $scriptFile = basename($scriptPath); - - $cmd = " - Write-Output 'STARTING_REBUILD'; - \$vm = Get-VM -Name '$vmName'; - cd '$scriptDir'; - if (Test-Path '.\\$scriptFile') { - Write-Output 'File found, executing...'; - & '.\\$scriptFile' | Out-String - } else { - Write-Error 'File NOT FOUND: $scriptPath'; - } - Write-Output 'ENDING_REBUILD'; - "; - - $result = $this->Command($cmd); - - - return [ - 'error' => false, - 'vm_name' => $vmName, - 'vm_id' => $vmId, - 'os' => $guestOS, - 'script' => $scriptPath, - 'result' => $result - ]; - - } catch (\Exception $e) { - Log::error("reBuildVPS Exception: " . $e->getMessage()); - return [ - 'error' => true, - 'message' => $e->getMessage() - ]; - } - } - } diff --git a/app/Http/Services/vCenter/Client.php b/app/Http/Services/vCenter/Client.php index ee64813..cf57fa0 100644 --- a/app/Http/Services/vCenter/Client.php +++ b/app/Http/Services/vCenter/Client.php @@ -66,4 +66,29 @@ class Client { return $this->password; } + + public function SetConfig($data): static + { + $this->username = $data['user'] ?? $data->user; + $this->password = \App\Http\Facade\SignRSA::DecryptFunc($data['password'] ?? $data->password); + return $this; + } + + public function resolveVCenterFromProduct(\App\Models\ModuleVmware $product): ?\App\Models\vCenter + { + if ($product->vCenter) { + return $product->vCenter; + } + if ($product->ipAddress?->host?->datacenter?->vCenter) { + return $product->ipAddress->host->datacenter->vCenter; + } + $service = $product->services()->first(); + if ($service) { + $service->load('vmware.vCenter'); + if ($service->vmware?->vCenter) { + return $service->vmware->vCenter; + } + } + return null; + } } diff --git a/app/Http/Services/vCenter/PowerManagementService.php b/app/Http/Services/vCenter/PowerManagementService.php index ed28d21..356c72c 100644 --- a/app/Http/Services/vCenter/PowerManagementService.php +++ b/app/Http/Services/vCenter/PowerManagementService.php @@ -170,4 +170,95 @@ class PowerManagementService } return "Login_Again"; } + + public function restartVM($product, $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 && isset($product->ipAddress) && $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', + ]; + } + + $check = $this->client->loginServer($vcenter); + if (!$check) { + return "Login_Again"; + } + + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->post("https://{$this->client->apiUri}/rest/vcenter/vm/$vmId/guest/power?action=reboot"); + + 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(), + ]; + } + } + + 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->client->loginServer($vcenter)) { + return 'Login_Again'; + } + + $response = Http::withoutVerifying() + ->withHeaders(['vmware-api-session-id' => $this->client->token]) + ->timeout(8) + ->get("https://{$this->client->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()]; + } + } } diff --git a/app/Http/Services/vCenter/PowerShellService.php b/app/Http/Services/vCenter/PowerShellService.php index 2738137..d1b9bfc 100644 --- a/app/Http/Services/vCenter/PowerShellService.php +++ b/app/Http/Services/vCenter/PowerShellService.php @@ -267,4 +267,74 @@ PS; return $data; } + + public function changeIpVM( + string $vmName, + string $vmwareId, + string $ipAddress, + string $gateway, + string $osType, + string $guestUser = '', + string $guestPassword = '', + string $dns = '8.8.8.8', + string $hostname = '' + ): string { + $q = fn(string $v) => str_replace("'", "''", $v); + + $isLinux = str_contains(strtolower($osType), 'linux') + || str_contains(strtolower($osType), 'ubuntu') + || str_contains(strtolower($osType), 'centos') + || str_contains(strtolower($osType), 'rocky'); + + if ($isLinux) { + $psFile = "C:\\CLISave\\changeIPLinux.ps1"; + $Command = "& '{$psFile}'" + . " -VMName '" . $q($vmName) . "'" + . " -IP '" . $q($ipAddress) . "'" + . " -Gateway '" . $q($gateway) . "'" + . " -DNS '" . $q($dns) . "'" + . " -GuestUser '" . $q($guestUser) . "'" + . " -GuestPassword '" . $q($guestPassword) . "'"; + + Log::channel('Custom')->info('[changeIpVM:linux] ' . $Command); + + return (string) $this->CommandChangePort($Command, true); + } + + $VirtualMachine = "VirtualMachine-$vmwareId"; + $hn = strtolower($hostname ?: $vmName); + $Command = "\$vm = get-vm -Id '{$VirtualMachine}';" + . " \$hostname = '{$q($hn)}';" + . " \$ipadd = '{$q($ipAddress)}';" + . " \$gw = '{$q($gateway)}';" + . " \$password = \"'{$q($guestPassword)}'\"; " + . " cd /; C:\\CLISave\\changeWindows1line.ps1;"; + + Log::channel('Custom')->info('[changeIpVM:windows] ' . $Command); + + return (string) $this->CommandRSP($Command); + } + + public function getWindowsRdpPort(string $vmwareId, string $guestUser, string $guestPassword): ?int + { + $ps = "(Get-ItemProperty 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server\\WinStations\\RDP-Tcp' -Name PortNumber).PortNumber"; + + $cmd = "Invoke-VMScript -VM '$vmwareId' " + . "-ScriptText \"$ps\" " + . "-ScriptType Powershell " + . "-GuestUser '$guestUser' " + . "-GuestPassword '$guestPassword'"; + + $output = $this->CommandRSP($cmd); + + if (!$output || str_contains($output, 'Login_Fail')) { + return null; + } + + if (preg_match('/\b(\d{2,5})\b/', $output, $m)) { + return (int) $m[1]; + } + + return null; + } } diff --git a/app/Http/Services/vCenter/ResourceService.php b/app/Http/Services/vCenter/ResourceService.php index 4b8b67b..f3d5dab 100644 --- a/app/Http/Services/vCenter/ResourceService.php +++ b/app/Http/Services/vCenter/ResourceService.php @@ -158,6 +158,12 @@ class ResourceService { return $this->getAllNetwork($host_id); } + + public function getNetwork($host = ''): array|bool|string + { + return $this->getAllNetwork($host); + } + public function GetFolderByHost($datacenter = '') { return \App\Models\ModuleFolder::where('module_datacenter_id', $datacenter)->first(); diff --git a/app/Http/Services/vCenter/SnapshotService.php b/app/Http/Services/vCenter/SnapshotService.php index 1fc0fd3..92e3f89 100644 --- a/app/Http/Services/vCenter/SnapshotService.php +++ b/app/Http/Services/vCenter/SnapshotService.php @@ -5,7 +5,6 @@ namespace App\Http\Services\vCenter; use App\Support\Cache\CacheKey; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; -use App\Http\Facade\SignRSA; class SnapshotService { @@ -291,6 +290,140 @@ class SnapshotService return $list; } + public function RemoveSnapShot($product, $snap = ''): string|array + { + $vcenter = $this->resolveVCenterFromProduct($product); + + if (!$vcenter) { + return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; + } + + $loginResult = $this->client->loginServer($vcenter); + if (!$loginResult) { + return "Login_Again"; + } + + $vmware_name = (string) $product->name; + $check = $this->client->checkApiExits(); + if ($check) { + try { + Log::channel('Custom')->info("==================================================================="); + Log::channel('Custom')->info("Remove Snapshot with VirtualMachine Name - $vmware_name - Snap #$snap"); + Log::channel('Custom')->info("\r\n"); + + $Command = "& 'C:\CLISave\deletesnap.ps1' -vm_name '$vmware_name' -name '$snap'"; + + Log::channel('Custom')->info("$Command"); + + $cmdResult = $this->powerShellService->executePowerCLI($Command); + + Log::channel('Custom')->info("Command Result: " . $cmdResult); + + if (strpos($cmdResult, 'Login_Fail') !== false) { + return [ + 'error' => true, + 'message' => 'SSH Login to PowerShell server failed' + ]; + } + + if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) { + return [ + 'error' => true, + 'message' => 'PowerShell command failed: ' . $cmdResult + ]; + } + + Log::channel('Custom')->info("\r\n"); + Log::channel('Custom')->info("After run Command |" . now()); + Log::channel('Custom')->info("==================================================================="); + + $this->clearSnapshotCache($product); + + return [ + 'error' => false + ]; + } catch (\Exception $error) { + Log::channel('Custom')->info("\r\n"); + Log::channel('Custom')->info("==================================================================="); + Log::channel('Custom')->info("Error Log: "); + Log::channel('Custom')->info($error->getMessage()); + Log::channel('Custom')->info("==================================================================="); + return [ + 'error' => true, + 'message' => $error->getMessage() + ]; + } + } + return "Login_Again"; + } + + public function RevertSnapShot($product, $snap = ''): string|array + { + $vcenter = $this->resolveVCenterFromProduct($product); + + if (!$vcenter) { + return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM']; + } + + $loginResult = $this->client->loginServer($vcenter); + if (!$loginResult) { + return "Login_Again"; + } + + $vmware_name = (string) $product->name; + $check = $this->client->checkApiExits(); + if ($check) { + try { + Log::channel('Custom')->info("==================================================================="); + Log::channel('Custom')->info("Revert Snapshot with VirtualMachine Name - $vmware_name - Snap #$snap"); + Log::channel('Custom')->info("\r\n"); + + $Command = "& 'C:\CLISave\backSnap.ps1' -vm_name '$vmware_name' -name '$snap'"; + + Log::channel('Custom')->info("$Command"); + + $cmdResult = $this->powerShellService->executePowerCLI($Command); + + Log::channel('Custom')->info("Command Result: " . $cmdResult); + + if (strpos($cmdResult, 'Login_Fail') !== false) { + return [ + 'error' => true, + 'message' => 'SSH Login to PowerShell server failed' + ]; + } + + if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) { + return [ + 'error' => true, + 'message' => 'PowerShell command failed: ' . $cmdResult + ]; + } + + Log::channel('Custom')->info("\r\n"); + Log::channel('Custom')->info("After run Command |" . now()); + Log::channel('Custom')->info("==================================================================="); + + $this->clearSnapshotCache($product); + + return [ + 'error' => false + ]; + } catch (\Exception $error) { + Log::channel('Custom')->info("\r\n"); + Log::channel('Custom')->info("==================================================================="); + Log::channel('Custom')->info("Error Log: "); + Log::channel('Custom')->info($error->getMessage()); + Log::channel('Custom')->info("==================================================================="); + return [ + 'error' => true, + 'message' => $error->getMessage() + ]; + } + } + return "Login_Again"; + } + private function callSoap($soapBody, $cookie = null) { $curl = curl_init(); diff --git a/app/Http/Services/vCenter/VirtualMachineService.php b/app/Http/Services/vCenter/VirtualMachineService.php index 2f92a1f..5df5d71 100644 --- a/app/Http/Services/vCenter/VirtualMachineService.php +++ b/app/Http/Services/vCenter/VirtualMachineService.php @@ -4,6 +4,8 @@ namespace App\Http\Services\vCenter; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Str; use App\Http\Services\Discord; use App\Models\ModuleDatastore; use Exception; @@ -11,10 +13,12 @@ use Exception; class VirtualMachineService { protected Client $client; + protected PowerShellService $powerShellService; - public function __construct(Client $client) + public function __construct(Client $client, PowerShellService $powerShellService) { $this->client = $client; + $this->powerShellService = $powerShellService; } public function getAllVirtualMachine($datacenter = '') @@ -353,4 +357,377 @@ class VirtualMachineService } return "Login_Again"; } + + /** + * Search VM by exact name in vCenter. + * Returns the first matching VM array or null if not found. + */ + public function findVirtualMachineByName(string $vmName): ?array + { + $check = $this->client->checkApiExits(); + if (!$check) { + Log::warning('findVirtualMachineByName: API check failed'); + return null; + } + + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->get("https://{$this->client->apiUri}/rest/vcenter/vm", [ + 'filter.names.1' => $vmName, + ]); + + if (!$response->successful()) { + Log::warning('findVirtualMachineByName: HTTP failed', ['status' => $response->status()]); + return null; + } + + $json = $response->json(); + if (isset($json['type']) || empty($json['value'])) { + return null; + } + + return $json['value'][0] ?? null; + } + + public function GetDetailVirtualMachine($vmware_id = ''): array + { + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->get("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id")->json(); + if (!isset($response['type'])) { + return [ + 'error' => false, + 'data' => $response['value'] + ]; + } + return [ + 'error' => true, + 'message' => $response['value'] + ]; + } + + public function GetIPAddressVirtualMachine($vmware_id = ''): array + { + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->get("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id/guest/identity")->json(); + if (!isset($response['type'])) { + return [ + 'error' => false, + 'data' => $response['value'] + ]; + } + return [ + 'error' => true, + 'message' => $response['value'] + ]; + } + + public function GetVMStats($vmware_id = ''): array + { + $stats = [ + 'cpu' => null, + 'memory' => null, + 'disk' => null, + ]; + + try { + $hardwareResponse = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->get("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id/hardware")->json(); + + if (is_array($hardwareResponse) && !isset($hardwareResponse['type']) && isset($hardwareResponse['value'])) { + foreach ($hardwareResponse['value'] as $hardware) { + if (isset($hardware['label']) && $hardware['label'] === 'CPU') { + $stats['cpu'] = [ + 'cores' => $hardware['spec']['num_cores'] ?? 0, + 'cores_per_socket' => $hardware['spec']['cores_per_socket'] ?? 0, + ]; + } + if (isset($hardware['label']) && $hardware['label'] === 'Memory') { + $stats['memory'] = [ + 'size_MiB' => $hardware['spec']['size_MiB'] ?? 0, + 'size_GB' => round(($hardware['spec']['size_MiB'] ?? 0) / 1024, 2), + ]; + } + if (isset($hardware['label']) && $hardware['label'] === 'Hard disk') { + $stats['disk'][] = [ + 'capacity' => $hardware['capacity'] ?? 0, + 'capacity_GB' => round(($hardware['capacity'] ?? 0) / (1024 * 1024 * 1024), 2), + ]; + } + } + } + + $guestMemoryResponse = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->get("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id/guest/memory")->json(); + + if (is_array($guestMemoryResponse) && !isset($guestMemoryResponse['type']) && isset($guestMemoryResponse['value'])) { + $stats['memory']['used_MiB'] = $guestMemoryResponse['value']['used'] ?? 0; + $stats['memory']['used_GB'] = round(($guestMemoryResponse['value']['used'] ?? 0) / 1024, 2); + $stats['memory']['usage_percent'] = $stats['memory']['size_MiB'] > 0 + ? round(($guestMemoryResponse['value']['used'] ?? 0) / $stats['memory']['size_MiB'] * 100, 1) + : 0; + } + + $guestCpuResponse = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->get("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id/guest/cpu")->json(); + + if (is_array($guestCpuResponse) && !isset($guestCpuResponse['type']) && isset($guestCpuResponse['value'])) { + $stats['cpu']['used_percent'] = $guestCpuResponse['value']['used'] ?? 0; + } + + return ['error' => false, 'data' => $stats]; + } catch (\Exception $e) { + return ['error' => true, 'message' => $e->getMessage(), 'data' => $stats]; + } + } + + public function ConfigNetworkVirtualMachine($vmware_id = '', $config = []): int + { + return Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->patch("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id/hardware/ethernet/4000", $config) + ->status(); + } + + public function FindVirtualMachine($name = ''): string|bool + { + $check = $this->client->checkApiExits(); + + if ($check) { + try { + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->get( + "https://{$this->client->apiUri}/rest/vcenter/vm?filter.names.1=$name" + )->json(); + } catch (ConnectionException $error) { + return false; + } + + if ($response['value']) { + return $response['value'][0]['vm']; + } + return false; + } + return "Login_Again"; + } + + public function reBuildVPS(string $vmName): array|string + { + if (!$this->client->checkApiExits()) { + return "Login_Again"; + } + + try { + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->get("https://{$this->client->apiUri}/rest/vcenter/vm", [ + 'filter.names' => $vmName + ]); + + if (!$response->successful()) { + return [ + 'error' => true, + 'message' => 'Failed to search for VM', + 'details' => $response->json() + ]; + } + + $vmList = $response->json()['value'] ?? []; + if (empty($vmList)) { + return [ + 'error' => true, + 'message' => "VM not found with name: {$vmName}" + ]; + } + + $vmId = $vmList[0]['vm']; + + $detailResponse = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->get("https://{$this->client->apiUri}/rest/vcenter/vm/{$vmId}"); + + if (!$detailResponse->successful()) { + return [ + 'error' => true, + 'message' => 'Failed to fetch VM details', + 'details' => $detailResponse->json() + ]; + } + + $vmData = $detailResponse->json(); + $guestOS = $vmData['value']['guest_OS'] ?? 'UNKNOWN'; + + $scriptPath = ''; + $osUpper = strtoupper($guestOS); + + if (Str::contains($osUpper, 'WINDOWS')) { + $scriptPath = 'C:\\CLISave\\Windows\\rebuild.ps1'; + } elseif (Str::contains($osUpper, ['UBUNTU', 'LINUX', 'CENTOS', 'DEBIAN'])) { + $scriptPath = 'C:\\CLISave\\Ubuntu\\rebuild.ps1'; + } else { + return [ + 'error' => true, + 'message' => "Unsupported OS for rebuild: {$guestOS}" + ]; + } + + $scriptDir = dirname($scriptPath); + $scriptFile = basename($scriptPath); + + $cmd = " + Write-Output 'STARTING_REBUILD'; + \$vm = Get-VM -Name '$vmName'; + cd '$scriptDir'; + if (Test-Path '.\\$scriptFile') { + Write-Output 'File found, executing...'; + & '.\\$scriptFile' | Out-String + } else { + Write-Error 'File NOT FOUND: $scriptPath'; + } + Write-Output 'ENDING_REBUILD'; + "; + + $result = $this->powerShellService->executePowerCLI($cmd); + + return [ + 'error' => false, + 'vm_name' => $vmName, + 'vm_id' => $vmId, + 'os' => $guestOS, + 'script' => $scriptPath, + 'result' => $result + ]; + + } catch (\Exception $e) { + Log::error("reBuildVPS Exception: " . $e->getMessage()); + return [ + 'error' => true, + 'message' => $e->getMessage() + ]; + } + } + + public function UpgradeMemoryVirtualMachine($vmware_id = '', $ram = 0): array|string + { + $check = $this->client->checkApiExits(); + + if ($check) { + $RAMConfig = [ + "spec" => [ + "size_MiB" => $ram * 1024, + ] + ]; + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->patch("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id/hardware/memory", $RAMConfig); + + if ($response->status() === 200) { + return [ + 'error' => false, + 'data' => 'success' + ]; + } + return [ + 'error' => true, + 'data' => $response['value'] + ]; + } + return "Login_Again"; + } + + public function UpgradeCPUVirtualMachine($vmware_id = '', $cpu = 0): array|string + { + $check = $this->client->checkApiExits(); + + if ($check) { + $CPUConfig = [ + "spec" => [ + "cores_per_socket" => 1, + "count" => $cpu + ] + ]; + $response = Http::withoutVerifying() + ->withHeaders([ + 'vmware-api-session-id' => $this->client->token + ]) + ->patch("https://{$this->client->apiUri}/rest/vcenter/vm/$vmware_id/hardware/cpu", $CPUConfig); + + if ($response->status() === 200) { + return [ + 'error' => false, + 'data' => 'success' + ]; + } + return [ + 'error' => true, + 'data' => $response['value'] + ]; + } + return "Login_Again"; + } + + public function UpgradeDiskVirtualMachine($product, $disk = 0): array|string + { + $vmware_id = $product->vmware_id; + $check = $this->client->checkApiExits(); + if ($check) { + try { + Log::channel('Custom')->info("==================================================================="); + Log::channel('Custom')->info("Upgrade Disk with VirtualMachine ID # - $vmware_id"); + Log::channel('Custom')->info("\r\n"); + + $Command = "\$vm = 'VirtualMachine-$vmware_id'; \$disksize = $disk; cd /; C:\CLISave\diskresize.ps1;"; + + Log::channel('Custom')->info("$Command"); + + $this->powerShellService->executePowerCLI($Command); + + Log::channel('Custom')->info("\r\n"); + Log::channel('Custom')->info("After run Command |" . now()); + Log::channel('Custom')->info("==================================================================="); + + return [ + 'error' => false + ]; + } catch (\Exception $error) { + Log::channel('Custom')->info("\r\n"); + Log::channel('Custom')->info("==================================================================="); + Log::channel('Custom')->info("Error Log: "); + Log::channel('Custom')->info($error->getMessage()); + Log::channel('Custom')->info("==================================================================="); + return [ + 'error' => true, + 'message' => $error->getMessage() + ]; + } + } + return "Login_Again"; + } } diff --git a/app/Jobs/VirtualMachine/Create/AssignIPJob.php b/app/Jobs/VirtualMachine/Create/AssignIPJob.php deleted file mode 100644 index 9831e49..0000000 --- a/app/Jobs/VirtualMachine/Create/AssignIPJob.php +++ /dev/null @@ -1,245 +0,0 @@ -vmId = $vmId; - $this->moduleVmwareId = $moduleVmwareId; - $this->availableIP = $availableIP; - } - - /** - * Execute the job. - */ - public function handle(vCenter $vCenter, Telegram $telegram) - { - try { - Log::info('🔧 Bắt đầu AssignIPJob', [ - 'vm_id' => $this->vmId, - 'module_vmware_id' => $this->moduleVmwareId, - 'assigned_ip' => $this->availableIP - ]); - - // Lấy thông tin ModuleVmware - $moduleVmware = ModuleVmware::with(['ipAddress.host.datacenter.vCenter'])->find($this->moduleVmwareId); - - if (!$moduleVmware || !$moduleVmware->ipAddress) { - Log::error('❌ Không tìm thấy ModuleVmware hoặc IP address', [ - 'vm_id' => $this->vmId, - 'module_vmware_id' => $this->moduleVmwareId - ]); - - $telegram->SendErrorMessage([ - 'error' => 'ModuleVmware or IP address not found', - 'vm_id' => $this->vmId, - 'module_vmware_id' => $this->moduleVmwareId - ], 'AssignIPJob - Data Not Found'); - - return; - } - - // Login vào vCenter - $vcenter = $moduleVmware->ipAddress->host->datacenter->vCenter; - if (!$vCenter->loginServer($vcenter)) { - Log::error('❌ Không thể login vào vCenter cho AssignIP', [ - 'vm_id' => $this->vmId, - 'vcenter_host' => $vcenter->host - ]); - - $telegram->SendErrorMessage([ - 'error' => 'Cannot login to vCenter', - 'vm_id' => $this->vmId, - 'vcenter_host' => $vcenter->host - ], 'AssignIPJob - vCenter Login Failed'); - - return; - } - - // Kiểm tra OS family và assign IP - $this->handleIPAssignment($vCenter, $this->vmId, $moduleVmware, $this->availableIP, $telegram); - - } catch (\Throwable $e) { - Log::error('❌ Lỗi trong AssignIPJob: ' . $e->getMessage(), [ - 'vm_id' => $this->vmId, - 'module_vmware_id' => $this->moduleVmwareId, - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString() - ]); - - $telegram->SendErrorMessage([ - 'error' => $e->getMessage(), - 'vm_id' => $this->vmId, - 'module_vmware_id' => $this->moduleVmwareId, - 'file' => $e->getFile(), - 'line' => $e->getLine() - ], 'AssignIPJob - Exception'); - } - } - - /** - * Handle IP assignment based on OS family - */ - private function handleIPAssignment(vCenter $vCenter, string $vmId, ModuleVmware $moduleVmware, string $availableIP, Telegram $telegram): void - { - try { - // Kiểm tra OS family trước khi assign IP - $osFamily = $this->checkVMOSFamily($vCenter, $vmId); - - if ($osFamily === 'WINDOWS') { - Log::info('✅ Phát hiện Windows VM, bắt đầu assign IP', [ - 'vm_id' => $vmId, - 'os_family' => $osFamily, - 'assigned_ip' => $availableIP - ]); - - // Assign IP cho Windows VM - $assignIPResult = $vCenter->assignIPWindows($moduleVmware, $vmId); - - if (is_array($assignIPResult) && !$assignIPResult['error']) { - Log::info('✅ Assign IP Windows thành công', [ - 'vm_id' => $vmId, - 'result' => $assignIPResult['data'] - ]); - - $telegram->SendJobRunInfo( - 'AssignIPWindows', - 'success', - [ - 'vm_id' => $vmId, - 'assigned_ip' => $availableIP, - 'result' => $assignIPResult['data'] - ] - ); - } else { - Log::warning('⚠️ Assign IP Windows thất bại', [ - 'vm_id' => $vmId, - 'error' => $assignIPResult['data'] ?? 'Unknown error' - ]); - - $telegram->SendErrorMessage([ - 'error' => 'Assign IP Windows failed', - 'vm_id' => $vmId, - 'details' => $assignIPResult['data'] ?? 'Unknown error' - ], 'AssignIPWindows - Failed'); - } - } elseif ($osFamily === 'LINUX' || $osFamily === 'OTHER') { - Log::info('ℹ️ Phát hiện non-Windows VM, hệ thống đang nâng cấp', [ - 'vm_id' => $vmId, - 'os_family' => $osFamily - ]); - - $telegram->SendJobRunInfo( - 'AssignIP', - 'upgrade', - [ - 'vm_id' => $vmId, - 'os_family' => $osFamily, - 'message' => 'Hệ thống đang nâng cấp để hỗ trợ IP assignment cho ' . $osFamily - ] - ); - } else { - Log::warning('⚠️ Không thể xác định OS family', [ - 'vm_id' => $vmId, - 'os_family' => $osFamily - ]); - - $telegram->SendErrorMessage([ - 'error' => 'Cannot determine OS family', - 'vm_id' => $vmId, - 'os_family' => $osFamily - ], 'AssignIP - Unknown OS'); - } - } catch (\Exception $e) { - Log::error('❌ Lỗi khi handle IP assignment: ' . $e->getMessage(), [ - 'vm_id' => $vmId, - 'error' => $e->getMessage() - ]); - - $telegram->SendErrorMessage([ - 'error' => 'IP Assignment handling failed', - 'vm_id' => $vmId, - 'details' => $e->getMessage() - ], 'HandleIPAssignment - Exception'); - } - } - - /** - * Check VM OS family using vCenter API - */ - private function checkVMOSFamily(vCenter $vCenter, string $vmId): string - { - try { - // Kiểm tra session còn valid không - $check = $vCenter->checkApiExits(); - if (!$check) { - Log::warning('vCenter session expired, cannot check OS family'); - return 'UNKNOWN'; - } - - // Gọi API để lấy guest identity - $response = Http::withoutVerifying() - ->withHeaders([ - 'vmware-api-session-id' => $vCenter->token - ]) - ->get("https://{$vCenter->apiUri}/rest/vcenter/vm/{$vmId}/guest/identity"); - - if ($response->successful()) { - $data = $response->json(); - - if (isset($data['value']['family'])) { - $family = strtoupper($data['value']['family']); - - Log::info('✅ Phát hiện OS family', [ - 'vm_id' => $vmId, - 'family' => $family, - 'full_name' => $data['value']['full_name'] ?? 'Unknown' - ]); - - return $family; - } else { - Log::warning('⚠️ Không tìm thấy family trong response', [ - 'vm_id' => $vmId, - 'response' => $data - ]); - return 'UNKNOWN'; - } - } else { - Log::warning('⚠️ API call thất bại khi check OS family', [ - 'vm_id' => $vmId, - 'status' => $response->status(), - 'response' => $response->body() - ]); - return 'UNKNOWN'; - } - } catch (\Exception $e) { - Log::error('❌ Exception khi check OS family: ' . $e->getMessage(), [ - 'vm_id' => $vmId, - 'error' => $e->getMessage() - ]); - return 'UNKNOWN'; - } - } -} \ No newline at end of file diff --git a/app/Jobs/VirtualMachine/Sync/SyncFolder.php b/app/Jobs/VirtualMachine/Sync/SyncFolder.php index 76b8e73..b5217fc 100644 --- a/app/Jobs/VirtualMachine/Sync/SyncFolder.php +++ b/app/Jobs/VirtualMachine/Sync/SyncFolder.php @@ -38,21 +38,20 @@ class SyncFolder implements ShouldQueue public function handle() { foreach ($this->data as $item) { - $this->vCenter->apiUri = $item->host; - if (!$item->token) { $token = $this->vCenter->loginServer($item); if ($token) { vCenter::where('id', $item->id)->update(['token' => $token]); - $this->vCenter->token = $token; + $item->token = $token; } } - $this->vCenter->token = $item->token; - if (!$this->vCenter->token) { + if (!$item->token) { continue; } + $this->vCenter->setConnection($item->host, $item->token); + $dataCenter = ModuleDatacenter::where('vcenter_id', $item->id)->select('id', 'datacenter_id')->get(); foreach ($dataCenter as $_item) { $folder = $this->vCenter->getAllFolder($_item['datacenter_id']); @@ -61,7 +60,7 @@ class SyncFolder implements ShouldQueue $token = $this->vCenter->loginServer($item); if ($token) { vCenter::where('id', $item->id)->update(['token' => $token]); - $this->vCenter->token = $token; + $this->vCenter->setConnection($item->host, $token); } $folder = $this->vCenter->getAllFolder($_item['datacenter_id']); } diff --git a/app/Mail/Admin/ManualDepositPendingNotification.php b/app/Mail/Admin/ManualDepositPendingNotification.php new file mode 100644 index 0000000..c4d8c00 --- /dev/null +++ b/app/Mail/Admin/ManualDepositPendingNotification.php @@ -0,0 +1,36 @@ +data['transaction_code'] ?? 'N/A'), + ); + } + + public function content(): Content + { + return new Content( + view: 'emails.admin.manual-deposit-pending', + with: $this->data, + ); + } + + public function attachments(): array + { + return []; + } +} diff --git a/app/Mail/Customer/ManualDepositStatus.php b/app/Mail/Customer/ManualDepositStatus.php new file mode 100644 index 0000000..16f3245 --- /dev/null +++ b/app/Mail/Customer/ManualDepositStatus.php @@ -0,0 +1,73 @@ +payload()['subject'] ?? 'Thông báo trạng thái nạp tiền', + ); + } + + public function content(): Content + { + $data = $this->payload(); + + return new Content( + view: 'emails.customer.manual-deposit-status', + with: [ + 'companyName' => $data['company_name'] ?? config('app.name', 'The Gioi So'), + 'companyLogo' => $data['company_logo'] ?? '', + 'customerName' => $data['customer_name'] ?? 'Quý khách', + 'transactionCode' => $data['transaction_code'] ?? 'N/A', + 'amount' => $data['amount'] ?? 0, + 'amountFormatted' => number_format((float) ($data['amount'] ?? 0), 0, ',', '.'), + 'statusLabel' => $data['status_label'] ?? 'Đang chờ duyệt', + 'statusColor' => $data['status_color'] ?? '#f16723', + 'statusBg' => $data['status_bg'] ?? '#fff7ed', + 'statusMessage' => $data['status_message'] ?? ($data['message'] ?? ''), + 'depositDate' => $data['deposit_date'] ?? now()->format('d/m/Y H:i:s'), + 'processedDate' => $data['processed_date'] ?? null, + 'portalUrl' => $data['portal_url'] ?? rtrim(env('FRONTEND_URL', config('app.url')), '/') . '/nap-tien', + 'supportEmail' => $data['support_email'] ?? config('mail.support_email', 'support@thegioiso.com'), + 'supportPhone' => $data['support_phone'] ?? config('mail.support_phone', '1900.xxxx'), + ], + ); + } + + public function attachments(): array + { + return []; + } + + private function payload(): array + { + try { + $setting = Setting::where('key', 'setting')->first(); + $settingValue = $setting ? json_decode((string) $setting->value, true) : []; + } catch (\Throwable) { + $settingValue = []; + } + + return array_merge([ + 'company_name' => $settingValue['company'] ?? config('app.name', 'The Gioi So'), + 'company_logo' => $settingValue['logo'] ?? '', + 'support_email' => $settingValue['email'] ?? config('mail.support_email', 'support@thegioiso.com'), + 'support_phone' => $settingValue['hotline'] ?? ($settingValue['phone'] ?? config('mail.support_phone', '1900.xxxx')), + 'portal_url' => rtrim(env('FRONTEND_URL', config('app.url')), '/') . '/nap-tien', + ], $this->data); + } +} diff --git a/app/Providers/MailConfigServiceProvider.php b/app/Providers/MailConfigServiceProvider.php index 5298e90..7d35c60 100644 --- a/app/Providers/MailConfigServiceProvider.php +++ b/app/Providers/MailConfigServiceProvider.php @@ -46,6 +46,12 @@ class MailConfigServiceProvider extends ServiceProvider $mail = json_decode($mail->value); $company = json_decode($setting->value)->company ?? 'Your Company'; + $password = ''; + if (isset($mail->password)) { + $decrypted = (new SignRSA())->DecryptFunc($mail->password); + $password = $decrypted ?: $mail->password; + } + return [ 'driver' => $mail->transport ?? 'smtp', 'host' => $mail->host ?? '', @@ -56,7 +62,7 @@ class MailConfigServiceProvider extends ServiceProvider ], 'encryption' => $mail->encryption === 0 ? 'tls' : $mail->encryption, 'username' => $mail->user ?? '', - 'password' => isset($mail->password) ? (new SignRSA())->DecryptFunc($mail->password) : '', + 'password' => $password, ]; }); diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index d6a4da5..aec3db7 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -47,10 +47,6 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace) ->group(base_path('routes/client.php')); - Route::middleware('web') - ->namespace($this->namespace) - ->group(base_path('routes/admin.php')); - Route::middleware('web') ->namespace($this->namespace) ->group(base_path('routes/web.php')); diff --git a/app/Repositories/Api/ServiceClient/ServiceClientRepository.php b/app/Repositories/Api/ServiceClient/ServiceClientRepository.php index 0e58b63..79d3ef2 100644 --- a/app/Repositories/Api/ServiceClient/ServiceClientRepository.php +++ b/app/Repositories/Api/ServiceClient/ServiceClientRepository.php @@ -18,7 +18,10 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient public function getActiveServices(int $customerId, ?string $productType = null, int $limit = 10) { $query = $this->model - ->with(['orderItem' => fn($q) => $q->with(['product', 'os.osGroup'])]) + ->with([ + 'invoice.order', + 'orderItem' => fn($q) => $q->with(['product', 'os.osGroup', 'order']), + ]) ->where('customer_id', $customerId) ->orderByRaw("FIELD(status, 'active', 'pending', 'suspended', 'expired', 'cancelled', 'terminated')") ->orderBy('started_at', 'desc'); diff --git a/app/Services/InvoicePdfService.php b/app/Services/InvoicePdfService.php index a381fb6..18ed325 100644 --- a/app/Services/InvoicePdfService.php +++ b/app/Services/InvoicePdfService.php @@ -41,7 +41,9 @@ class InvoicePdfService ], ]; - return Pdf::loadView('pdf.invoice', $data)->output(); + return Pdf::loadView('pdf.invoice', $data) + ->setPaper('a4', 'portrait') + ->output(); } public function generateFileName(Invoice $invoice): string diff --git a/composer.json b/composer.json index 0683a28..baba989 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "laravel/horizon": "^5.4.0", "laravel/tinker": "^2.7", "maatwebsite/excel": "^3.1", + "nwidart/laravel-modules": "^10.0", "opcodesio/log-viewer": "^3.18", "phpseclib/phpseclib": "*", "predis/predis": "^2.2", diff --git a/composer.lock b/composer.lock index b4dc628..eeaf17c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2efed3ac7c2e4830f01f9b6f0977247f", + "content-hash": "a7d98940e2202cf1c8ed3014e17d748c", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -3545,6 +3545,91 @@ ], "time": "2024-11-21T10:36:35+00:00" }, + { + "name": "nwidart/laravel-modules", + "version": "10.0.6", + "source": { + "type": "git", + "url": "https://github.com/nWidart/laravel-modules.git", + "reference": "a6f2c8b53ae7945ef41d296735e963cee885ebde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/a6f2c8b53ae7945ef41d296735e963cee885ebde", + "reference": "a6f2c8b53ae7945ef41d296735e963cee885ebde", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.6", + "laravel/framework": "^10.41", + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^10.0", + "spatie/phpunit-snapshot-assertions": "^5.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Module": "Nwidart\\Modules\\Facades\\Module" + }, + "providers": [ + "Nwidart\\Modules\\LaravelModulesServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "10.0-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Nwidart\\Modules\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com", + "homepage": "https://nicolaswidart.com", + "role": "Developer" + } + ], + "description": "Laravel Module management", + "keywords": [ + "laravel", + "module", + "modules", + "nwidart", + "rad" + ], + "support": { + "issues": "https://github.com/nWidart/laravel-modules/issues", + "source": "https://github.com/nWidart/laravel-modules/tree/10.0.6" + }, + "funding": [ + { + "url": "https://github.com/dcblogdev", + "type": "github" + }, + { + "url": "https://github.com/nwidart", + "type": "github" + } + ], + "time": "2024-01-28T10:04:15+00:00" + }, { "name": "opcodesio/log-viewer", "version": "v3.24.0", diff --git a/config/modules.php b/config/modules.php new file mode 100644 index 0000000..1504a38 --- /dev/null +++ b/config/modules.php @@ -0,0 +1,218 @@ + 'Modules', + + /* + |-------------------------------------------------------------------------- + | Module Stubs + |-------------------------------------------------------------------------- + | + | Default module stubs. + | + */ + + 'stubs' => [ + 'enabled' => false, + 'path' => base_path('vendor/nwidart/laravel-modules/src/Commands/stubs'), + 'files' => [ + 'routes/api' => 'routes/api.php', + 'scaffold/config' => 'config/config.php', + 'composer' => 'composer.json', + ], + 'replacements' => [ + 'routes/api' => ['LOWER_NAME', 'STUDLY_NAME'], + 'scaffold/config' => ['STUDLY_NAME'], + 'composer' => [ + 'LOWER_NAME', + 'STUDLY_NAME', + 'VENDOR', + 'AUTHOR_NAME', + 'AUTHOR_EMAIL', + 'MODULE_NAMESPACE', + 'PROVIDER_NAMESPACE', + ], + ], + 'gitkeep' => true, + ], + 'paths' => [ + /* + |-------------------------------------------------------------------------- + | Modules path + |-------------------------------------------------------------------------- + | + | This path is used to save the generated module. + | This path will also be added automatically to the list of scanned folders. + | + */ + + 'modules' => base_path('modules'), + /* + |-------------------------------------------------------------------------- + | Modules assets path + |-------------------------------------------------------------------------- + | + | Here you may update the modules' assets path. + | + */ + + 'assets' => public_path('modules'), + /* + |-------------------------------------------------------------------------- + | The migrations' path + |-------------------------------------------------------------------------- + | + | Where you run the 'module:publish-migration' command, where do you publish the + | the migration files? + | + */ + + 'migration' => base_path('database/migrations'), + /* + |-------------------------------------------------------------------------- + | Generator path + |-------------------------------------------------------------------------- + | Customise the paths where the folders will be generated. + | Setting the generate key to false will not generate that folder + */ + 'generator' => [ + 'config' => ['path' => 'config', 'generate' => true], + 'command' => ['path' => 'App/Console', 'generate' => false], + 'channels' => ['path' => 'App/Broadcasting', 'generate' => false], + 'migration' => ['path' => 'Database/migrations', 'generate' => false], + 'seeder' => ['path' => 'Database/Seeders', 'generate' => true], + 'factory' => ['path' => 'Database/Factories', 'generate' => false], + 'model' => ['path' => 'App/Models', 'generate' => false], + 'observer' => ['path' => 'App/Observers', 'generate' => false], + 'routes' => ['path' => 'routes', 'generate' => true], + 'controller' => ['path' => 'App/Http/Controllers', 'generate' => true], + 'filter' => ['path' => 'App/Http/Middleware', 'generate' => false], + 'request' => ['path' => 'App/Http/Requests', 'generate' => false], + 'provider' => ['path' => 'App/Providers', 'generate' => true], + 'assets' => ['path' => 'resources/assets', 'generate' => false], + 'lang' => ['path' => 'lang', 'generate' => false], + 'views' => ['path' => 'resources/views', 'generate' => true], + 'test' => ['path' => 'tests/Unit', 'generate' => false], + 'test-feature' => ['path' => 'tests/Feature', 'generate' => false], + 'repository' => ['path' => 'App/Repositories', 'generate' => false], + 'event' => ['path' => 'App/Events', 'generate' => false], + 'listener' => ['path' => 'App/Listeners', 'generate' => false], + 'policies' => ['path' => 'App/Policies', 'generate' => false], + 'rules' => ['path' => 'App/Rules', 'generate' => false], + 'jobs' => ['path' => 'App/Jobs', 'generate' => false], + 'emails' => ['path' => 'App/Emails', 'generate' => false], + 'notifications' => ['path' => 'App/Notifications', 'generate' => false], + 'resource' => ['path' => 'App/resources', 'generate' => false], + 'component-view' => ['path' => 'resources/views/components', 'generate' => false], + 'component-class' => ['path' => 'App/View/Components', 'generate' => false], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Package commands + |-------------------------------------------------------------------------- + | + | Here you can define which commands will be visible and used in your + | application. You can add your own commands to merge section. + | + */ + 'commands' => ConsoleServiceProvider::defaultCommands() + ->merge([ + // New commands go here + ]) + ->toArray(), + + /* + |-------------------------------------------------------------------------- + | Scan Path + |-------------------------------------------------------------------------- + | + | Here you define which folder will be scanned. By default will scan vendor + | directory. This is useful if you host the package in packagist website. + | + */ + + 'scan' => [ + 'enabled' => false, + 'paths' => [base_path('vendor/*/*')], + ], + /* + |-------------------------------------------------------------------------- + | Composer File Template + |-------------------------------------------------------------------------- + | + | Here is the config for the composer.json file, generated by this package + | + */ + + 'composer' => [ + 'vendor' => 'nwidart', + 'author' => [ + 'name' => 'Nicolas Widart', + 'email' => 'n.widart@gmail.com', + ], + 'composer-output' => false, + ], + + /* + |-------------------------------------------------------------------------- + | Caching + |-------------------------------------------------------------------------- + | + | Here is the config for setting up the caching feature. + | + */ + 'cache' => [ + 'enabled' => false, + 'driver' => 'file', + 'key' => 'laravel-modules', + 'lifetime' => 60, + ], + /* + |-------------------------------------------------------------------------- + | Choose what laravel-modules will register as custom namespaces. + | Setting one to false will require you to register that part + | in your own Service Provider class. + |-------------------------------------------------------------------------- + */ + 'register' => [ + 'translations' => true, + /** + * load files on boot or register method + */ + 'files' => 'register', + ], + + /* + |-------------------------------------------------------------------------- + | Activators + |-------------------------------------------------------------------------- + | + | You can define new types of activators here, file, database, etc. The only + | required parameter is 'class'. + | The file activator will store the activation status in storage/installed_modules + */ + 'activators' => [ + 'file' => [ + 'class' => FileActivator::class, + 'statuses-file' => base_path('modules_statuses.json'), + 'cache-key' => 'activator.installed', + 'cache-lifetime' => 604800, + ], + ], + + 'activator' => 'file', +]; diff --git a/database/seeders/DomainProductSeeder.php b/database/seeders/DomainProductSeeder.php new file mode 100644 index 0000000..e8f5da1 --- /dev/null +++ b/database/seeders/DomainProductSeeder.php @@ -0,0 +1,117 @@ +firstOrNew([ + 'slug' => 'domain', + ]); + + $domainCategory->fill([ + 'name' => 'Domain', + 'parent_id' => null, + 'is_active' => true, + 'sort_order' => 6, + 'type' => 'domain', + ]); + + $domainCategory->save(); + + if ($domainCategory->trashed()) { + $domainCategory->restore(); + } + + $products = [ + [ + 'name' => 'Ten mien .vn', + 'slug' => 'domain-vn', + 'extension' => '.vn', + 'price' => 650000, + 'price_sale' => 600000, + 'sort_order' => 1, + ], + [ + 'name' => 'Ten mien .com', + 'slug' => 'domain-com', + 'extension' => '.com', + 'price' => 350000, + 'price_sale' => 320000, + 'sort_order' => 2, + ], + [ + 'name' => 'Ten mien .com.vn', + 'slug' => 'domain-com-vn', + 'extension' => '.com.vn', + 'price' => 550000, + 'price_sale' => 500000, + 'sort_order' => 3, + ], + [ + 'name' => 'Ten mien .net', + 'slug' => 'domain-net', + 'extension' => '.net', + 'price' => 400000, + 'price_sale' => 360000, + 'sort_order' => 4, + ], + [ + 'name' => 'Ten mien .io.vn', + 'slug' => 'domain-io-vn', + 'extension' => '.io.vn', + 'price' => 120000, + 'price_sale' => 100000, + 'sort_order' => 5, + ], + ]; + + foreach ($products as $productData) { + $product = Product::withTrashed()->firstOrNew([ + 'slug' => $productData['slug'], + ]); + + $product->fill([ + 'name' => $productData['name'], + 'short_description' => "Dang ky ten mien {$productData['extension']}", + 'description' => "

Dang ky va gia han ten mien {$productData['extension']} hang nam.

", + 'thumbnail' => '/images/domain/domain.png', + 'stock' => null, + 'is_unlimited' => true, + 'is_active' => true, + 'sort_order' => $productData['sort_order'], + 'product_categories_id' => $domainCategory->id, + 'published_at' => now(), + ]); + + $product->save(); + + if ($product->trashed()) { + $product->restore(); + } + + ProductPricings::updateOrCreate( + [ + 'product_id' => $product->id, + 'billing_cycle' => 'annually', + ], + [ + 'price' => $productData['price'], + 'price_sale' => $productData['price_sale'], + 'currency' => 'VND', + 'is_active' => true, + ] + ); + + $this->command?->info("Seeded domain product: {$product->name}"); + } + + $this->command?->info('Domain products seeding completed.'); + } +} diff --git a/public/.DS_Store b/public/.DS_Store index 1a9f62e9b06fdf0282d4cddf2a90cdf1365b6468..e55bddd08af207168b36f0c51c1dd4f08692099d 100644 GIT binary patch delta 49 zcmZp1XmQxkBdikteZ$<{H;y|pFfcGPYfAIz$ResHKvw`wSke zHkR$$r^)j}^%84aT-|J{4z2xqLF&0U1I~am;0*jv25@JKqqR-BNGtg%s(QPL8{|P>s(I&qi;yY)+8Texi@T8a*GrW}Dt%KK-yEb5) tV~B`fCkqJHy`KPVr-%1R^0mIRihyz&n7lNpk=I diff --git a/resources/views/emails/admin/manual-deposit-pending.blade.php b/resources/views/emails/admin/manual-deposit-pending.blade.php new file mode 100644 index 0000000..6a08b66 --- /dev/null +++ b/resources/views/emails/admin/manual-deposit-pending.blade.php @@ -0,0 +1,98 @@ + + + + + + Yêu cầu nạp tiền chờ duyệt + + + +
+
+
+
🔔
+

Yêu cầu nạp tiền chờ duyệt

+

{{ $transaction_code ?? 'N/A' }} — {{ $created_at ?? now()->format('d/m/Y H:i:s') }}

+
+ +
+ + + + + + + + + + + @if(!empty($customer_phone)) + + + + + @endif + @if(!empty($client_ip)) + + + + + @endif +
Tên khách hàng{{ $customer_name ?? 'N/A' }}
Email{{ $customer_email ?? 'N/A' }}
Số điện thoại{{ $customer_phone }}
IP gửi yêu cầu{{ $client_ip }}
+ + + + + + + + + + + + + + + + @if(!empty($proof_image_url)) + + + + + @endif +
Mã giao dịch{{ $transaction_code ?? 'N/A' }}
Số tiền{{ number_format($amount ?? 0, 0, ',', '.') }} VNĐ
Trạng tháiĐang chờ duyệt
Chứng từXem chứng từ chuyển khoản
+ +
+ Vui lòng vào trang quản trị để kiểm tra chứng từ và duyệt hoặc từ chối giao dịch. +
+
+ + +
+
+ + diff --git a/resources/views/emails/customer/manual-deposit-status.blade.php b/resources/views/emails/customer/manual-deposit-status.blade.php new file mode 100644 index 0000000..4900d80 --- /dev/null +++ b/resources/views/emails/customer/manual-deposit-status.blade.php @@ -0,0 +1,74 @@ + + + + + + {{ $companyName }} - Trạng thái nạp tiền + + + + + + +
+ + + + + + + +
+ @if(!empty($companyLogo)) + {{ $companyName }} + @else + {{ $companyName }} + @endif +
+

Kính chào {{ $customerName }},

+

{{ $statusMessage }}

+ + + + + + + + + + + + + + + + + + + @if(!empty($processedDate)) + + + + + @endif +
Trạng thái + {{ $statusLabel }} +
Mã giao dịch{{ $transactionCode }}
Số tiền{{ $amountFormatted }} VNĐ
Thời gian gửi{{ $depositDate }}
Thời gian xử lý{{ $processedDate }}
+ + + + + +
+ XEM YÊU CẦU NẠP TIỀN +
+ +

Nếu bạn cần hỗ trợ thêm, vui lòng liên hệ {{ $supportEmail }} hoặc {{ $supportPhone }}.

+

Lưu ý:

+
    +
  • Đây là tin nhắn tự động. Vui lòng không trả lời tin nhắn này!
  • +
+
+
+ + diff --git a/resources/views/pdf/invoice.blade.php b/resources/views/pdf/invoice.blade.php index 630a4e8..30896a4 100644 --- a/resources/views/pdf/invoice.blade.php +++ b/resources/views/pdf/invoice.blade.php @@ -27,85 +27,87 @@ $cfg = array_merge([