feat: Implement IP assignment and verification for VM guests; add related PowerShell scripts
This commit is contained in:
@@ -2,21 +2,16 @@
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Create;
|
||||
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\Mail\Customer\VMCreationFailedJob;
|
||||
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;
|
||||
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;
|
||||
@@ -222,31 +217,19 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
// VM started — SSH vào vSphere host để đổi mật khẩu qua PS1 script
|
||||
|
||||
try {
|
||||
$customer = $this->item->order->customer;
|
||||
$customer = $this->item->order->customer;
|
||||
$serverName = $this->item->server_name ?? 'VM-' . $vmId;
|
||||
|
||||
$networkId = $product->ipAddress?->module_network_id ?? 1;
|
||||
$availableIP = $this->getAvailableIPFromPool($networkId);
|
||||
|
||||
$moduleIpAddress = ModuleIpAddress::create([
|
||||
'vm_name' => $serverName,
|
||||
'module_host_id' => $host->id,
|
||||
'module_network_id' => $networkId,
|
||||
'ip' => $availableIP,
|
||||
'gateway' => $product->ipAddress?->gateway ?? '0.0.0.0',
|
||||
'netmask' => $product->ipAddress?->netmask ?? '255.255.255.0',
|
||||
'dns' => $product->ipAddress?->dns ?? '8.8.8.8',
|
||||
'status' => 1 // 1 = Assigned
|
||||
]);
|
||||
|
||||
$months = (int) ($this->item->months ?? 0);
|
||||
$months = (int) ($this->item->months ?? 0);
|
||||
$firstPayment = (int) $this->item->price * ($months ?: 1);
|
||||
$nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null;
|
||||
$nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null;
|
||||
|
||||
// Create ModuleVmware without IP — AssignIpToGuestJob will
|
||||
// pick, assign, and verify the real IP later, then write it to DB.
|
||||
$moduleVmware = ModuleVmware::create([
|
||||
'name' => $serverName,
|
||||
'module_ip_address' => $moduleIpAddress->id,
|
||||
'ip' => $availableIP,
|
||||
'module_ip_address' => null, // set by AssignIpToGuestJob
|
||||
'ip' => null, // set by AssignIpToGuestJob
|
||||
'port' => $vmPort,
|
||||
'order_id' => $this->item->order->id,
|
||||
'first_payment' => $firstPayment,
|
||||
@@ -267,7 +250,9 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
// Sync services table
|
||||
$this->syncServiceRecord($moduleVmware);
|
||||
|
||||
// SSH → vSphere host → PS1 → change password → send email
|
||||
// Step 1 of guest provisioning: change template password.
|
||||
// On success ChangePasswordViaVsphereJob dispatches AssignIpToGuestJob,
|
||||
// which handles IP selection, assignment, verification, and email.
|
||||
ChangePasswordViaVsphereJob::dispatch(
|
||||
$moduleVmware->id,
|
||||
$vmId,
|
||||
@@ -398,35 +383,6 @@ class CreateVirtualMachineJob implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
private function getAvailableIPFromPool(int $networkId): string
|
||||
{
|
||||
// Lấy tất cả IPs đã được assign trong network này
|
||||
$assignedIPs = ModuleIpAddress::where('module_network_id', $networkId)
|
||||
->pluck('ip')
|
||||
->toArray();
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
Log::info('getAvailableIPFromPool: IP được chọn', [
|
||||
'network_id' => $networkId,
|
||||
'ip' => $available->ip_address,
|
||||
]);
|
||||
|
||||
return $available->ip_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start VM directly using vCenter API
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Provision;
|
||||
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Models\IpPool;
|
||||
use App\Models\ModuleIpAddress;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Os;
|
||||
use App\Models\vCenter;
|
||||
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\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* AssignIpToGuestJob
|
||||
*
|
||||
* Assigns a real static IP to the VM guest OS and verifies it is active.
|
||||
* Only persists the IP to DB after successful guest-side verification.
|
||||
*
|
||||
* Flow:
|
||||
* [1] Resolve network config from order item → product → ipAddress template
|
||||
* [2] for attempt 1..MAX_ATTEMPTS:
|
||||
* [3] Pick a free IP with DB transaction + lock (reserveIp)
|
||||
* [4] Pre-check: ping IP — if reachable → release and try next
|
||||
* [5] Call assignIPLinux.ps1 / assignIPWindows.ps1 via SSH
|
||||
* [6] On FAIL → release IP, try next
|
||||
* [7] Call verifyIPLinux.ps1 / verifyIPWindows.ps1 via SSH
|
||||
* [8] On FAIL → release IP, try next
|
||||
* [9] SUCCESS → persistIp() → dispatch SendVmCredentialsEmailJob
|
||||
* [10] All attempts exhausted → mark VM inactive (status=0)
|
||||
*/
|
||||
class AssignIpToGuestJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** All retries handled internally — no Laravel-level retry needed */
|
||||
public int $tries = 1;
|
||||
|
||||
/** 5 attempts × ~90 s each + overhead */
|
||||
public int $timeout = 600;
|
||||
|
||||
private const MAX_ATTEMPTS = 5;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $moduleVmwareId,
|
||||
public readonly string $vmId,
|
||||
public readonly string $osFamily, // VmProvisionService::OS_LINUX | OS_WINDOWS
|
||||
public readonly string $guestPassword, // New password from ChangePasswordViaVsphereJob
|
||||
) {}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public function handle(VsphereHostSshService $ssh): void
|
||||
{
|
||||
Log::info('AssignIpToGuestJob: start', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
'os_family' => $this->osFamily,
|
||||
]);
|
||||
|
||||
// Load ModuleVmware with the relations needed to derive network config
|
||||
$moduleVmware = ModuleVmware::with([
|
||||
'order.orderItems.product.ipAddress',
|
||||
'vcenter',
|
||||
])->find($this->moduleVmwareId);
|
||||
|
||||
if (!$moduleVmware) {
|
||||
Log::error('AssignIpToGuestJob: ModuleVmware not found', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve network config: product template IP → gateway, netmask, dns, networkId
|
||||
$orderItem = $moduleVmware->order?->orderItems?->first();
|
||||
$osModel = $orderItem?->os_id ? Os::with('osGroup.host')->find($orderItem->os_id) : null;
|
||||
|
||||
$networkId = $orderItem?->product?->ipAddress?->module_network_id ?? 1;
|
||||
$gateway = $orderItem?->product?->ipAddress?->gateway ?? '0.0.0.0';
|
||||
$netmask = $orderItem?->product?->ipAddress?->netmask ?? '255.255.255.0';
|
||||
$dns = $orderItem?->product?->ipAddress?->dns ?? '8.8.8.8';
|
||||
$hostId = $osModel?->osGroup?->host?->id ?? 1;
|
||||
$cidr = (string) $this->netmaskToCidr($netmask);
|
||||
|
||||
// vCenter credentials
|
||||
$vcenter = $moduleVmware->vcenter ?? vCenter::first();
|
||||
$vcenterHost = $vcenter->host;
|
||||
$vcenterUser = $vcenter->user;
|
||||
$vcenterPass = SignRSA::DecryptFunc($vcenter->password);
|
||||
|
||||
// ─── Attempt loop ────────────────────────────────────────────────────
|
||||
|
||||
for ($attempt = 1; $attempt <= self::MAX_ATTEMPTS; $attempt++) {
|
||||
Log::info('AssignIpToGuestJob: attempt', [
|
||||
'attempt' => $attempt,
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'network_id' => $networkId,
|
||||
]);
|
||||
|
||||
// [3] Reserve a free IP with transaction + row lock
|
||||
$reservation = $this->reserveIp($networkId, $hostId, $gateway, $netmask, $dns);
|
||||
|
||||
if (!$reservation) {
|
||||
Log::error('AssignIpToGuestJob: no free IPs in pool', [
|
||||
'network_id' => $networkId,
|
||||
'attempt' => $attempt,
|
||||
]);
|
||||
break; // No point retrying — no IPs available
|
||||
}
|
||||
|
||||
$ip = $reservation->ip;
|
||||
|
||||
// [4] Pre-check: skip IPs that are already reachable (in use by another host)
|
||||
if ($this->isIpReachable($ip)) {
|
||||
Log::warning('AssignIpToGuestJob: IP reachable (likely in use), skipping', [
|
||||
'ip' => $ip,
|
||||
'attempt' => $attempt,
|
||||
]);
|
||||
$this->releaseIp($reservation);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [5] Assign IP into VM guest OS via PS1 script
|
||||
$assignOutput = $ssh->assignIp(
|
||||
vmId: $this->vmId,
|
||||
ip: $ip,
|
||||
gateway: $gateway,
|
||||
cidr: $cidr,
|
||||
vcenterHost: $vcenterHost,
|
||||
vcenterUser: $vcenterUser,
|
||||
vcenterPassword: $vcenterPass,
|
||||
guestPassword: $this->guestPassword,
|
||||
osFamily: $this->osFamily,
|
||||
);
|
||||
|
||||
// [6] If assign failed → release and try next IP
|
||||
if (!$this->isSuccess($assignOutput)) {
|
||||
Log::warning('AssignIpToGuestJob: assignIp FAIL', [
|
||||
'ip' => $ip,
|
||||
'attempt' => $attempt,
|
||||
'output' => trim($assignOutput),
|
||||
]);
|
||||
$this->releaseIp($reservation);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [7] Verify IP is active inside guest OS via PS1 script
|
||||
$verifyOutput = $ssh->verifyIpInGuest(
|
||||
vmId: $this->vmId,
|
||||
ip: $ip,
|
||||
vcenterHost: $vcenterHost,
|
||||
vcenterUser: $vcenterUser,
|
||||
vcenterPassword: $vcenterPass,
|
||||
guestPassword: $this->guestPassword,
|
||||
osFamily: $this->osFamily,
|
||||
);
|
||||
|
||||
// [8] If verification failed → release and try next IP
|
||||
if (!$this->isSuccess($verifyOutput)) {
|
||||
Log::warning('AssignIpToGuestJob: verifyIpInGuest FAIL', [
|
||||
'ip' => $ip,
|
||||
'attempt' => $attempt,
|
||||
'output' => trim($verifyOutput),
|
||||
]);
|
||||
$this->releaseIp($reservation);
|
||||
continue;
|
||||
}
|
||||
|
||||
// [9] SUCCESS — persist confirmed IP to DB
|
||||
$this->persistIp($reservation, $moduleVmware);
|
||||
|
||||
Log::info('AssignIpToGuestJob: IP assigned and verified', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
'ip' => $ip,
|
||||
'attempt' => $attempt,
|
||||
]);
|
||||
|
||||
// Dispatch credential email now that we have the real IP
|
||||
SendVmCredentialsEmailJob::dispatch($this->moduleVmwareId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// [10] All attempts exhausted
|
||||
Log::error('AssignIpToGuestJob: all attempts failed — marking VM inactive', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
'attempts' => self::MAX_ATTEMPTS,
|
||||
]);
|
||||
|
||||
ModuleVmware::where('id', $this->moduleVmwareId)->update(['status' => 0]);
|
||||
}
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('AssignIpToGuestJob: job failed with exception', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'vm_id' => $this->vmId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
ModuleVmware::where('id', $this->moduleVmwareId)->update(['status' => 0]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// assignIp / verifyIp helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Reserve a free IP from the pool inside a DB transaction with row-level locks.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Lock all existing ModuleIpAddress rows for the network → prevents double-pick
|
||||
* 2. Lock the chosen IpPool row → prevents concurrent reservations
|
||||
* 3. Create a pending ModuleIpAddress (status=0) to hold the slot
|
||||
*
|
||||
* Returns the pending ModuleIpAddress record, or null if pool is exhausted.
|
||||
*/
|
||||
private function reserveIp(
|
||||
int $networkId,
|
||||
int $hostId,
|
||||
string $gateway,
|
||||
string $netmask,
|
||||
string $dns,
|
||||
): ?ModuleIpAddress {
|
||||
return DB::transaction(function () use ($networkId, $hostId, $gateway, $netmask, $dns) {
|
||||
// Lock existing rows to prevent another job from picking the same IP
|
||||
$usedIps = ModuleIpAddress::where('module_network_id', $networkId)
|
||||
->lockForUpdate()
|
||||
->pluck('ip')
|
||||
->toArray();
|
||||
|
||||
$ipPool = IpPool::where('module_network_id', $networkId)
|
||||
->whereNotIn('ip_address', $usedIps)
|
||||
->lockForUpdate()
|
||||
->inRandomOrder()
|
||||
->first();
|
||||
|
||||
if (!$ipPool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a pending reservation — acts as a distributed lock on this IP
|
||||
return ModuleIpAddress::create([
|
||||
'vm_name' => '__pending__',
|
||||
'module_host_id' => $hostId,
|
||||
'module_network_id' => $networkId,
|
||||
'ip' => $ipPool->ip_address,
|
||||
'gateway' => $gateway,
|
||||
'netmask' => $netmask,
|
||||
'dns' => $dns,
|
||||
'status' => 0, // 0 = pending/reserved, not yet confirmed
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a failed reservation by deleting the pending ModuleIpAddress row.
|
||||
* This returns the IP to the available pool.
|
||||
*/
|
||||
private function releaseIp(ModuleIpAddress $reservation): void
|
||||
{
|
||||
try {
|
||||
$reservation->delete();
|
||||
Log::info('AssignIpToGuestJob: IP reservation released', ['ip' => $reservation->ip]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('AssignIpToGuestJob: could not release IP reservation', [
|
||||
'ip' => $reservation->ip,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm a successful IP assignment:
|
||||
* - Update ModuleIpAddress: status → 1 (assigned), vm_name → real name
|
||||
* - Update ModuleVmware: ip, dns, module_ip_address FK
|
||||
*
|
||||
* IP is only written to DB AFTER guest-side verification passes.
|
||||
*/
|
||||
private function persistIp(ModuleIpAddress $reservation, ModuleVmware $moduleVmware): void
|
||||
{
|
||||
DB::transaction(function () use ($reservation, $moduleVmware) {
|
||||
$reservation->update([
|
||||
'vm_name' => $moduleVmware->name ?? ('VM-' . $this->vmId),
|
||||
'status' => 1, // assigned
|
||||
]);
|
||||
|
||||
$moduleVmware->update([
|
||||
'ip' => $reservation->ip,
|
||||
'dns' => $reservation->dns,
|
||||
'module_ip_address' => $reservation->id,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Micro-helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Ping the IP from the Laravel host (1 packet, 1 s timeout).
|
||||
* Returns true if reachable → the IP is already in use by something else.
|
||||
*/
|
||||
private function isIpReachable(string $ip): bool
|
||||
{
|
||||
exec(sprintf('ping -c 1 -W 1 %s 2>/dev/null', escapeshellarg($ip)), $output, $exitCode);
|
||||
return $exitCode === 0;
|
||||
}
|
||||
|
||||
/** PS1 scripts output exactly "SUCCESS" or "FAIL". */
|
||||
private function isSuccess(string $output): bool
|
||||
{
|
||||
return str_contains(strtoupper(trim($output)), 'SUCCESS');
|
||||
}
|
||||
|
||||
/** Convert dotted-decimal netmask to CIDR prefix length (e.g. 255.255.255.0 → 24). */
|
||||
private function netmaskToCidr(string $netmask): int
|
||||
{
|
||||
$bits = 0;
|
||||
foreach (explode('.', $netmask) as $octet) {
|
||||
$bits += substr_count(decbin((int) $octet), '1');
|
||||
}
|
||||
return $bits;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use App\Models\ModuleVmware;
|
||||
use App\Models\vCenter;
|
||||
use App\Services\VmProvisionService;
|
||||
use App\Services\VsphereHostSshService;
|
||||
use App\Jobs\VirtualMachine\Provision\AssignIpToGuestJob;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -29,8 +30,9 @@ use Illuminate\Support\Facades\Log;
|
||||
* [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
|
||||
* [7] On SUCCESS → save encrypted password to DB
|
||||
* [8] Dispatch AssignIpToGuestJob with the new guest password
|
||||
* [9] On FAIL → throw exception → job retries
|
||||
*/
|
||||
class ChangePasswordViaVsphereJob implements ShouldQueue
|
||||
{
|
||||
@@ -83,11 +85,16 @@ class ChangePasswordViaVsphereJob implements ShouldQueue
|
||||
// [6] Parse output — throw on FAIL so job retries
|
||||
$this->assertSuccess($output);
|
||||
|
||||
// [7] Save encrypted + plain password; IP already set during VM creation
|
||||
// [7] Save encrypted + plain password (IP will be set later by AssignIpToGuestJob)
|
||||
$provision->updatePassword($this->moduleVmwareId, $newPassword);
|
||||
|
||||
// [8] Send credentials email (loads fresh VM data from DB)
|
||||
SendVmCredentialsEmailJob::dispatch($this->moduleVmwareId);
|
||||
// [8] Assign real IP into guest OS using the new password
|
||||
AssignIpToGuestJob::dispatch(
|
||||
$this->moduleVmwareId,
|
||||
$this->vmId,
|
||||
$this->osFamily,
|
||||
$newPassword, // guest_password — MUST use new password, not template
|
||||
);
|
||||
|
||||
Log::info('ChangePasswordViaVsphereJob: completed', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
|
||||
@@ -28,6 +28,12 @@ final class VsphereHostSshService
|
||||
private const SCRIPT_LINUX = 'changePasswordLinux.ps1';
|
||||
private const SCRIPT_WINDOWS = 'changePasswordWindows.ps1';
|
||||
|
||||
private const ASSIGN_DIR = 'C:\\CLISave\\AssignIP';
|
||||
private const SCRIPT_ASSIGN_LINUX = 'assignIPLinux.ps1';
|
||||
private const SCRIPT_ASSIGN_WINDOWS = 'assignIPWindows.ps1';
|
||||
private const SCRIPT_VERIFY_LINUX = 'verifyIPLinux.ps1';
|
||||
private const SCRIPT_VERIFY_WINDOWS = 'verifyIPWindows.ps1';
|
||||
|
||||
private const CONNECT_TIMEOUT = 20;
|
||||
private const MAX_RETRIES = 5;
|
||||
private const RETRY_DELAY = 5;
|
||||
@@ -81,6 +87,112 @@ final class VsphereHostSshService
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Assign a static IP to the VM guest OS via PS1 script.
|
||||
* Calls assignIPLinux.ps1 or assignIPWindows.ps1 in C:\CLISave\AssignIP\.
|
||||
*
|
||||
* Script params: vm_id ip gateway cidr vcenter_host vcenter_user vcenter_password guest_password
|
||||
* Script output: SUCCESS | FAIL
|
||||
*/
|
||||
public function assignIp(
|
||||
string $vmId,
|
||||
string $ip,
|
||||
string $gateway,
|
||||
string $cidr,
|
||||
string $vcenterHost,
|
||||
string $vcenterUser,
|
||||
string $vcenterPassword,
|
||||
string $guestPassword,
|
||||
string $osFamily,
|
||||
): string {
|
||||
$script = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS
|
||||
? self::SCRIPT_ASSIGN_WINDOWS
|
||||
: self::SCRIPT_ASSIGN_LINUX;
|
||||
|
||||
$remoteCmd = sprintf(
|
||||
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s %s %s',
|
||||
self::ASSIGN_DIR,
|
||||
$script,
|
||||
$this->warg($vmId),
|
||||
$this->warg($ip),
|
||||
$this->warg($gateway),
|
||||
$this->warg($cidr),
|
||||
$this->warg($vcenterHost),
|
||||
$this->warg($vcenterUser),
|
||||
$this->warg($vcenterPassword),
|
||||
$this->warg($guestPassword),
|
||||
);
|
||||
|
||||
Log::info('VsphereHostSshService: executing assignIp', [
|
||||
'vm_id' => $vmId,
|
||||
'ip' => $ip,
|
||||
'script' => $script,
|
||||
'os' => $osFamily,
|
||||
]);
|
||||
|
||||
$output = $this->sshExec($remoteCmd);
|
||||
|
||||
Log::info('VsphereHostSshService: assignIp output', [
|
||||
'vm_id' => $vmId,
|
||||
'ip' => $ip,
|
||||
'output' => trim($output),
|
||||
]);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that an IP is active inside the VM guest OS.
|
||||
* Calls verifyIPLinux.ps1 or verifyIPWindows.ps1 in C:\CLISave\AssignIP\.
|
||||
*
|
||||
* Script params: vm_id ip vcenter_host vcenter_user vcenter_password guest_password
|
||||
* Script output: SUCCESS | FAIL
|
||||
*/
|
||||
public function verifyIpInGuest(
|
||||
string $vmId,
|
||||
string $ip,
|
||||
string $vcenterHost,
|
||||
string $vcenterUser,
|
||||
string $vcenterPassword,
|
||||
string $guestPassword,
|
||||
string $osFamily,
|
||||
): string {
|
||||
$script = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS
|
||||
? self::SCRIPT_VERIFY_WINDOWS
|
||||
: self::SCRIPT_VERIFY_LINUX;
|
||||
|
||||
$remoteCmd = sprintf(
|
||||
'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s',
|
||||
self::ASSIGN_DIR,
|
||||
$script,
|
||||
$this->warg($vmId),
|
||||
$this->warg($ip),
|
||||
$this->warg($vcenterHost),
|
||||
$this->warg($vcenterUser),
|
||||
$this->warg($vcenterPassword),
|
||||
$this->warg($guestPassword),
|
||||
);
|
||||
|
||||
Log::info('VsphereHostSshService: executing verifyIpInGuest', [
|
||||
'vm_id' => $vmId,
|
||||
'ip' => $ip,
|
||||
'script' => $script,
|
||||
'os' => $osFamily,
|
||||
]);
|
||||
|
||||
$output = $this->sshExec($remoteCmd);
|
||||
|
||||
Log::info('VsphereHostSshService: verifyIpInGuest output', [
|
||||
'vm_id' => $vmId,
|
||||
'ip' => $ip,
|
||||
'output' => trim($output),
|
||||
]);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Run a command on the vSphere SSH host via sshpass.
|
||||
* Retries up to MAX_RETRIES times on connection failure.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# assignIPLinux.ps1
|
||||
# Deploy to: C:\CLISave\AssignIP\assignIPLinux.ps1 on the vSphere Windows host
|
||||
#
|
||||
# Usage: powershell -ExecutionPolicy Bypass -NonInteractive -File assignIPLinux.ps1 \
|
||||
# <vm_id> <ip> <gateway> <cidr> <vcenter_host> <vcenter_user> <vcenter_password> <guest_password>
|
||||
#
|
||||
# Output: SUCCESS | FAIL (exactly one line)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$VmId,
|
||||
[Parameter(Mandatory=$true)][string]$Ip,
|
||||
[Parameter(Mandatory=$true)][string]$Gateway,
|
||||
[Parameter(Mandatory=$true)][string]$Cidr,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterHost,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterUser,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterPassword,
|
||||
[Parameter(Mandatory=$true)][string]$GuestPassword
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
try {
|
||||
Import-Module VMware.VimAutomation.Core -ErrorAction Stop
|
||||
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
|
||||
Set-PowerCLIConfiguration -Scope User -ParticipateInCEIP $false -Confirm:$false | Out-Null
|
||||
|
||||
Connect-VIServer -Server $VCenterHost -User $VCenterUser -Password $VCenterPassword -Force -ErrorAction Stop | Out-Null
|
||||
|
||||
$vm = Get-VM -Id "VirtualMachine-$VmId" -ErrorAction Stop
|
||||
|
||||
# Bash script executed inside the Linux guest via VMware Tools (Invoke-VMScript)
|
||||
$bashScript = @"
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Find the first usable non-loopback interface
|
||||
IFACE=`$(ip -o link show | awk -F': ' '{print `$2}' | grep -Ev '^(lo|vir|docker|br-)' | head -1)
|
||||
if [ -z "`$IFACE" ]; then
|
||||
echo "ERROR: no usable network interface found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Apply static IP immediately (survives until reboot without persistence layer)
|
||||
ip addr flush dev `$IFACE 2>/dev/null || true
|
||||
ip addr add $Ip/$Cidr dev `$IFACE
|
||||
ip link set `$IFACE up
|
||||
ip route del default 2>/dev/null || true
|
||||
ip route add default via $Gateway dev `$IFACE
|
||||
|
||||
# Persist via NetworkManager (RHEL/CentOS/Ubuntu 20+)
|
||||
if command -v nmcli &>/dev/null; then
|
||||
CON_NAME=`$(nmcli -t -f NAME con show 2>/dev/null | head -1)
|
||||
if [ -n "`$CON_NAME" ]; then
|
||||
nmcli con mod "`$CON_NAME" \
|
||||
ipv4.method manual \
|
||||
ipv4.addresses "$Ip/$Cidr" \
|
||||
ipv4.gateway "$Gateway" 2>/dev/null || true
|
||||
nmcli con up "`$CON_NAME" 2>/dev/null || true
|
||||
fi
|
||||
elif [ -d /etc/netplan ]; then
|
||||
# Netplan fallback (Ubuntu 18+ without NM)
|
||||
cat > /etc/netplan/99-static.yaml << 'YAML'
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
IFACE_PLACEHOLDER:
|
||||
dhcp4: false
|
||||
addresses: [IP_PLACEHOLDER/CIDR_PLACEHOLDER]
|
||||
gateway4: GATEWAY_PLACEHOLDER
|
||||
YAML
|
||||
sed -i "s/IFACE_PLACEHOLDER/`$IFACE/g;s|IP_PLACEHOLDER/CIDR_PLACEHOLDER|$Ip/$Cidr|g;s/GATEWAY_PLACEHOLDER/$Gateway/g" /etc/netplan/99-static.yaml
|
||||
netplan apply 2>/dev/null || true
|
||||
elif [ -d /etc/sysconfig/network-scripts ]; then
|
||||
# RHEL/CentOS legacy ifcfg
|
||||
cat > /etc/sysconfig/network-scripts/ifcfg-`$IFACE << EOF
|
||||
DEVICE=`$IFACE
|
||||
BOOTPROTO=static
|
||||
ONBOOT=yes
|
||||
IPADDR=$Ip
|
||||
PREFIX=$Cidr
|
||||
GATEWAY=$Gateway
|
||||
EOF
|
||||
ifdown `$IFACE 2>/dev/null || true
|
||||
ifup `$IFACE 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo SUCCESS
|
||||
"@
|
||||
|
||||
$result = Invoke-VMScript -VM $vm `
|
||||
-ScriptText $bashScript `
|
||||
-GuestUser "root" `
|
||||
-GuestPassword $GuestPassword `
|
||||
-ScriptType Bash `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($result.ScriptOutput -match "SUCCESS") {
|
||||
Write-Output "SUCCESS"
|
||||
} else {
|
||||
Write-Output "FAIL"
|
||||
}
|
||||
} catch {
|
||||
Write-Output "FAIL"
|
||||
} finally {
|
||||
Disconnect-VIServer -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# assignIPWindows.ps1
|
||||
# Deploy to: C:\CLISave\AssignIP\assignIPWindows.ps1 on the vSphere Windows host
|
||||
#
|
||||
# Usage: powershell -ExecutionPolicy Bypass -NonInteractive -File assignIPWindows.ps1 \
|
||||
# <vm_id> <ip> <gateway> <cidr> <vcenter_host> <vcenter_user> <vcenter_password> <guest_password>
|
||||
#
|
||||
# Output: SUCCESS | FAIL (exactly one line)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$VmId,
|
||||
[Parameter(Mandatory=$true)][string]$Ip,
|
||||
[Parameter(Mandatory=$true)][string]$Gateway,
|
||||
[Parameter(Mandatory=$true)][string]$Cidr,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterHost,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterUser,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterPassword,
|
||||
[Parameter(Mandatory=$true)][string]$GuestPassword
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
try {
|
||||
Import-Module VMware.VimAutomation.Core -ErrorAction Stop
|
||||
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
|
||||
Set-PowerCLIConfiguration -Scope User -ParticipateInCEIP $false -Confirm:$false | Out-Null
|
||||
|
||||
Connect-VIServer -Server $VCenterHost -User $VCenterUser -Password $VCenterPassword -Force -ErrorAction Stop | Out-Null
|
||||
|
||||
$vm = Get-VM -Id "VirtualMachine-$VmId" -ErrorAction Stop
|
||||
|
||||
# PowerShell script executed inside the Windows guest via VMware Tools
|
||||
$guestScript = @"
|
||||
`$ErrorActionPreference = 'Stop'
|
||||
try {
|
||||
# Find the first active ethernet adapter that is not a loopback
|
||||
`$adapter = Get-NetAdapter |
|
||||
Where-Object { `$_.Status -eq 'Up' -and `$_.InterfaceDescription -notlike '*Loopback*' } |
|
||||
Sort-Object -Property InterfaceIndex |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not `$adapter) { throw 'No active network adapter found' }
|
||||
|
||||
# Remove existing IPv4 addresses and routes on this interface
|
||||
`$adapter | Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||||
Remove-NetIPAddress -Confirm:`$false -ErrorAction SilentlyContinue
|
||||
Get-NetRoute -InterfaceIndex `$adapter.InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||||
Remove-NetRoute -Confirm:`$false -ErrorAction SilentlyContinue
|
||||
|
||||
# Set new static IP
|
||||
New-NetIPAddress `
|
||||
-InterfaceIndex `$adapter.InterfaceIndex `
|
||||
-IPAddress '$Ip' `
|
||||
-PrefixLength $Cidr `
|
||||
-DefaultGateway '$Gateway' `
|
||||
-ErrorAction Stop | Out-Null
|
||||
|
||||
Write-Output 'SUCCESS'
|
||||
} catch {
|
||||
Write-Output 'FAIL'
|
||||
}
|
||||
"@
|
||||
|
||||
$result = Invoke-VMScript -VM $vm `
|
||||
-ScriptText $guestScript `
|
||||
-GuestUser "administrator" `
|
||||
-GuestPassword $GuestPassword `
|
||||
-ScriptType Powershell `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($result.ScriptOutput -match "SUCCESS") {
|
||||
Write-Output "SUCCESS"
|
||||
} else {
|
||||
Write-Output "FAIL"
|
||||
}
|
||||
} catch {
|
||||
Write-Output "FAIL"
|
||||
} finally {
|
||||
Disconnect-VIServer -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# verifyIPLinux.ps1
|
||||
# Deploy to: C:\CLISave\AssignIP\verifyIPLinux.ps1 on the vSphere Windows host
|
||||
#
|
||||
# Usage: powershell -ExecutionPolicy Bypass -NonInteractive -File verifyIPLinux.ps1 \
|
||||
# <vm_id> <ip> <vcenter_host> <vcenter_user> <vcenter_password> <guest_password>
|
||||
#
|
||||
# Checks that the IP is configured and the interface is UP inside the Linux guest.
|
||||
# Output: SUCCESS | FAIL (exactly one line)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$VmId,
|
||||
[Parameter(Mandatory=$true)][string]$Ip,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterHost,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterUser,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterPassword,
|
||||
[Parameter(Mandatory=$true)][string]$GuestPassword
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
try {
|
||||
Import-Module VMware.VimAutomation.Core -ErrorAction Stop
|
||||
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
|
||||
Set-PowerCLIConfiguration -Scope User -ParticipateInCEIP $false -Confirm:$false | Out-Null
|
||||
|
||||
Connect-VIServer -Server $VCenterHost -User $VCenterUser -Password $VCenterPassword -Force -ErrorAction Stop | Out-Null
|
||||
|
||||
$vm = Get-VM -Id "VirtualMachine-$VmId" -ErrorAction Stop
|
||||
|
||||
# Bash snippet: verify IP exists on an active (UP) interface
|
||||
$verifyScript = @"
|
||||
#!/bin/bash
|
||||
# Check the IP appears in 'ip a' output on an interface that is UP (state UP)
|
||||
if ip a | grep -q '$Ip'; then
|
||||
IFACE=`$(ip -o addr show | awk '/$Ip/{print `$2}' | head -1)
|
||||
STATE=`$(cat /sys/class/net/`$IFACE/operstate 2>/dev/null || echo 'unknown')
|
||||
if [ "`$STATE" = "up" ] || [ "`$STATE" = "unknown" ]; then
|
||||
echo SUCCESS
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo FAIL
|
||||
exit 1
|
||||
"@
|
||||
|
||||
$result = Invoke-VMScript -VM $vm `
|
||||
-ScriptText $verifyScript `
|
||||
-GuestUser "root" `
|
||||
-GuestPassword $GuestPassword `
|
||||
-ScriptType Bash `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($result.ScriptOutput -match "SUCCESS") {
|
||||
Write-Output "SUCCESS"
|
||||
} else {
|
||||
Write-Output "FAIL"
|
||||
}
|
||||
} catch {
|
||||
Write-Output "FAIL"
|
||||
} finally {
|
||||
Disconnect-VIServer -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# verifyIPWindows.ps1
|
||||
# Deploy to: C:\CLISave\AssignIP\verifyIPWindows.ps1 on the vSphere Windows host
|
||||
#
|
||||
# Usage: powershell -ExecutionPolicy Bypass -NonInteractive -File verifyIPWindows.ps1 \
|
||||
# <vm_id> <ip> <vcenter_host> <vcenter_user> <vcenter_password> <guest_password>
|
||||
#
|
||||
# Checks that the IP is configured and not in Duplicate state inside the Windows guest.
|
||||
# Output: SUCCESS | FAIL (exactly one line)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$VmId,
|
||||
[Parameter(Mandatory=$true)][string]$Ip,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterHost,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterUser,
|
||||
[Parameter(Mandatory=$true)][string]$VCenterPassword,
|
||||
[Parameter(Mandatory=$true)][string]$GuestPassword
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
try {
|
||||
Import-Module VMware.VimAutomation.Core -ErrorAction Stop
|
||||
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
|
||||
Set-PowerCLIConfiguration -Scope User -ParticipateInCEIP $false -Confirm:$false | Out-Null
|
||||
|
||||
Connect-VIServer -Server $VCenterHost -User $VCenterUser -Password $VCenterPassword -Force -ErrorAction Stop | Out-Null
|
||||
|
||||
$vm = Get-VM -Id "VirtualMachine-$VmId" -ErrorAction Stop
|
||||
|
||||
# PowerShell snippet run inside the Windows guest
|
||||
$guestScript = @"
|
||||
`$ErrorActionPreference = 'SilentlyContinue'
|
||||
`$addr = Get-NetIPAddress -IPAddress '$Ip' -ErrorAction SilentlyContinue
|
||||
if (`$addr -and `$addr.AddressState -ne 'Duplicate' -and `$addr.AddressState -ne 'Invalid') {
|
||||
Write-Output 'SUCCESS'
|
||||
} else {
|
||||
Write-Output 'FAIL'
|
||||
}
|
||||
"@
|
||||
|
||||
$result = Invoke-VMScript -VM $vm `
|
||||
-ScriptText $guestScript `
|
||||
-GuestUser "administrator" `
|
||||
-GuestPassword $GuestPassword `
|
||||
-ScriptType Powershell `
|
||||
-ErrorAction Stop
|
||||
|
||||
if ($result.ScriptOutput -match "SUCCESS") {
|
||||
Write-Output "SUCCESS"
|
||||
} else {
|
||||
Write-Output "FAIL"
|
||||
}
|
||||
} catch {
|
||||
Write-Output "FAIL"
|
||||
} finally {
|
||||
Disconnect-VIServer -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
Reference in New Issue
Block a user