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) . '"'; } }