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; } // ───────────────────────────────────────────────────────────────────────── /** * 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. * * @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) . '"'; } }