feat: Add IP address and DNS fields to module_vmware table
- Introduced `ip` and `dns` columns to the `module_vmware` table to store assigned IP addresses and DNS configurations. - Implemented `ChangePasswordViaSshJob` for secure password changes via SSH. - Created `ChangePasswordJob` for handling password changes during VM provisioning. - Developed `ChangePasswordViaVsphereJob` to reset VM guest OS passwords using PowerShell scripts executed on the vSphere host. - Added `SendVmCredentialsEmailJob` to send final VM credentials to customers. - Implemented `WaitVmReadyJob` to poll vSphere API until VMware Tools are running and guest IP is available. - Created `VmProvisionService` for managing VM provisioning tasks, including password generation and updates. - Developed `VsphereHostSshService` for executing PowerShell scripts on the vSphere host via SSH. - Added `IpPoolSeeder` to populate IP pools for VM service initialization.
This commit is contained in:
@@ -23,13 +23,16 @@ class VmController extends Controller
|
||||
/**
|
||||
* POST /v1/system/vm-power-on
|
||||
*
|
||||
* Body: { "product": <ModuleVmware.id> }
|
||||
* Body: { "product": <ModuleVmware.id|vmware_id|service_uuid> }
|
||||
*/
|
||||
public function powerOn(Request $request, PowerOnVMAction $action): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['product' => ['required', 'integer']]);
|
||||
$productId = $request->input('product');
|
||||
if (!$productId) {
|
||||
return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422);
|
||||
}
|
||||
|
||||
$vm = $this->resolveVm((int) $validated['product'], $request);
|
||||
$vm = $this->resolveVmById($productId, $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
@@ -46,13 +49,16 @@ class VmController extends Controller
|
||||
/**
|
||||
* POST /v1/system/vm-power-off
|
||||
*
|
||||
* Body: { "product": <ModuleVmware.id> }
|
||||
* Body: { "product": <ModuleVmware.id|vmware_id|service_uuid> }
|
||||
*/
|
||||
public function powerOff(Request $request, PowerOffVMAction $action): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['product' => ['required', 'integer']]);
|
||||
$productId = $request->input('product');
|
||||
if (!$productId) {
|
||||
return response()->json(['success' => false, 'message' => 'Thiếu product id.'], 422);
|
||||
}
|
||||
|
||||
$vm = $this->resolveVm((int) $validated['product'], $request);
|
||||
$vm = $this->resolveVmById($productId, $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
@@ -190,17 +196,17 @@ class VmController extends Controller
|
||||
* POST /v1/system/vm-change-port
|
||||
*
|
||||
* Body:
|
||||
* product int ModuleVmware.id
|
||||
* product int|string ModuleVmware.id|vmware_id|service_uuid
|
||||
* new_port int New RDP port (1024–65535)
|
||||
*/
|
||||
public function changePort(Request $request, ChangePortVMAction $action): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'product' => ['required', 'integer'],
|
||||
'product' => ['required'],
|
||||
'new_port' => ['required', 'integer', 'min:1024', 'max:65535'],
|
||||
]);
|
||||
|
||||
$vm = $this->resolveVm((int) $validated['product'], $request);
|
||||
$vm = $this->resolveVmById($validated['product'], $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
@@ -239,25 +245,57 @@ class VmController extends Controller
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load a ModuleVmware by ID, verify the authenticated user owns it.
|
||||
* Load a ModuleVmware by ID (int), vmware_id (string), or Service UUID.
|
||||
* Verify the authenticated user owns it.
|
||||
* Returns JsonResponse on error, ModuleVmware on success.
|
||||
*/
|
||||
private function resolveVm(int $id, Request $request): ModuleVmware|JsonResponse
|
||||
private function resolveVmById(int|string $productId, Request $request): ModuleVmware|JsonResponse
|
||||
{
|
||||
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->find($id);
|
||||
$customer = $request->user();
|
||||
$vm = null;
|
||||
|
||||
// Check if it's a UUID (Service UUID)
|
||||
if (is_string($productId) && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $productId)) {
|
||||
$service = Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter')
|
||||
->where('uuid', $productId)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
if ($service && $service->vmware) {
|
||||
$vm = $service->vmware;
|
||||
}
|
||||
} elseif (is_numeric($productId)) {
|
||||
// Numeric ID
|
||||
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->find((int) $productId);
|
||||
} else {
|
||||
// vmware_id string (e.g., "vm-3238")
|
||||
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->where('vmware_id', $productId)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$vm) {
|
||||
return response()->json(['success' => false, 'message' => 'Không tìm thấy VM.'], 404);
|
||||
}
|
||||
|
||||
if ((int) $vm->customer_id !== (int) $request->user()->id) {
|
||||
if ((int) $vm->customer_id !== (int) $customer->id) {
|
||||
return response()->json(['success' => false, 'message' => 'Bạn không có quyền thao tác VM này.'], 403);
|
||||
}
|
||||
|
||||
return $vm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a ModuleVmware by ID, verify the authenticated user owns it.
|
||||
* Returns JsonResponse on error, ModuleVmware on success.
|
||||
* @deprecated Use resolveVmById instead for better ID format support
|
||||
*/
|
||||
private function resolveVm(int $id, Request $request): ModuleVmware|JsonResponse
|
||||
{
|
||||
return $this->resolveVmById($id, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise the array|string return value from Power ON/OFF actions.
|
||||
*/
|
||||
|
||||
@@ -1159,13 +1159,25 @@ PS;
|
||||
->status();
|
||||
}
|
||||
|
||||
public function ConfigVirtualMachineUbuntu($vmware_id = '', $product = [], $password = ''): void
|
||||
public function ConfigVirtualMachineUbuntu($vmware_id = '', $product = null, $password = ''): void
|
||||
{
|
||||
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
|
||||
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;
|
||||
$Gateway = $product->IpAddress->gateway;
|
||||
$HostName = strtolower($product->hostname);
|
||||
$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");
|
||||
|
||||
@@ -1317,11 +1329,20 @@ PS;
|
||||
|
||||
public function ConfigCentOSVersion8($vmware_id = '', $product = [], $password = ''): void
|
||||
{
|
||||
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
|
||||
// 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);
|
||||
$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 {
|
||||
@@ -1350,11 +1371,20 @@ PS;
|
||||
|
||||
public function ConfigCentOSVersion7($vmware_id = '', $product = [], $password = ''): void
|
||||
{
|
||||
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
|
||||
// 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);
|
||||
$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 {
|
||||
@@ -1385,19 +1415,21 @@ PS;
|
||||
{
|
||||
try {
|
||||
// Kiểm tra null safety
|
||||
if (!$product || !$product->IpAddress || !$product->IpAddress->Host || !$product->IpAddress->Host->Datacenter || !$product->IpAddress->Host->Datacenter->vCenter) {
|
||||
Log::error('❌ ConfigWindowsVersion10: Missing required data', [
|
||||
'vmware_id' => $vmware_id,
|
||||
'product_id' => $product?->id ?? 'unknown',
|
||||
'ip_address' => $product?->IpAddress?->ip ?? 'unknown',
|
||||
'host' => $product?->IpAddress?->Host?->name ?? 'unknown',
|
||||
'datacenter' => $product?->IpAddress?->Host?->Datacenter?->name ?? 'unknown',
|
||||
'vcenter' => $product?->IpAddress?->Host?->Datacenter?->vCenter?->host ?? 'unknown'
|
||||
]);
|
||||
if (!$product) {
|
||||
Log::error('❌ ConfigWindowsVersion10: Product is null', ['vmware_id' => $vmware_id]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
|
||||
// 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;
|
||||
@@ -1429,19 +1461,21 @@ PS;
|
||||
{
|
||||
try {
|
||||
// Kiểm tra null safety
|
||||
if (!$product || !$product->IpAddress || !$product->IpAddress->Host || !$product->IpAddress->Host->Datacenter || !$product->IpAddress->Host->Datacenter->vCenter) {
|
||||
Log::error('❌ ConfigWindowsServer: Missing required data', [
|
||||
'vmware_id' => $vmware_id,
|
||||
'product_id' => $product?->id ?? 'unknown',
|
||||
'ip_address' => $product?->IpAddress?->ip ?? 'unknown',
|
||||
'host' => $product?->IpAddress?->Host?->name ?? 'unknown',
|
||||
'datacenter' => $product?->IpAddress?->Host?->Datacenter?->name ?? 'unknown',
|
||||
'vcenter' => $product?->IpAddress?->Host?->Datacenter?->vCenter?->host ?? 'unknown'
|
||||
]);
|
||||
if (!$product) {
|
||||
Log::error('❌ ConfigWindowsServer: Product is null', ['vmware_id' => $vmware_id]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
|
||||
// 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;
|
||||
@@ -1473,19 +1507,18 @@ PS;
|
||||
{
|
||||
try {
|
||||
// Kiểm tra null safety
|
||||
if (!$product || !$product->IpAddress || !$product->IpAddress->Host || !$product->IpAddress->Host->Datacenter || !$product->IpAddress->Host->Datacenter->vCenter) {
|
||||
Log::error('❌ ConfigFixWindowsClick: Missing required data', [
|
||||
'vmware_id' => $vmware_id,
|
||||
'product_id' => $product?->id ?? 'unknown',
|
||||
'ip_address' => $product?->IpAddress?->ip ?? 'unknown',
|
||||
'host' => $product?->IpAddress?->Host?->name ?? 'unknown',
|
||||
'datacenter' => $product?->IpAddress?->Host?->Datacenter?->name ?? 'unknown',
|
||||
'vcenter' => $product?->IpAddress?->Host?->Datacenter?->vCenter?->host ?? 'unknown'
|
||||
]);
|
||||
if (!$product) {
|
||||
Log::error('❌ ConfigFixWindowsClick: Product is null', ['vmware_id' => $vmware_id]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
|
||||
// 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("===================================================================");
|
||||
@@ -1513,23 +1546,22 @@ PS;
|
||||
{
|
||||
try {
|
||||
// Kiểm tra null safety
|
||||
if (!$product || !$product->IpAddress || !$product->IpAddress->Host || !$product->IpAddress->Host->Datacenter || !$product->IpAddress->Host->Datacenter->vCenter) {
|
||||
Log::error('❌ ConfigRockyLinuxVersion9: Missing required data', [
|
||||
'vmware_id' => $vmware_id,
|
||||
'product_id' => $product?->id ?? 'unknown',
|
||||
'ip_address' => $product?->IpAddress?->ip ?? 'unknown',
|
||||
'host' => $product?->IpAddress?->Host?->name ?? 'unknown',
|
||||
'datacenter' => $product?->IpAddress?->Host?->Datacenter?->name ?? 'unknown',
|
||||
'vcenter' => $product?->IpAddress?->Host?->Datacenter?->vCenter?->host ?? 'unknown'
|
||||
]);
|
||||
if (!$product) {
|
||||
Log::error('❌ ConfigRockyLinuxVersion9: Product is null', ['vmware_id' => $vmware_id]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetConfig($product->IpAddress->Host->Datacenter->vCenter);
|
||||
// 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;
|
||||
$Gateway = $product->IpAddress->gateway;
|
||||
$HostName = strtolower($product->hostname);
|
||||
$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("===================================================================");
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Create;
|
||||
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Jobs\Mail\SendInformationVirtualMachine;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ChangePasswordViaSshJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
public int $backoff = 120;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $moduleVmwareId,
|
||||
public readonly string $ip,
|
||||
public readonly string $username,
|
||||
public readonly string $initialPassword,
|
||||
public readonly string $newPassword,
|
||||
) {}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
Log::info('ChangePasswordViaSshJob: starting', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'ip' => $this->ip,
|
||||
'username' => $this->username,
|
||||
]);
|
||||
|
||||
$output = $this->runWithRetry();
|
||||
|
||||
if ($output === null) {
|
||||
$this->fail(new \RuntimeException(
|
||||
"SSH connection to {$this->ip} failed after max attempts"
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
Log::info('ChangePasswordViaSshJob: chpasswd output', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'output' => trim($output),
|
||||
]);
|
||||
|
||||
// Update DB
|
||||
$vm = ModuleVmware::find($this->moduleVmwareId);
|
||||
|
||||
if (!$vm) {
|
||||
Log::error('ChangePasswordViaSshJob: ModuleVmware not found', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$vm->update([
|
||||
'password' => SignRSA::EncryptFunc($this->newPassword),
|
||||
'plain_password' => $this->newPassword,
|
||||
]);
|
||||
|
||||
$this->sendPasswordEmail($vm);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Run chpasswd on the remote VM via sshpass + native SSH.
|
||||
* Retries up to 10 times with 30 s delay (VM may still be booting).
|
||||
*/
|
||||
private function runWithRetry(int $maxAttempts = 10, int $sleepSeconds = 30): ?string
|
||||
{
|
||||
// Remote bash command: echo 'user:pass' | chpasswd
|
||||
$credential = $this->username . ':' . $this->newPassword;
|
||||
$remoteCmd = 'echo ' . escapeshellarg($credential) . ' | chpasswd 2>&1';
|
||||
|
||||
$sshCmd = sprintf(
|
||||
'sshpass -p %s ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -p 22 %s@%s %s 2>&1',
|
||||
escapeshellarg($this->initialPassword),
|
||||
escapeshellarg($this->username),
|
||||
escapeshellarg($this->ip),
|
||||
escapeshellarg($remoteCmd),
|
||||
);
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
||||
$output = shell_exec($sshCmd);
|
||||
|
||||
// null = shell_exec failed (command not found, etc.)
|
||||
// sshpass exits non-zero on connection failure → output contains error text
|
||||
if ($output !== null && !$this->isSshConnectionError($output)) {
|
||||
Log::info('ChangePasswordViaSshJob: SSH connected', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'attempt' => $attempt,
|
||||
]);
|
||||
return $output;
|
||||
}
|
||||
|
||||
Log::warning('ChangePasswordViaSshJob: SSH not ready, retrying', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'attempt' => $attempt,
|
||||
'output_preview' => substr((string) $output, 0, 200),
|
||||
]);
|
||||
|
||||
if ($attempt < $maxAttempts) {
|
||||
sleep($sleepSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect SSH connection failures (not auth errors) in sshpass output.
|
||||
*/
|
||||
private function isSshConnectionError(string $output): bool
|
||||
{
|
||||
$lower = strtolower($output);
|
||||
|
||||
return str_contains($lower, 'connection refused')
|
||||
|| str_contains($lower, 'no route to host')
|
||||
|| str_contains($lower, 'connection timed out')
|
||||
|| str_contains($lower, 'host is down')
|
||||
|| str_contains($lower, 'network is unreachable');
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('ChangePasswordViaSshJob: failed', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'ip' => $this->ip,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private function sendPasswordEmail(ModuleVmware $vm): void
|
||||
{
|
||||
try {
|
||||
$customer = $vm->customer;
|
||||
|
||||
if (!$customer) {
|
||||
Log::error('ChangePasswordViaSshJob: customer not found', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value) : null;
|
||||
|
||||
$emailData = [
|
||||
'company_name' => $settingValue->company ?? 'Thế Giới Số',
|
||||
'company_logo' => $settingValue->logo ?? '',
|
||||
'client_name' => trim($customer->firstname . ' ' . $customer->lastname),
|
||||
'product_name' => 'Cloud VPS',
|
||||
'product_billing' => 'N/A',
|
||||
'product_price' => 'N/A',
|
||||
'service_host' => $this->ip,
|
||||
'service_username' => $this->username,
|
||||
'service_port' => 22,
|
||||
'service_password' => $this->newPassword,
|
||||
'os_name' => ($vm->os?->osGroup?->name ?? '') . ' ' . ($vm->os?->version ?? ''),
|
||||
'service_due_exp' => $vm->next_due_date ? date('d/m/Y', (int) $vm->next_due_date) : 'N/A',
|
||||
];
|
||||
|
||||
SendInformationVirtualMachine::dispatch($emailData, $customer)
|
||||
->delay(now()->addSeconds(5));
|
||||
|
||||
Log::info('ChangePasswordViaSshJob: email dispatched', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'customer_email' => $customer->email,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('ChangePasswordViaSshJob: email failed: ' . $e->getMessage(), [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Jobs\VirtualMachine\Create;
|
||||
|
||||
use App\Http\Facade\HelperFacade as Helper;
|
||||
use App\Jobs\Mail\SendInformationVirtualMachine;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -36,87 +38,171 @@ class ConfigCLICommand implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Generate new password
|
||||
$PasswordVirtualMachine = Helper::general_password();
|
||||
$usersName = '';
|
||||
$port = '';
|
||||
|
||||
// Get VM with relationships
|
||||
$vm = \App\Models\ModuleVmware::with(['ipAddress', 'os.osGroup'])
|
||||
->where('vmware_id', $this->vmware_id)
|
||||
->first();
|
||||
|
||||
if (!$vm) {
|
||||
Log::error('❌ ConfigCLICommand: VM not found', ['vmware_id' => $this->vmware_id]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine OS type and config
|
||||
$osGroupName = $vm->os?->osGroup?->name ?? '';
|
||||
$osVersion = $vm->os?->version ?? '';
|
||||
|
||||
try {
|
||||
$vm = \App\Models\ModuleVmware::where('vmware_id', $this->vmware_id)->first();
|
||||
if ($vm) {
|
||||
$vm->update([
|
||||
'password' => \App\Http\Facade\SignRSA::EncryptFunc($PasswordVirtualMachine),
|
||||
'plain_password' => $PasswordVirtualMachine,
|
||||
]);
|
||||
switch (true) {
|
||||
case (str_contains($osGroupName, "Windows") && str_contains($osVersion, 10)):
|
||||
$usersName = 'administrator';
|
||||
$port = 3389;
|
||||
$this->serviceVCenter->ConfigWindowsVersion10($this->vmware_id, $vm, $PasswordVirtualMachine);
|
||||
break;
|
||||
|
||||
case (str_contains($osGroupName, "Windows")):
|
||||
$usersName = 'administrator';
|
||||
$port = 3389;
|
||||
$this->serviceVCenter->ConfigWindowsServer($this->vmware_id, $vm, $PasswordVirtualMachine);
|
||||
break;
|
||||
|
||||
case (str_contains($osGroupName, "CentOS") && str_contains($osVersion, 7)):
|
||||
$usersName = 'root';
|
||||
$port = 22;
|
||||
$this->serviceVCenter->ConfigCentOSVersion7($this->vmware_id, $vm, $PasswordVirtualMachine);
|
||||
break;
|
||||
|
||||
case (str_contains($osGroupName, "CentOS") && str_contains($osVersion, 8)):
|
||||
$usersName = 'root';
|
||||
$port = 22;
|
||||
$this->serviceVCenter->ConfigCentOSVersion8($this->vmware_id, $vm, $PasswordVirtualMachine);
|
||||
break;
|
||||
|
||||
case (str_contains($osGroupName, "Ubuntu")):
|
||||
$usersName = 'root';
|
||||
$port = 22;
|
||||
$this->serviceVCenter->ConfigVirtualMachineUbuntu($this->vmware_id, $vm, $PasswordVirtualMachine);
|
||||
break;
|
||||
|
||||
case (str_contains($osGroupName, "Rocky")):
|
||||
$usersName = 'root';
|
||||
$port = 22;
|
||||
$this->serviceVCenter->ConfigRockyLinuxVersion9($this->vmware_id, $vm, $PasswordVirtualMachine);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::warning('❌ ConfigCLICommand: Unknown OS', [
|
||||
'vmware_id' => $this->vmware_id,
|
||||
'os_group' => $osGroupName,
|
||||
'os_version' => $osVersion,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for VM to be ready
|
||||
sleep(20);
|
||||
|
||||
$VirtualMachine = $this->serviceVCenter->CheckVirtualMachine($this->vmware_id);
|
||||
|
||||
if ($VirtualMachine['data']['operations_ready'] === false) {
|
||||
$maxAttempts = 30; // 5 minutes max (30 * 10 seconds)
|
||||
$attempt = 0;
|
||||
do {
|
||||
sleep(10);
|
||||
$VirtualMachine = $this->serviceVCenter->CheckVirtualMachine($this->vmware_id);
|
||||
Log::channel('Custom')->info(json_encode($VirtualMachine));
|
||||
$attempt++;
|
||||
} while ($VirtualMachine['data']['operations_ready'] !== true && $attempt < $maxAttempts);
|
||||
}
|
||||
|
||||
// Fix Windows click if Windows
|
||||
if (str_contains($osGroupName, "Windows")) {
|
||||
$this->serviceVCenter->ConfigFixWindowsClick($this->vmware_id, $vm);
|
||||
}
|
||||
|
||||
// ✅ Save password to DB AFTER successful config
|
||||
$vm->update([
|
||||
'password' => \App\Http\Facade\SignRSA::EncryptFunc($PasswordVirtualMachine),
|
||||
'plain_password' => $PasswordVirtualMachine,
|
||||
'port' => $port,
|
||||
'user' => $usersName,
|
||||
]);
|
||||
|
||||
Log::info('✅ ConfigCLICommand: Password changed and saved', [
|
||||
'vmware_id' => $this->vmware_id,
|
||||
'username' => $usersName,
|
||||
'port' => $port,
|
||||
]);
|
||||
|
||||
// Send email with new password to customer
|
||||
$this->sendPasswordEmail($vm, $PasswordVirtualMachine, $usersName, $port);
|
||||
|
||||
// Dispatch CheckIP job
|
||||
$jobs_CheckIP = new CheckIPAddress(
|
||||
$this->vmware_id,
|
||||
$vm,
|
||||
['user' => $usersName, 'password' => $PasswordVirtualMachine, 'port' => $port],
|
||||
$this->serviceVCenter
|
||||
);
|
||||
dispatch($jobs_CheckIP)->delay(now()->addSeconds(5));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('❌ Lỗi khi lưu password: ' . $e->getMessage());
|
||||
Log::error('❌ ConfigCLICommand: Error during configuration', [
|
||||
'vmware_id' => $this->vmware_id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case (str_contains($this->data->Os->OsGroup->name, "Windows") && str_contains($this->data->Os->version, 10)):
|
||||
$usersName = 'administrator';
|
||||
$port = 533892;
|
||||
$this->serviceVCenter->ConfigWindowsVersion10($this->vmware_id, $this->data, $PasswordVirtualMachine);
|
||||
break;
|
||||
/**
|
||||
* Send email with new password to customer
|
||||
*/
|
||||
private function sendPasswordEmail($vm, string $password, string $username, int $port): void
|
||||
{
|
||||
try {
|
||||
$customer = $vm->customer;
|
||||
if (!$customer) {
|
||||
Log::error('ConfigCLICommand: Cannot find customer', ['vm_id' => $vm->id]);
|
||||
return;
|
||||
}
|
||||
|
||||
case (str_contains($this->data->Os->OsGroup->name, "Windows")):
|
||||
$usersName = 'administrator';
|
||||
$port = 53389;
|
||||
$this->serviceVCenter->ConfigWindowsServer($this->vmware_id, $this->data,
|
||||
$PasswordVirtualMachine);
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode($setting->value) : null;
|
||||
|
||||
break;
|
||||
$emailData = [
|
||||
'company_name' => $settingValue->company ?? 'Thế Giới Số',
|
||||
'company_logo' => $settingValue->logo ?? '',
|
||||
'client_name' => trim($customer->firstname . ' ' . $customer->lastname),
|
||||
'product_name' => 'Cloud VPS',
|
||||
'product_billing' => 'N/A',
|
||||
'product_price' => 'N/A',
|
||||
'service_host' => $vm->ipAddress?->ip ?? 'N/A',
|
||||
'service_username' => $username,
|
||||
'service_port' => $port,
|
||||
'service_password' => $password,
|
||||
'os_name' => $vm->os?->osGroup?->name . ' ' . $vm->os?->version ?? 'Unknown OS',
|
||||
'service_due_exp' => $vm->next_due_date ? date('d/m/Y', $vm->next_due_date) : 'N/A',
|
||||
];
|
||||
|
||||
case (str_contains($this->data->Os->OsGroup->name, "CentOS") && str_contains($this->data->Os->version, 7)):
|
||||
$usersName = 'root';
|
||||
$port = 22;
|
||||
$this->serviceVCenter->ConfigCentOSVersion7($this->vmware_id, $this->data,
|
||||
$PasswordVirtualMachine);
|
||||
SendInformationVirtualMachine::dispatch($emailData, $customer)
|
||||
->delay(now()->addSeconds(10));
|
||||
|
||||
break;
|
||||
|
||||
case (str_contains($this->data->Os->OsGroup->name, "CentOS") && str_contains($this->data->Os->version, 8)):
|
||||
$usersName = 'root';
|
||||
$port = 22;
|
||||
$this->serviceVCenter->ConfigCentOSVersion8($this->vmware_id, $this->data,
|
||||
$PasswordVirtualMachine);
|
||||
|
||||
break;
|
||||
|
||||
case (str_contains($this->data->Os->OsGroup->name, "Ubuntu")):
|
||||
$usersName = 'root';
|
||||
$port = 22;
|
||||
$this->serviceVCenter->ConfigVirtualMachineUbuntu($this->vmware_id, $this->data,
|
||||
$PasswordVirtualMachine);
|
||||
break;
|
||||
|
||||
case (str_contains($this->data->Os->OsGroup->name, "Rocky")):
|
||||
$usersName = 'root';
|
||||
$port = 22;
|
||||
$this->serviceVCenter->ConfigRockyLinuxVersion9($this->vmware_id, $this->data,
|
||||
$PasswordVirtualMachine);
|
||||
break;
|
||||
Log::info('✅ ConfigCLICommand: Password email dispatched', [
|
||||
'customer_email' => $customer->email,
|
||||
'vmware_id' => $vm->vmware_id,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('❌ ConfigCLICommand: Failed to dispatch password email: ' . $e->getMessage(), [
|
||||
'vmware_id' => $vm->vmware_id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
sleep(20);
|
||||
|
||||
$VirtualMachine = $this->serviceVCenter->CheckVirtualMachine($this->vmware_id);
|
||||
|
||||
if ($VirtualMachine['data']['operations_ready'] === false) {
|
||||
do {
|
||||
sleep(10);
|
||||
|
||||
$VirtualMachine = $this->serviceVCenter->CheckVirtualMachine($this->vmware_id);
|
||||
|
||||
Log::channel('Custom')->info(json_encode($VirtualMachine));
|
||||
} while ($VirtualMachine['data']['operations_ready'] !== true);
|
||||
|
||||
}
|
||||
if (str_contains($this->data->Os->OsGroup->name, "Windows"))
|
||||
{
|
||||
$this->serviceVCenter->ConfigFixWindowsClick($this->vmware_id, $this->data);
|
||||
}
|
||||
$jobs_CheckIP = new CheckIPAddress($this->vmware_id,$this->data,['user' => $usersName,'password' => $PasswordVirtualMachine,'port' => $port],$this->serviceVCenter);
|
||||
dispatch($jobs_CheckIP)->delay(now()->addSeconds(5));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +72,9 @@ class ConfigDatabase implements ShouldQueue
|
||||
'service_due_exp' => Carbon::parse($this->data->next_due_date)->format('d/m/Y')
|
||||
];
|
||||
|
||||
$mail = new SendInformationVirtualMachine($data, $this->data->Customer);
|
||||
|
||||
dispatch($mail)->delay(now()->addSeconds(5));
|
||||
// Note: Email is now sent by ConfigCLICommand after password is changed
|
||||
// $mail = new SendInformationVirtualMachine($data, $this->data->Customer);
|
||||
// dispatch($mail)->delay(now()->addSeconds(5));
|
||||
|
||||
Event(new Create_VirtualMachine_Success("Máy chủ của bạn đã được khởi tạo thành công!",
|
||||
$this->data->customer->id));
|
||||
|
||||
@@ -6,8 +6,9 @@ use App\Http\Facade\SignRSA;
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\Mail\Customer\VMCreationFailedJob;
|
||||
use App\Jobs\VirtualMachine\Create\AssignIPJob;
|
||||
use App\Jobs\VirtualMachine\Create\ConfigCLICommand;
|
||||
use App\Jobs\Mail\SendInformationVirtualMachine;
|
||||
use App\Jobs\VirtualMachine\Provision\ChangePasswordViaVsphereJob;
|
||||
use App\Services\VmProvisionService;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Os;
|
||||
use App\Models\ModuleVmware;
|
||||
@@ -15,6 +16,7 @@ use App\Models\ModuleIpAddress;
|
||||
use App\Models\IpPool;
|
||||
use App\Models\Log as CustomerLog;
|
||||
use App\Models\Service;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -27,6 +29,12 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** Cho phép job chạy tối đa 10 phút (VM clone có thể mất 5-8 phút) */
|
||||
public int $timeout = 600;
|
||||
|
||||
/** Không retry tự động — lỗi VM cần admin kiểm tra thủ công */
|
||||
public int $tries = 1;
|
||||
|
||||
public OrderItem $item;
|
||||
public bool $isRetry = false;
|
||||
|
||||
@@ -152,6 +160,12 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// Xác định OS family để chọn username và luồng cấu hình phù hợp
|
||||
$osGroupName = $osModel->osGroup?->name ?? '';
|
||||
$isWindows = str_contains(strtolower($osGroupName), 'windows');
|
||||
$vmUser = $isWindows ? 'administrator' : 'root';
|
||||
$vmPort = $isWindows ? 3389 : 22;
|
||||
|
||||
$result = $vCenter->CreateVirtualMachine([
|
||||
'item' => $this->item,
|
||||
'host' => $host,
|
||||
@@ -205,10 +219,7 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
// vm_id được lưu vào tbl.services qua ModuleVmwareObserver::created()
|
||||
$startResult = $this->startVirtualMachineDirectly($vCenter, $vmId);
|
||||
|
||||
if (is_array($startResult) && $startResult['error'] === false) {
|
||||
$configJob = new ConfigCLICommand($vmId, $this->item, $vCenter);
|
||||
dispatch($configJob)->delay(now()->addMinutes(2)); // Chờ 2 phút để VM boot hoàn toàn
|
||||
}
|
||||
// VM started — SSH vào vSphere host để đổi mật khẩu qua PS1 script
|
||||
|
||||
try {
|
||||
$customer = $this->item->order->customer;
|
||||
@@ -232,20 +243,19 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
$firstPayment = (int) $this->item->price * ($months ?: 1);
|
||||
$nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null;
|
||||
|
||||
$initialPassword = 'TGS@' . \Illuminate\Support\Str::random(8);
|
||||
|
||||
$moduleVmware = ModuleVmware::create([
|
||||
'name' => $serverName,
|
||||
'module_ip_address' => $moduleIpAddress->id,
|
||||
'port' => 22,
|
||||
'ip' => $availableIP,
|
||||
'port' => $vmPort,
|
||||
'order_id' => $this->item->order->id,
|
||||
'first_payment' => $firstPayment,
|
||||
'next_payment' => $firstPayment,
|
||||
'next_due_date' => $nextDueDate,
|
||||
'next_invoice' => $nextDueDate,
|
||||
'user' => 'administrator',
|
||||
'password' => $initialPassword,
|
||||
'plain_password' => $initialPassword,
|
||||
'user' => $vmUser,
|
||||
'password' => VmProvisionService::TEMPLATE_PASSWORD,
|
||||
'plain_password' => VmProvisionService::TEMPLATE_PASSWORD,
|
||||
'hostname' => $serverName,
|
||||
'vmware_id' => $vmId,
|
||||
'vcenter_id' => $vcenter->id,
|
||||
@@ -254,11 +264,21 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
// Explicit sync: update services table directly (belt + suspenders vs observer)
|
||||
// Sync services table
|
||||
$this->syncServiceRecord($moduleVmware);
|
||||
|
||||
AssignIPJob::dispatch($vmId, $moduleVmware->id, $moduleIpAddress->ip)
|
||||
->delay(now()->addMinutes(5));
|
||||
// SSH → vSphere host → PS1 → change password → send email
|
||||
ChangePasswordViaVsphereJob::dispatch(
|
||||
$moduleVmware->id,
|
||||
$vmId,
|
||||
$isWindows ? VmProvisionService::OS_WINDOWS : VmProvisionService::OS_LINUX,
|
||||
);
|
||||
|
||||
Log::info('ChangePasswordViaVsphereJob dispatched', [
|
||||
'module_vmware_id' => $moduleVmware->id,
|
||||
'vm_id' => $vmId,
|
||||
'os_family' => $isWindows ? 'WINDOWS' : 'LINUX',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('❌ Lỗi khi lưu dữ liệu VM và IP address: ' . $e->getMessage(), [
|
||||
'vmware_id' => $vmId,
|
||||
@@ -380,26 +400,31 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
|
||||
private function getAvailableIPFromPool(int $networkId): string
|
||||
{
|
||||
// Lấy tất cả IP từ IPPool theo network_id, order by ip_address
|
||||
$ipPoolList = IpPool::where('module_network_id', $networkId)
|
||||
->orderBy('ip_address')
|
||||
->get();
|
||||
|
||||
if ($ipPoolList->isEmpty()) {
|
||||
// Nếu không có IP trong pool, trả về IP default
|
||||
return '0.0.0.0';
|
||||
}
|
||||
|
||||
// Lấy tất cả IPs đã được assign trong network này
|
||||
$assignedIPs = ModuleIpAddress::where('module_network_id', $networkId)
|
||||
->pluck('ip')
|
||||
->toArray();
|
||||
|
||||
foreach ($ipPoolList as $ipPool) {
|
||||
if (!in_array($ipPool->ip_address, $assignedIPs)) {
|
||||
return $ipPool->ip_address;
|
||||
}
|
||||
// Lấy ngẫu nhiên 1 IP từ pool chưa được assign
|
||||
$available = IpPool::where('module_network_id', $networkId)
|
||||
->when(!empty($assignedIPs), fn ($q) => $q->whereNotIn('ip_address', $assignedIPs))
|
||||
->inRandomOrder()
|
||||
->first();
|
||||
|
||||
if (!$available) {
|
||||
Log::warning('getAvailableIPFromPool: không còn IP trống trong pool', [
|
||||
'network_id' => $networkId,
|
||||
'assigned_count' => count($assignedIPs),
|
||||
]);
|
||||
return '0.0.0.0';
|
||||
}
|
||||
return '0.0.0.0';
|
||||
|
||||
Log::info('getAvailableIPFromPool: IP được chọn', [
|
||||
'network_id' => $networkId,
|
||||
'ip' => $available->ip_address,
|
||||
]);
|
||||
|
||||
return $available->ip_address;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -504,4 +529,20 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gửi email thông báo VM đã khởi tạo thành công cho khách hàng.
|
||||
* Gọi ngay sau khi tạo module_vmware — cho cả Linux lẫn Windows.
|
||||
*/
|
||||
private function getBillingCycleLabel(?int $months): string
|
||||
{
|
||||
return match ($months) {
|
||||
1 => 'Hàng tháng',
|
||||
3 => '3 Tháng',
|
||||
6 => '6 Tháng',
|
||||
12 => 'Hàng năm',
|
||||
24 => '2 Năm',
|
||||
default => $months . ' Tháng',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Provision;
|
||||
|
||||
use App\Services\VmProvisionService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* ChangePasswordJob
|
||||
*
|
||||
* Connects to the VM via SSH (Linux) or WinRM (Windows) using the
|
||||
* template default password (TGS@123), then:
|
||||
* [7] Generates a new secure 16-char password
|
||||
* [8] Changes the password on the OS
|
||||
* [9] Verifies the new password works (re-login)
|
||||
* [10] Saves encrypted password + plain_password to module_vmware
|
||||
* [11] Dispatches SendVmCredentialsEmailJob
|
||||
*
|
||||
* Uses the REAL IP reported by VMware Tools (from WaitVmReadyJob),
|
||||
* NOT the IP pool address.
|
||||
*
|
||||
* Up to 3 job-level retries if an unexpected error occurs.
|
||||
*/
|
||||
class ChangePasswordJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
/** SSH + WinRM operations should complete within 2 min */
|
||||
public int $timeout = 120;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $moduleVmwareId,
|
||||
public readonly string $vmId,
|
||||
public readonly string $guestIp, // real IP from VMware Tools
|
||||
public readonly string $osFamily, // VmProvisionService::OS_LINUX | OS_WINDOWS
|
||||
public readonly string $username, // root | Administrator
|
||||
) {}
|
||||
|
||||
public function handle(VmProvisionService $provision): void
|
||||
{
|
||||
Log::info('ChangePasswordJob: starting', [
|
||||
'vm_id' => $this->vmId,
|
||||
'ip' => $this->guestIp,
|
||||
'os_family' => $this->osFamily,
|
||||
'username' => $this->username,
|
||||
]);
|
||||
|
||||
// [7] Generate secure password
|
||||
$newPassword = $provision->generatePassword(16);
|
||||
|
||||
// [8] Change password on OS
|
||||
$this->changePassword($provision, $newPassword);
|
||||
|
||||
// [9] Verify new credentials work (critical — throws on failure)
|
||||
$this->verifyPassword($provision, $newPassword);
|
||||
|
||||
// [10] Persist encrypted password + guest IP to DB
|
||||
$provision->savePassword($this->moduleVmwareId, $this->guestIp, $newPassword);
|
||||
|
||||
// [11] Send credentials email
|
||||
SendVmCredentialsEmailJob::dispatch($this->moduleVmwareId);
|
||||
|
||||
Log::info('ChangePasswordJob: completed', [
|
||||
'vm_id' => $this->vmId,
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('ChangePasswordJob: failed after max retries', [
|
||||
'vm_id' => $this->vmId,
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
\App\Models\ModuleVmware::where('id', $this->moduleVmwareId)
|
||||
->update(['status' => 0]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function changePassword(VmProvisionService $provision, string $newPassword): void
|
||||
{
|
||||
if ($this->osFamily === VmProvisionService::OS_WINDOWS) {
|
||||
Log::info('ChangePasswordJob: changing password via WinRM', [
|
||||
'vm_id' => $this->vmId,
|
||||
'ip' => $this->guestIp,
|
||||
]);
|
||||
|
||||
$provision->winrmChangePassword(
|
||||
$this->guestIp,
|
||||
$this->username,
|
||||
$newPassword,
|
||||
$this->vmId,
|
||||
);
|
||||
} else {
|
||||
Log::info('ChangePasswordJob: changing password via SSH', [
|
||||
'vm_id' => $this->vmId,
|
||||
'ip' => $this->guestIp,
|
||||
]);
|
||||
|
||||
$provision->sshChangePassword(
|
||||
$this->guestIp,
|
||||
$this->username,
|
||||
$newPassword,
|
||||
$this->vmId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function verifyPassword(VmProvisionService $provision, string $newPassword): void
|
||||
{
|
||||
Log::info('ChangePasswordJob: verifying new credentials', [
|
||||
'vm_id' => $this->vmId,
|
||||
'ip' => $this->guestIp,
|
||||
]);
|
||||
|
||||
if ($this->osFamily === VmProvisionService::OS_WINDOWS) {
|
||||
$provision->verifyWinrmLogin($this->guestIp, $this->username, $newPassword, $this->vmId);
|
||||
} else {
|
||||
$provision->verifySshLogin($this->guestIp, $this->username, $newPassword, $this->vmId);
|
||||
}
|
||||
|
||||
Log::info('ChangePasswordJob: credential verification passed', [
|
||||
'vm_id' => $this->vmId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Provision;
|
||||
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\vCenter;
|
||||
use App\Services\VmProvisionService;
|
||||
use App\Services\VsphereHostSshService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* ChangePasswordViaVsphereJob
|
||||
*
|
||||
* Resets the VM guest OS password via:
|
||||
* Laravel → SSH (10.10.10.173) → PowerShell PS1 → Invoke-VMScript (guest OS)
|
||||
*
|
||||
* Flow:
|
||||
* [1] VM already exists, vm_id known
|
||||
* [2] Generate random 16-char password (plain text)
|
||||
* [3] Load vCenter credentials from module_vcenter
|
||||
* [4] SSH into vSphere host
|
||||
* [5] Execute changePassword{Linux|Windows}.ps1 with 5 params
|
||||
* [6] Parse "SUCCESS" / "FAIL" from script output
|
||||
* [7] On SUCCESS → save encrypted password to DB, dispatch email
|
||||
* [8] On FAIL → throw exception → job retries
|
||||
*/
|
||||
class ChangePasswordViaVsphereJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 5;
|
||||
public int $timeout = 180;
|
||||
public int $backoff = 60;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $moduleVmwareId,
|
||||
public readonly string $vmId,
|
||||
public readonly string $osFamily, // VmProvisionService::OS_LINUX | OS_WINDOWS
|
||||
) {}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public function handle(VmProvisionService $provision, VsphereHostSshService $ssh): void
|
||||
{
|
||||
Log::info('ChangePasswordViaVsphereJob: starting', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
'os_family' => $this->osFamily,
|
||||
]);
|
||||
|
||||
// [2] Generate plain-text password (not hashed)
|
||||
$newPassword = $provision->generatePassword(16);
|
||||
|
||||
// [3] Load vCenter credentials from database
|
||||
$vcenter = vCenter::first();
|
||||
|
||||
if (!$vcenter) {
|
||||
throw new \RuntimeException('No vCenter record found in module_vcenter');
|
||||
}
|
||||
|
||||
$vcenterHost = $vcenter->host;
|
||||
$vcenterUser = $vcenter->user;
|
||||
$vcenterPass = SignRSA::DecryptFunc($vcenter->password);
|
||||
|
||||
// [4–5] SSH → vSphere host → run PS1 with 5 params
|
||||
$output = $ssh->changePassword(
|
||||
$this->vmId,
|
||||
$newPassword,
|
||||
$this->osFamily,
|
||||
$vcenterHost,
|
||||
$vcenterUser,
|
||||
$vcenterPass,
|
||||
);
|
||||
|
||||
// [6] Parse output — throw on FAIL so job retries
|
||||
$this->assertSuccess($output);
|
||||
|
||||
// [7] Save encrypted + plain password; IP already set during VM creation
|
||||
$provision->updatePassword($this->moduleVmwareId, $newPassword);
|
||||
|
||||
// [8] Send credentials email (loads fresh VM data from DB)
|
||||
SendVmCredentialsEmailJob::dispatch($this->moduleVmwareId);
|
||||
|
||||
Log::info('ChangePasswordViaVsphereJob: completed', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('ChangePasswordViaVsphereJob: all retries exhausted', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
ModuleVmware::where('id', $this->moduleVmwareId)->update(['status' => 0]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* PS1 scripts output exactly "SUCCESS" or "FAIL".
|
||||
* Throw RuntimeException on FAIL → triggers job retry.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function assertSuccess(string $output): void
|
||||
{
|
||||
$trimmed = trim($output);
|
||||
|
||||
if (str_contains(strtoupper($trimmed), 'SUCCESS')) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
"PS1 script returned failure for VM {$this->vmId}. Output: {$trimmed}",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Provision;
|
||||
|
||||
use App\Jobs\Mail\SendInformationVirtualMachine;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* SendVmCredentialsEmailJob
|
||||
*
|
||||
* Sends the final VM credential email to the customer.
|
||||
* Always loads fresh data from DB so it gets the real IP and
|
||||
* the final password saved by ChangePasswordJob.
|
||||
*
|
||||
* This is step [11] in the provisioning flow.
|
||||
*/
|
||||
class SendVmCredentialsEmailJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
public int $timeout = 60;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $moduleVmwareId,
|
||||
) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$vm = ModuleVmware::with(['customer', 'os.osGroup', 'order.orderItems'])->find($this->moduleVmwareId);
|
||||
|
||||
if (!$vm) {
|
||||
Log::error('SendVmCredentialsEmailJob: ModuleVmware not found', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$customer = $vm->customer;
|
||||
|
||||
if (!$customer?->email) {
|
||||
Log::error('SendVmCredentialsEmailJob: customer or email missing', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value) : null;
|
||||
|
||||
// Resolve billing info from order items
|
||||
$orderItem = $vm->order?->orderItems?->first();
|
||||
$months = (int) ($orderItem?->months ?? 0);
|
||||
|
||||
$emailData = [
|
||||
'company_name' => $settingValue->company ?? 'Thế Giới Số',
|
||||
'company_logo' => $settingValue->logo ?? '',
|
||||
'client_name' => trim($customer->firstname . ' ' . $customer->lastname),
|
||||
'product_name' => $orderItem?->getProductAttribute()?->name ?? 'Cloud VPS',
|
||||
'product_billing' => $this->billingLabel($months),
|
||||
'product_price' => $orderItem ? number_format((float) $orderItem->price, 0, ',', '.') : 'N/A',
|
||||
'service_host' => $vm->ip ?? 'N/A',
|
||||
'service_username' => $vm->user,
|
||||
'service_port' => $vm->port,
|
||||
'service_password' => $vm->plain_password ?? '(xem tài khoản quản lý)',
|
||||
'os_name' => $vm->os
|
||||
? (($vm->os->osGroup?->name ?? '') . ' ' . $vm->os->version)
|
||||
: 'Unknown OS',
|
||||
'service_due_exp' => $vm->next_due_date
|
||||
? date('d/m/Y', (int) $vm->next_due_date)
|
||||
: 'N/A',
|
||||
];
|
||||
|
||||
SendInformationVirtualMachine::dispatch($emailData, $customer);
|
||||
|
||||
Log::info('SendVmCredentialsEmailJob: email dispatched', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'customer_email' => $customer->email,
|
||||
'ip' => $vm->ip,
|
||||
]);
|
||||
}
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('SendVmCredentialsEmailJob: failed', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function billingLabel(?int $months): string
|
||||
{
|
||||
return match ($months) {
|
||||
1 => 'Hàng tháng',
|
||||
3 => '3 Tháng',
|
||||
6 => '6 Tháng',
|
||||
12 => 'Hàng năm',
|
||||
24 => '2 Năm',
|
||||
default => ($months ?? 0) . ' Tháng',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Provision;
|
||||
|
||||
use App\Http\Services\vCenter as VCenterService;
|
||||
use App\Services\VmProvisionService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* WaitVmReadyJob
|
||||
*
|
||||
* Polls vSphere REST API every 15 s until VMware Tools is running
|
||||
* and the guest IP is available. No direct TCP port check is required
|
||||
* because password changes are performed via Invoke-VMScript (vSphere
|
||||
* path), not direct SSH/WinRM into the VM.
|
||||
*
|
||||
* Max 40 attempts × 15 s ≈ 10 minutes before giving up.
|
||||
* On success → dispatches ChangePasswordViaVsphereJob.
|
||||
*/
|
||||
class WaitVmReadyJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** 40 attempts × 15 s release delay ≈ 10 min total */
|
||||
public int $tries = 40;
|
||||
|
||||
/** Each poll attempt must finish within 30 s */
|
||||
public int $timeout = 30;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $moduleVmwareId,
|
||||
public readonly string $vmId,
|
||||
public readonly int $vcenterId,
|
||||
public readonly string $osFamily, // VmProvisionService::OS_LINUX | OS_WINDOWS
|
||||
public readonly string $username, // root | Administrator
|
||||
) {}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function handle(VCenterService $vCenterService, VmProvisionService $provision): void
|
||||
{
|
||||
$attempt = $this->attempts();
|
||||
|
||||
Log::info('WaitVmReadyJob: polling', [
|
||||
'vm_id' => $this->vmId,
|
||||
'attempt' => $attempt,
|
||||
'os_family' => $this->osFamily,
|
||||
]);
|
||||
|
||||
// ── 1. Login to vCenter ───────────────────────────────────────────────
|
||||
$vcenter = \App\Models\vCenter::find($this->vcenterId);
|
||||
|
||||
if (!$vcenter) {
|
||||
Log::error('WaitVmReadyJob: vCenter model not found', ['vcenter_id' => $this->vcenterId]);
|
||||
$this->fail(new \RuntimeException("vCenter id={$this->vcenterId} not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$provision->loginVCenter($vcenter, $vCenterService);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('WaitVmReadyJob: vCenter login failed, will retry', [
|
||||
'vm_id' => $this->vmId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->releaseOrFail(15, $attempt);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 2. Check VMware Tools + guest IP ─────────────────────────────────
|
||||
// Invoke-VMScript requires VMware Tools to be running inside the guest.
|
||||
$guest = $provision->getVmGuestInfo($vCenterService, $this->vmId);
|
||||
|
||||
Log::info('WaitVmReadyJob: guest info', [
|
||||
'vm_id' => $this->vmId,
|
||||
'attempt' => $attempt,
|
||||
'tools_running' => $guest['tools_running'],
|
||||
'ip_address' => $guest['ip_address'],
|
||||
]);
|
||||
|
||||
if (!$guest['tools_running'] || $guest['ip_address'] === null) {
|
||||
Log::info('WaitVmReadyJob: VMware Tools not ready yet', ['vm_id' => $this->vmId]);
|
||||
$this->releaseOrFail(15, $attempt);
|
||||
return;
|
||||
}
|
||||
|
||||
$guestIp = $guest['ip_address'];
|
||||
|
||||
// ── 3. VM ready — dispatch password-change job ────────────────────────
|
||||
Log::info('WaitVmReadyJob: VMware Tools ready, dispatching ChangePasswordViaVsphereJob', [
|
||||
'vm_id' => $this->vmId,
|
||||
'guest_ip' => $guestIp,
|
||||
]);
|
||||
|
||||
ChangePasswordViaVsphereJob::dispatch(
|
||||
$this->moduleVmwareId,
|
||||
$this->vmId,
|
||||
$guestIp,
|
||||
$this->osFamily,
|
||||
$this->username,
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('WaitVmReadyJob: exceeded max attempts, marking VM failed', [
|
||||
'vm_id' => $this->vmId,
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
\App\Models\ModuleVmware::where('id', $this->moduleVmwareId)
|
||||
->update(['status' => 0]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function releaseOrFail(int $delaySeconds, int $attempt): void
|
||||
{
|
||||
if ($attempt >= $this->tries) {
|
||||
$this->fail(new \RuntimeException(
|
||||
"VM {$this->vmId} not ready after {$this->tries} attempts (~10 min)",
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->release($delaySeconds);
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,9 @@ class ModuleNetwork extends Model
|
||||
{
|
||||
return $this->hasMany(\App\Models\ModuleIpAddress::class, 'module_network_id');
|
||||
}
|
||||
|
||||
public function ipPools()
|
||||
{
|
||||
return $this->hasMany(\App\Models\IpPool::class, 'module_network_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Http\Services\vCenter as VCenterService;
|
||||
use App\Models\ModuleVmware;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
final class VmProvisionService
|
||||
{
|
||||
/** Default password baked into every VM template */
|
||||
public const TEMPLATE_PASSWORD = 'TGS@123';
|
||||
|
||||
public const OS_LINUX = 'LINUX';
|
||||
public const OS_WINDOWS = 'WINDOWS';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// vCenter REST helpers (used by WaitVmReadyJob if still in flow)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function loginVCenter(\App\Models\vCenter $vcenter, VCenterService $service): void
|
||||
{
|
||||
if (!$service->loginServer($vcenter)) {
|
||||
throw new \RuntimeException("vCenter login failed for host={$vcenter->host}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{tools_running:bool,ip_address:string|null,os_family:string|null}
|
||||
*/
|
||||
public function getVmGuestInfo(VCenterService $service, string $vmId): array
|
||||
{
|
||||
try {
|
||||
$response = Http::withoutVerifying()
|
||||
->withHeaders(['vmware-api-session-id' => $service->token])
|
||||
->timeout(10)
|
||||
->get("https://{$service->apiUri}/rest/vcenter/vm/{$vmId}/guest/identity");
|
||||
|
||||
if (!$response->successful()) {
|
||||
return ['tools_running' => false, 'ip_address' => null, 'os_family' => null];
|
||||
}
|
||||
|
||||
$value = $response->json('value', []);
|
||||
$ip = trim((string) ($value['ip_address'] ?? ''));
|
||||
|
||||
return [
|
||||
'tools_running' => $ip !== '' && $ip !== '0.0.0.0',
|
||||
'ip_address' => $ip ?: null,
|
||||
'os_family' => isset($value['family']) ? strtoupper((string) $value['family']) : null,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('VmProvisionService::getVmGuestInfo error', [
|
||||
'vm_id' => $vmId, 'error' => $e->getMessage(),
|
||||
]);
|
||||
return ['tools_running' => false, 'ip_address' => null, 'os_family' => null];
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Password
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generate a cryptographically secure 16-char password.
|
||||
* Charset deliberately excludes % (breaks cmd.exe env expansion)
|
||||
* and " (breaks PowerShell argument quoting).
|
||||
*/
|
||||
public function generatePassword(int $length = 16): string
|
||||
{
|
||||
$sets = [
|
||||
'ABCDEFGHJKLMNPQRSTUVWXYZ',
|
||||
'abcdefghjkmnpqrstuvwxyz',
|
||||
'23456789',
|
||||
'!@#$*', // no % or " — safe in cmd.exe double-quoted arguments
|
||||
];
|
||||
|
||||
$password = '';
|
||||
$allChars = implode('', $sets);
|
||||
|
||||
foreach ($sets as $set) {
|
||||
$password .= $set[random_int(0, strlen($set) - 1)];
|
||||
}
|
||||
|
||||
for ($i = count($sets); $i < $length; $i++) {
|
||||
$password .= $allChars[random_int(0, strlen($allChars) - 1)];
|
||||
}
|
||||
|
||||
$chars = str_split($password);
|
||||
for ($i = count($chars) - 1; $i > 0; $i--) {
|
||||
$j = random_int(0, $i);
|
||||
[$chars[$i], $chars[$j]] = [$chars[$j], $chars[$i]];
|
||||
}
|
||||
|
||||
return implode('', $chars);
|
||||
}
|
||||
|
||||
public function encryptPassword(string $plaintext): string
|
||||
{
|
||||
return SignRSA::EncryptFunc($plaintext);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Persistence
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update password only — IP already set during VM creation.
|
||||
*/
|
||||
public function updatePassword(int $moduleVmwareId, string $plainPassword): void
|
||||
{
|
||||
ModuleVmware::where('id', $moduleVmwareId)->update([
|
||||
'password' => $this->encryptPassword($plainPassword),
|
||||
'plain_password' => $plainPassword,
|
||||
]);
|
||||
|
||||
Log::info('VmProvisionService::updatePassword saved', [
|
||||
'module_vmware_id' => $moduleVmwareId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update password + overwrite IP (used when real guest IP differs from pool IP).
|
||||
*/
|
||||
public function savePassword(int $moduleVmwareId, string $ip, string $plainPassword): void
|
||||
{
|
||||
ModuleVmware::where('id', $moduleVmwareId)->update([
|
||||
'ip' => $ip,
|
||||
'password' => $this->encryptPassword($plainPassword),
|
||||
'plain_password' => $plainPassword,
|
||||
]);
|
||||
|
||||
Log::info('VmProvisionService::savePassword saved', [
|
||||
'module_vmware_id' => $moduleVmwareId,
|
||||
'ip' => $ip,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* VsphereHostSshService
|
||||
*
|
||||
* Executes pre-deployed PowerShell scripts on the vSphere Windows host
|
||||
* via native SSH (sshpass). No phpseclib dependency.
|
||||
*
|
||||
* Scripts in C:/CLISave/ accept 5 positional params:
|
||||
* VM_ID NEW_PASSWORD VCENTER_HOST VCENTER_USER VCENTER_PASSWORD
|
||||
*
|
||||
* Scripts only output "SUCCESS" or "FAIL".
|
||||
*/
|
||||
final class VsphereHostSshService
|
||||
{
|
||||
private const HOST = '10.10.10.173';
|
||||
private const PORT = 22;
|
||||
private const USER = 'administrator';
|
||||
private const PASS = 'Thegioiso@123**$$8899TgTk2';
|
||||
|
||||
private const SCRIPT_DIR = 'C:\\CLISave'; // backslash required for Windows cmd.exe
|
||||
private const SCRIPT_LINUX = 'changePasswordLinux.ps1';
|
||||
private const SCRIPT_WINDOWS = 'changePasswordWindows.ps1';
|
||||
|
||||
private const CONNECT_TIMEOUT = 20;
|
||||
private const MAX_RETRIES = 5;
|
||||
private const RETRY_DELAY = 5;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Run the OS-appropriate PS1 script on the vSphere host.
|
||||
*
|
||||
* @throws \RuntimeException on SSH failure
|
||||
*/
|
||||
public function changePassword(
|
||||
string $vmId,
|
||||
string $newPassword,
|
||||
string $osFamily,
|
||||
string $vcenterHost,
|
||||
string $vcenterUser,
|
||||
string $vcenterPassword,
|
||||
): string {
|
||||
$script = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS
|
||||
? self::SCRIPT_WINDOWS
|
||||
: self::SCRIPT_LINUX;
|
||||
|
||||
// Remote Windows cmd.exe command — all args double-quoted
|
||||
$remoteCmd = sprintf(
|
||||
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s',
|
||||
self::SCRIPT_DIR,
|
||||
$script,
|
||||
$this->warg($vmId),
|
||||
$this->warg($newPassword),
|
||||
$this->warg($vcenterHost),
|
||||
$this->warg($vcenterUser),
|
||||
$this->warg($vcenterPassword),
|
||||
);
|
||||
|
||||
Log::info('VsphereHostSshService: executing script', [
|
||||
'vm_id' => $vmId,
|
||||
'script' => $script,
|
||||
'vcenter_host' => $vcenterHost,
|
||||
]);
|
||||
|
||||
$output = $this->sshExec($remoteCmd);
|
||||
|
||||
Log::info('VsphereHostSshService: script output', [
|
||||
'vm_id' => $vmId,
|
||||
'output' => trim($output),
|
||||
]);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Run a command on the vSphere SSH host via sshpass.
|
||||
* Retries up to MAX_RETRIES times on connection failure.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function sshExec(string $remoteCmd): string
|
||||
{
|
||||
// Full native SSH command executed on local shell
|
||||
$sshCmd = sprintf(
|
||||
'sshpass -p %s ssh -o StrictHostKeyChecking=no -o ConnectTimeout=%d -p %d %s@%s %s 2>&1',
|
||||
escapeshellarg(self::PASS),
|
||||
self::CONNECT_TIMEOUT,
|
||||
self::PORT,
|
||||
escapeshellarg(self::USER),
|
||||
escapeshellarg(self::HOST),
|
||||
escapeshellarg($remoteCmd),
|
||||
);
|
||||
|
||||
for ($attempt = 1; $attempt <= self::MAX_RETRIES; $attempt++) {
|
||||
$output = shell_exec($sshCmd);
|
||||
|
||||
if ($output === null) {
|
||||
Log::warning('VsphereHostSshService: shell_exec returned null', [
|
||||
'attempt' => $attempt,
|
||||
]);
|
||||
} else {
|
||||
Log::info('VsphereHostSshService: SSH executed', ['attempt' => $attempt]);
|
||||
return $output;
|
||||
}
|
||||
|
||||
if ($attempt < self::MAX_RETRIES) {
|
||||
sleep(self::RETRY_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
'SSH exec to ' . self::HOST . ' failed after ' . self::MAX_RETRIES . ' attempts',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows cmd.exe safe argument: wrap in double quotes, escape inner double quotes.
|
||||
* Password charset excludes " and % so wrapping is always safe.
|
||||
*/
|
||||
private function warg(string $value): string
|
||||
{
|
||||
return '"' . str_replace('"', '\\"', $value) . '"';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('module_vmware', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('module_vmware', 'ip')) {
|
||||
$table->string('ip', 45)->nullable()->after('plain_password')
|
||||
->comment('IP address assigned from ip_pools');
|
||||
}
|
||||
if (!Schema::hasColumn('module_vmware', 'dns')) {
|
||||
$table->string('dns', 100)->nullable()->after('ip')
|
||||
->comment('DNS assigned from network config');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('module_vmware', function (Blueprint $table) {
|
||||
$table->dropColumn(array_filter(['ip', 'dns'], fn ($col) => Schema::hasColumn('module_vmware', $col)));
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\IpPool;
|
||||
use App\Models\ModuleNetwork;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* IpPoolSeeder — nhập danh sách IP vào ip_pools để assign cho VM khi khởi tạo dịch vụ.
|
||||
*
|
||||
* Cách cấu hình pools() bên dưới:
|
||||
*
|
||||
* // Theo CIDR:
|
||||
* ['network' => 'LAN-VPS-01', 'cidr' => '103.90.224.0/26']
|
||||
*
|
||||
* // Theo range:
|
||||
* ['network' => 'LAN-VPS-01', 'start' => '103.90.224.10', 'end' => '103.90.224.60']
|
||||
*
|
||||
* // Danh sách IP thủ công:
|
||||
* ['network' => 'LAN-VPS-01', 'ips' => ['103.90.224.10', '103.90.224.11', ...]]
|
||||
*
|
||||
* // Có thể dùng module_network_id thay vì tên network:
|
||||
* ['network_id' => 1, 'cidr' => '103.90.224.0/26']
|
||||
*
|
||||
* IP đầu (network address) và IP cuối (broadcast) trong CIDR tự động bị loại bỏ.
|
||||
* Khi VM được tạo, hệ thống sẽ lấy IP đầu tiên chưa được assign trong module_ip_address.
|
||||
*/
|
||||
class IpPoolSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* ============================================================
|
||||
* ĐỊA CHỈ IP POOLS — CHỈNH SỬA TẠI ĐÂY
|
||||
* ============================================================
|
||||
*/
|
||||
private function pools(): array
|
||||
{
|
||||
return [
|
||||
// --- Pool 1: VPS Public Network (module_network id=1, name="VM Network") ---
|
||||
[
|
||||
'network_id' => 1, // module_network.id
|
||||
'cidr' => '103.90.224.0/26', // /26 = 62 usable IPs (.1 → .62)
|
||||
],
|
||||
|
||||
// --- Pool 2: thêm range thủ công ---
|
||||
// [
|
||||
// 'network' => 'LAN-VPS-02',
|
||||
// 'start' => '10.10.20.50',
|
||||
// 'end' => '10.10.20.100',
|
||||
// ],
|
||||
|
||||
// --- Pool 3: danh sách IP cụ thể ---
|
||||
// [
|
||||
// 'network_id' => 2,
|
||||
// 'ips' => [
|
||||
// '192.168.100.10',
|
||||
// '192.168.100.11',
|
||||
// '192.168.100.12',
|
||||
// ],
|
||||
// ],
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$totalInserted = 0;
|
||||
$totalSkipped = 0;
|
||||
|
||||
foreach ($this->pools() as $pool) {
|
||||
$network = $this->resolveNetwork($pool);
|
||||
|
||||
if (!$network) {
|
||||
$this->command->warn('IpPoolSeeder: không tìm thấy network: ' . json_encode($pool));
|
||||
continue;
|
||||
}
|
||||
|
||||
$ips = $this->generateIps($pool);
|
||||
|
||||
if (empty($ips)) {
|
||||
$this->command->warn("IpPoolSeeder: không có IP nào được generate cho network #{$network->id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($ips as $ip) {
|
||||
$created = IpPool::updateOrCreate(
|
||||
[
|
||||
'ip_address' => $ip,
|
||||
'module_network_id' => $network->id,
|
||||
]
|
||||
);
|
||||
|
||||
if ($created->wasRecentlyCreated) {
|
||||
$totalInserted++;
|
||||
} else {
|
||||
$totalSkipped++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->info("✅ Network [{$network->name}] (id={$network->id}): " . count($ips) . " IPs xử lý xong.");
|
||||
}
|
||||
|
||||
$this->command->info("IpPoolSeeder hoàn tất: {$totalInserted} IPs mới, {$totalSkipped} IPs đã tồn tại.");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function resolveNetwork(array $pool): ?ModuleNetwork
|
||||
{
|
||||
if (!empty($pool['network_id'])) {
|
||||
return ModuleNetwork::find($pool['network_id']);
|
||||
}
|
||||
|
||||
if (!empty($pool['network'])) {
|
||||
return ModuleNetwork::where('name', $pool['network'])->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate danh sách IP từ CIDR, range start–end, hoặc mảng IP thủ công.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function generateIps(array $pool): array
|
||||
{
|
||||
if (!empty($pool['ips'])) {
|
||||
return array_values(array_filter($pool['ips'], fn($ip) => filter_var($ip, FILTER_VALIDATE_IP)));
|
||||
}
|
||||
|
||||
if (!empty($pool['cidr'])) {
|
||||
return $this->ipsFromCidr($pool['cidr']);
|
||||
}
|
||||
|
||||
if (!empty($pool['start']) && !empty($pool['end'])) {
|
||||
return $this->ipsFromRange($pool['start'], $pool['end']);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lấy tất cả usable IPs từ CIDR (bỏ network address và broadcast).
|
||||
* Ví dụ: 103.90.224.0/26 → .1 đến .62
|
||||
*/
|
||||
private function ipsFromCidr(string $cidr): array
|
||||
{
|
||||
[$baseIp, $prefix] = explode('/', $cidr);
|
||||
|
||||
$prefix = (int) $prefix;
|
||||
$baseInt = ip2long($baseIp);
|
||||
$mask = ~((1 << (32 - $prefix)) - 1);
|
||||
$network = $baseInt & $mask;
|
||||
$broadcast = $network | ~$mask;
|
||||
|
||||
$ips = [];
|
||||
// Bỏ qua network address (.0) và broadcast (.255 hoặc cuối range)
|
||||
for ($i = $network + 1; $i < $broadcast; $i++) {
|
||||
$ips[] = long2ip($i);
|
||||
}
|
||||
|
||||
return $ips;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lấy tất cả IPs trong range start → end (inclusive).
|
||||
*/
|
||||
private function ipsFromRange(string $start, string $end): array
|
||||
{
|
||||
$startInt = ip2long($start);
|
||||
$endInt = ip2long($end);
|
||||
|
||||
if ($startInt === false || $endInt === false || $startInt > $endInt) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ips = [];
|
||||
for ($i = $startInt; $i <= $endInt; $i++) {
|
||||
$ips[] = long2ip($i);
|
||||
}
|
||||
|
||||
return $ips;
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -227,7 +227,29 @@ Route::prefix('v1/system')->middleware(['api.auth'])->group(function () {
|
||||
], 422);
|
||||
}
|
||||
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find($productId);
|
||||
// Kiểm tra nếu là UUID (Service UUID)
|
||||
$isUuid = preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $productId);
|
||||
|
||||
$product = null;
|
||||
if ($isUuid) {
|
||||
// Tìm qua Service UUID
|
||||
$service = \App\Models\Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter')
|
||||
->where('uuid', $productId)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
if ($service && $service->vmware) {
|
||||
$product = $service->vmware;
|
||||
}
|
||||
} elseif (is_numeric($productId)) {
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')->find((int)$productId);
|
||||
} else {
|
||||
// Tìm theo vmware_id (ví dụ: vm-3238)
|
||||
$product = \App\Models\ModuleVmware::with('ipAddress.host.datacenter.vCenter', 'vCenter')
|
||||
->where('vmware_id', $productId)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$product) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
|
||||
Reference in New Issue
Block a user