Implement VM deletion functionality and update related services; add confirmation check and logging

This commit is contained in:
baodev0
2026-07-17 09:14:26 +07:00
parent 5cf7411d3e
commit abc12b71ef
12 changed files with 320 additions and 161 deletions
@@ -225,6 +225,7 @@ class VCenterController extends Controller
'type' => 'virtual_machine',
'meta' => [
'db_id' => $vm->id,
'vmware_db_id' => $vm->vmware?->id,
'uuid' => $vm->uuid,
'vm_id' => $vm->vm_id,
'ip' => $vm->ip,
@@ -302,6 +303,7 @@ class VCenterController extends Controller
'type' => 'virtual_machine',
'meta' => [
'db_id' => $vm->id,
'vmware_db_id' => $vm->vmware?->id,
'uuid' => $vm->uuid,
'vm_id' => $vm->vm_id,
'ip' => $vm->ip,
@@ -313,20 +313,20 @@ class VCenterManagerController extends Controller
{
$vcenterId = $request->input('vcenter_id');
$vcenter = $this->vCenterRepo->find($vcenterId);
if (!$vcenter) {
return response()->json(['error' => 'vCenter not found'], 404);
}
$vcenterService = new \App\Http\Services\vCenter();
$loginResult = $vcenterService->loginServer($vcenter);
if (!$loginResult) {
return response()->json(['error' => 'Cannot login to vCenter'], 400);
}
$datacenters = $vcenterService->getAllDatacenter();
if ($datacenters === false || $datacenters === 'Login_Again') {
return response()->json(['error' => 'Cannot get datacenters'], 400);
}
@@ -341,9 +341,9 @@ class VCenterManagerController extends Controller
{
$vcenterId = $request->input('vcenter_id');
$datacenterId = $request->input('datacenter_id');
$vcenter = $this->vCenterRepo->find($vcenterId);
if (!$vcenter) {
return response()->json(['error' => 'vCenter not found'], 404);
}
@@ -352,7 +352,7 @@ class VCenterManagerController extends Controller
$vcenterService->loginServer($vcenter);
$hosts = $vcenterService->getAllHost($datacenterId);
if ($hosts === false || $hosts === 'Login_Again') {
return response()->json(['error' => 'Cannot get hosts'], 400);
}
@@ -367,9 +367,9 @@ class VCenterManagerController extends Controller
{
$vcenterId = $request->input('vcenter_id');
$hostId = $request->input('host_id');
$vcenter = $this->vCenterRepo->find($vcenterId);
if (!$vcenter) {
return response()->json(['error' => 'vCenter not found'], 404);
}
@@ -445,15 +445,15 @@ class VCenterManagerController extends Controller
public function powerOn(Request $request)
{
$vmId = (int) $request->input('vm_id');
$vm = VirtualMachines::find($vmId);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'VM not found'], 404);
}
$vmwareId = $vm->vm_id;
if (!$vmwareId) {
return response()->json(['error' => true, 'message' => 'VM VMware ID not found'], 400);
}
@@ -463,7 +463,7 @@ class VCenterManagerController extends Controller
}
$vcenter = null;
if ($vm->datacenter?->vCenter) {
$vcenter = $vm->datacenter->vCenter;
}
@@ -477,7 +477,7 @@ class VCenterManagerController extends Controller
}
$vCenterService = new \App\Http\Services\vCenter();
if (!$vCenterService->loginServer($vcenter)) {
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
}
@@ -505,15 +505,15 @@ class VCenterManagerController extends Controller
public function powerOff(Request $request)
{
$vmId = (int) $request->input('vm_id');
$vm = VirtualMachines::find($vmId);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'VM not found'], 404);
}
$vmwareId = $vm->vm_id;
if (!$vmwareId) {
return response()->json(['error' => true, 'message' => 'VM VMware ID not found'], 400);
}
@@ -523,7 +523,7 @@ class VCenterManagerController extends Controller
}
$vcenter = null;
if ($vm->datacenter?->vCenter) {
$vcenter = $vm->datacenter->vCenter;
}
@@ -537,7 +537,7 @@ class VCenterManagerController extends Controller
}
$vCenterService = new \App\Http\Services\vCenter();
if (!$vCenterService->loginServer($vcenter)) {
return response()->json(['error' => true, 'message' => 'Cannot login to vCenter'], 400);
}
@@ -683,19 +683,28 @@ class VCenterManagerController extends Controller
public function suspend(Request $request)
{
$vmId = $request->input('vm_id');
$vmId = (int) $request->input('vm_id');
$vm = VirtualMachines::where('vm_id', $vmId)->first();
// vm_id from frontend = services.vm_id = module_vmware.id (FK)
$vmware = ModuleVmware::with('vCenter')->find($vmId);
if (!$vm) {
if (!$vmware) {
return response()->json(['error' => true, 'message' => 'VM not found'], 404);
}
if ($vm->power_state === 'SUSPENDED') {
$vmwareId = $vmware->vmware_id;
if (!$vmwareId) {
return response()->json(['error' => true, 'message' => 'VM VMware ID not found'], 400);
}
$vm = VirtualMachines::where('vm_id', $vmwareId)->first();
if ($vm?->power_state === 'SUSPENDED') {
return response()->json(['error' => true, 'message' => 'VM is already suspended'], 400);
}
$vcenter = $vm->datacenter?->vCenter ?? $vm->host?->datacenter?->vCenter;
$vcenter = $vmware->vCenter;
if (!$vcenter) {
return response()->json(['error' => true, 'message' => 'vCenter not found'], 400);
@@ -709,18 +718,18 @@ class VCenterManagerController extends Controller
$response = Http::withoutVerifying()
->withHeaders(['vmware-api-session-id' => $vCenterService->token])
->post("https://{$vCenterService->apiUri}/rest/vcenter/vm/$vmId/power/suspend");
->post("https://{$vCenterService->apiUri}/rest/vcenter/vm/$vmwareId/power/suspend");
if ($response->status() === 200) {
$vm->update(['power_state' => 'SUSPENDED']);
$this->sendSuspendNotification($vm);
$vm?->update(['power_state' => 'SUSPENDED']);
$this->sendSuspendNotification($vmware, $vm);
return response()->json(['error' => false, 'message' => 'Suspend command sent successfully']);
}
$errorData = $response->json();
if (isset($errorData['error_type']) && $errorData['error_type'] === 'ALREADY_IN_DESIRED_STATE') {
$vm->update(['power_state' => 'SUSPENDED']);
$this->sendSuspendNotification($vm);
$vm?->update(['power_state' => 'SUSPENDED']);
$this->sendSuspendNotification($vmware, $vm);
return response()->json(['error' => false, 'message' => 'VM is already suspended']);
}
@@ -730,19 +739,69 @@ class VCenterManagerController extends Controller
], 400);
}
private function sendSuspendNotification(VirtualMachines $vm): void
public function deleteVM(Request $request)
{
$vmware = ModuleVmware::with('customer')
->where('vmware_id', $vm->vm_id)
->first();
$request->validate([
'vm_id' => 'required|integer',
'confirm_name' => 'required|string',
]);
if (!$vmware || !$vmware->customer || !$vmware->customer->email) {
$vmId = (int) $request->input('vm_id');
$vm = VirtualMachines::find($vmId);
if (!$vm) {
return response()->json(['error' => true, 'message' => 'VM not found'], 404);
}
if ($request->input('confirm_name') !== $vm->name) {
return response()->json(['error' => true, 'message' => "Tên xác nhận không khớp. Vui lòng nhập đúng: {$vm->name}"], 422);
}
if ($vm->power_state !== 'POWERED_OFF') {
return response()->json(['error' => true, 'message' => 'Vui lòng tắt máy ảo trước khi xoá'], 400);
}
$vcenter = $vm->datacenter?->vCenter ?? $vm->host?->datacenter?->vCenter;
if (!$vcenter) {
return response()->json(['error' => true, 'message' => 'vCenter not found'], 400);
}
$powerShellService = app(\App\Http\Services\vCenter\PowerShellService::class);
$q = fn(string $v) => str_replace("'", "''", $v);
$vmName = $vm->name;
$host = $vcenter->host;
$user = $vcenter->user;
$password = \App\Http\Facade\SignRSA::DecryptFunc($vcenter->password);
$command = "& 'C:\\CLISave\\DeleteVM\\deleteVM.ps1' -VmName '" . $q($vmName) . "' -VCenterHost '" . $q($host) . "' -VCenterUser '" . $q($user) . "' -VCenterPassword '" . $q($password) . "'";
$cmdResult = $powerShellService->runPsCommand($command);
if (strpos($cmdResult, 'Login_Fail') !== false) {
return response()->json(['error' => true, 'message' => 'SSH Login to PowerShell server failed'], 400);
}
if (strpos($cmdResult, 'SUCCESS') === false) {
return response()->json(['error' => true, 'message' => 'Xoá VM thất bại: ' . $cmdResult], 400);
}
$vm->delete();
return response()->json(['error' => false, 'message' => 'VM đã được xoá thành công']);
}
private function sendSuspendNotification(ModuleVmware $vmware, ?VirtualMachines $vm = null): void
{
$vmware->loadMissing('customer');
if (!$vmware->customer || !$vmware->customer->email) {
return;
}
try {
Mail::to($vmware->customer->email)
->queue(new VmSuspendedMail($vmware->customer, $vm->name, $vm->vm_id));
->queue(new VmSuspendedMail($vmware->customer, $vm?->name ?? $vmware->name, $vmware->vmware_id));
} catch (\Throwable) {
// Email failure must not block the suspend response
}
+1 -1
View File
@@ -240,7 +240,7 @@ class vCenter
public function CreateSnapShot($product, ?string $snapshotName = null): string|array
{
return $this->snapshot->CreateSnapShot($product);
return $this->snapshot->CreateSnapShot($product, $snapshotName);
}
public function getSnapShot($product): string|array
{
@@ -0,0 +1,94 @@
<?php
namespace App\Http\Services\vCenter;
use Illuminate\Support\Facades\Log;
class DeleteVMService
{
protected Client $client;
protected PowerShellService $powerShellService;
public function __construct(Client $client, PowerShellService $powerShellService)
{
$this->client = $client;
$this->powerShellService = $powerShellService;
}
protected function resolveVCenterFromProduct($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;
}
public function deleteVM($product, string $confirmName): array
{
$vmName = (string) $product->name;
// ponytail: confirm guard — caller must pass exact vm name to proceed
if ($confirmName !== $vmName) {
return ['error' => true, 'message' => "Tên server xác nhận không khớp. Vui lòng nhập đúng: $vmName"];
}
$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 ['error' => true, 'message' => 'Login_Again'];
}
$check = $this->client->checkApiExits();
if (!$check) {
return ['error' => true, 'message' => 'Login_Again'];
}
$host = $this->client->apiUri;
$user = $this->client->getUsername();
$password = $this->client->getPassword();
try {
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Delete VM - $vmName");
$command = "& 'C:\CLISave\DeleteVM\deleteVM.ps1' -VmName '$vmName' -VCenterHost '$host' -VCenterUser '$user' -VCenterPassword '$password'";
Log::channel('Custom')->info("Executing deleteVM command for: $vmName");
$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, 'SUCCESS') === false) {
return ['error' => true, 'message' => 'PowerShell command failed: ' . $cmdResult];
}
Log::channel('Custom')->info("After run Command |" . now());
Log::channel('Custom')->info("===================================================================");
return ['error' => false];
} catch (\Exception $error) {
Log::channel('Custom')->error("Delete VM error: " . $error->getMessage());
return ['error' => true, 'message' => $error->getMessage()];
}
}
}
@@ -79,7 +79,8 @@ class PowerShellService
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
$realErr = $errOut && !str_starts_with(trim($errOut), '#< CLIXML');
return $out . ($realErr ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
@@ -132,7 +133,9 @@ PS;
try {
$out = $auth->exec($action);
$errOut = $auth->getStdError();
return $out . ($errOut ? "\n[STDERR]\n" . $errOut : '');
// PowerCLI banner is CLIXML informational records — not an error
$realErr = $errOut && !str_starts_with(trim($errOut), '#< CLIXML');
return $out . ($realErr ? "\n[STDERR]\n" . $errOut : '');
} catch (\Throwable $e) {
return "Error: " . $e->getMessage();
}
+40 -43
View File
@@ -47,57 +47,54 @@ class SnapshotService
return null;
}
public function CreateSnapShot($product): string|array
public function CreateSnapShot($product, ?string $snapshotName = null): 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";
}
$vmName = (string) $product->name;
$name = $snapshotName ?? ($vmName . '_' . date('d_m_Y_H_i_s'));
$check = $this->client->checkApiExits();
if (!$check) {
return "Login_Again";
}
try {
// Note: credentials setup logic from original vCenter.php seemed redundant with Client logic,
// but CreateSnapShot was overwriting $this->username/password.
// Here we assume Client is used, or we need to update Client.
// Client stores token. If we need username/password for something else, Client has them now via getters.
// But CreateSnapshot used $product->ipaddress...->vCenter->user/password.
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Create Snapshot with VirtualMachine Name - $vmName - Snap #$name");
// Re-login to ensure we have the right credentials for the vCenter associated with the product
$vcenterModel = $this->resolveVCenterFromProduct($product);
$command = "& 'C:\CLISave\createsnapshot.ps1' -vm_name '$vmName' -name '$name'";
Log::channel('Custom')->info($command);
if (!$vcenterModel) {
return ['error' => true, 'message' => 'Không tìm thấy thông tin vCenter cho VM'];
$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'];
}
$this->client->loginServer($vcenterModel);
} catch (\Exception $e) {
return ['error' => true, 'message' => 'Lỗi setup credentials: ' . $e->getMessage()];
}
$vmware_name = (string) $product->name;
// In original code, it checks checkApiExits(), if true proceed.
if ($this->client->checkApiExits()) {
try {
$Snapshot = $vmware_name . "_" . date('d_m_Y_H_i_s');
Log::channel('Custom')->info("===================================================================");
Log::channel('Custom')->info("Create Snapshot with VirtualMachine Name - $vmware_name - Snap #$Snapshot");
$Command = "\$vm_name = '$vmware_name';\$name = '$Snapshot'; cd /; C:\CLISave\createsnapshot.ps1;";
Log::channel('Custom')->info("$Command");
$this->powerShellService->executePowerCLI($Command);
$this->clearSnapshotCache($product);
return [
'error' => false,
'data' => $Snapshot
];
} catch (\Exception $error) {
return [
'error' => true,
'message' => $error->getMessage()
];
if (strpos($cmdResult, '[STDERR]') !== false || stripos($cmdResult, 'error') !== false) {
return ['error' => true, 'message' => 'PowerShell command failed: ' . $cmdResult];
}
Log::channel('Custom')->info("After run Command |" . now());
Log::channel('Custom')->info("===================================================================");
$this->clearSnapshotCache($product);
return ['error' => false, 'data' => $name];
} catch (\Exception $error) {
Log::channel('Custom')->error("Create Snapshot error: " . $error->getMessage());
return ['error' => true, 'message' => $error->getMessage()];
}
return "Login_Again";
}
private function clearSnapshotCache($product): void