From 8aef607e1f1a96cb64e2c82cbe1b2b05630eee43 Mon Sep 17 00:00:00 2001 From: pbao9 Date: Sat, 18 Apr 2026 08:51:02 +0700 Subject: [PATCH] feat: Implement IP assignment and verification for VM guests; add related PowerShell scripts --- .../Create/CreateVirtualMachineJob.php | 64 +--- .../Provision/AssignIpToGuestJob.php | 335 ++++++++++++++++++ .../Provision/ChangePasswordViaVsphereJob.php | 17 +- app/Services/VsphereHostSshService.php | 112 ++++++ resources/scripts/vm/assignIPLinux.ps1 | 107 ++++++ resources/scripts/vm/assignIPWindows.ps1 | 80 +++++ resources/scripts/vm/verifyIPLinux.ps1 | 63 ++++ resources/scripts/vm/verifyIPWindows.ps1 | 58 +++ 8 files changed, 777 insertions(+), 59 deletions(-) create mode 100644 app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php create mode 100644 resources/scripts/vm/assignIPLinux.ps1 create mode 100644 resources/scripts/vm/assignIPWindows.ps1 create mode 100644 resources/scripts/vm/verifyIPLinux.ps1 create mode 100644 resources/scripts/vm/verifyIPWindows.ps1 diff --git a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php index 2929e8a..bf293e0 100644 --- a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php +++ b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php @@ -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 */ diff --git a/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php b/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php new file mode 100644 index 0000000..c6016b8 --- /dev/null +++ b/app/Jobs/VirtualMachine/Provision/AssignIpToGuestJob.php @@ -0,0 +1,335 @@ + $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; + } +} diff --git a/app/Jobs/VirtualMachine/Provision/ChangePasswordViaVsphereJob.php b/app/Jobs/VirtualMachine/Provision/ChangePasswordViaVsphereJob.php index dc4a178..14430b2 100644 --- a/app/Jobs/VirtualMachine/Provision/ChangePasswordViaVsphereJob.php +++ b/app/Jobs/VirtualMachine/Provision/ChangePasswordViaVsphereJob.php @@ -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, diff --git a/app/Services/VsphereHostSshService.php b/app/Services/VsphereHostSshService.php index 54a1ced..2654e07 100644 --- a/app/Services/VsphereHostSshService.php +++ b/app/Services/VsphereHostSshService.php @@ -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. diff --git a/resources/scripts/vm/assignIPLinux.ps1 b/resources/scripts/vm/assignIPLinux.ps1 new file mode 100644 index 0000000..1c66f67 --- /dev/null +++ b/resources/scripts/vm/assignIPLinux.ps1 @@ -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 \ +# +# +# 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 +} diff --git a/resources/scripts/vm/assignIPWindows.ps1 b/resources/scripts/vm/assignIPWindows.ps1 new file mode 100644 index 0000000..5105a02 --- /dev/null +++ b/resources/scripts/vm/assignIPWindows.ps1 @@ -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 \ +# +# +# 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 +} diff --git a/resources/scripts/vm/verifyIPLinux.ps1 b/resources/scripts/vm/verifyIPLinux.ps1 new file mode 100644 index 0000000..d78c115 --- /dev/null +++ b/resources/scripts/vm/verifyIPLinux.ps1 @@ -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 \ +# +# +# 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 +} diff --git a/resources/scripts/vm/verifyIPWindows.ps1 b/resources/scripts/vm/verifyIPWindows.ps1 new file mode 100644 index 0000000..9350547 --- /dev/null +++ b/resources/scripts/vm/verifyIPWindows.ps1 @@ -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 \ +# +# +# 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 +}