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\. * * Windows script params (7): VMName IP MaskBits Gateway DNS GuestUser GuestPassword * Linux script params (10): VCenter VCUser VCPass VMName IP MaskBits Gateway DNS GuestUser GuestPassword * * Script output: JSON {"status":"ok"} or {"status":"error","message":"..."} */ public function assignIp( string $vmId, string $ip, string $gateway, string $cidr, string $vcenterHost, string $vcenterUser, string $vcenterPassword, string $guestPassword, string $osFamily, string $dns, string $guestUser, ): string { $isWindows = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS; $script = $isWindows ? self::SCRIPT_ASSIGN_WINDOWS : self::SCRIPT_ASSIGN_LINUX; if ($isWindows) { // assignIPWindows.ps1: VMName IP MaskBits Gateway DNS GuestUser GuestPassword $remoteCmd = sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s %s', self::ASSIGN_DIR, $script, $this->warg($vmId), $this->warg($ip), $this->warg($cidr), $this->warg($gateway), $this->warg($dns), $this->warg($guestUser), $this->warg($guestPassword), ); } else { // assignIPLinux.ps1: VCenter VCUser VCPass VMName IP MaskBits Gateway DNS GuestUser GuestPassword $remoteCmd = sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s %s %s %s %s', self::ASSIGN_DIR, $script, $this->warg($vcenterHost), $this->warg($vcenterUser), $this->warg($vcenterPassword), $this->warg($vmId), $this->warg($ip), $this->warg($cidr), $this->warg($gateway), $this->warg($dns), $this->warg($guestUser), $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\. * * Windows script params (4): VMName IP GuestUser GuestPassword * Linux script params (7): VCenter VCUser VCPass VMName IP GuestUser GuestPassword * * Script output: JSON {"status":"ok"} or {"status":"error","message":"..."} */ public function verifyIpInGuest( string $vmId, string $ip, string $vcenterHost, string $vcenterUser, string $vcenterPassword, string $guestPassword, string $osFamily, string $guestUser, ): string { $isWindows = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS; $script = $isWindows ? self::SCRIPT_VERIFY_WINDOWS : self::SCRIPT_VERIFY_LINUX; if ($isWindows) { // verifyIPWindows.ps1: VMName IP GuestUser GuestPassword $remoteCmd = sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s', self::ASSIGN_DIR, $script, $this->warg($vmId), $this->warg($ip), $this->warg($guestUser), $this->warg($guestPassword), ); } else { // verifyIPLinux.ps1: VCenter VCUser VCPass VMName IP GuestUser GuestPassword $remoteCmd = sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s %s', self::ASSIGN_DIR, $script, $this->warg($vcenterHost), $this->warg($vcenterUser), $this->warg($vcenterPassword), $this->warg($vmId), $this->warg($ip), $this->warg($guestUser), $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', ); } /** * Customer-initiated VM password change. * * Calls changePwdLinux.ps1 / changePwdWindows.ps1 in C:\CLISave\changePassword\. * * Param order (matches PS1 declaration): * $vm vm_id / vmware display name double-quoted * $password new password single-quoted * $vcenter_host vCenter host double-quoted * $vcenter_user vCenter user double-quoted * $vcenter_password vCenter password single-quoted * $guest_password current guest password single-quoted * * PS1 outputs "SUCCESS" or "ERROR". */ public function changePasswordCustomer( string $vmId, string $newPassword, string $currentGuestPassword, string $osFamily, string $vcenterHost, string $vcenterUser, string $vcenterPassword, ): string { $script = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS ? self::SCRIPT_CHANGE_PWD_WINDOWS : self::SCRIPT_CHANGE_PWD_LINUX; $remoteCmd = sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s', self::CHANGE_PWD_DIR, $script, $this->warg($vmId), $this->warg($newPassword), $this->warg($vcenterHost), $this->warg($vcenterUser), $this->warg($vcenterPassword), $this->warg($currentGuestPassword), ); Log::info('VsphereHostSshService: changePasswordCustomer', [ 'vm_id' => $vmId, 'script' => $script, 'os' => $osFamily, ]); $output = $this->sshExec($remoteCmd); Log::info('VsphereHostSshService: changePasswordCustomer output', [ 'vm_id' => $vmId, 'output' => trim($output), ]); return $output; } // ───────────────────────────────────────────────────────────────────────── /** * Windows cmd.exe safe argument: wrap in double quotes, escape inner double quotes. */ private function warg(string $value): string { return '"' . str_replace('"', '\\"', $value) . '"'; } /** * Single-quote wrap for passwords — prevents PowerShell variable expansion on $, %, ^ etc. * Inside a single-quoted PS string, a literal single quote is escaped as ''. */ private function sarg(string $value): string { return "'" . str_replace("'", "''", $value) . "'"; } }