powerCliVmId($vmId); $remoteCmd = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS ? $this->windowsPowerShellCommand(self::SCRIPT_DIR, $script, [ $powerCliVmId, $newPassword, $vcenterHost, $vcenterUser, $vcenterPassword, ]) : sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s', self::SCRIPT_DIR, $script, $this->warg($powerCliVmId), $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: * VmId IP Gateway Cidr VCenterHost VCenterUser VCenterPassword 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; $powerCliVmId = $this->powerCliVmId($vmId); $remoteCmd = $isWindows ? $this->windowsPowerShellCommand(self::ASSIGN_DIR, $script, [ $powerCliVmId, $ip, $gateway, $cidr, $vcenterHost, $vcenterUser, $vcenterPassword, $guestPassword, ]) : sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s %s %s', self::ASSIGN_DIR, $script, $this->warg($powerCliVmId), $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, 'powercli_vm_id' => $powerCliVmId, '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: * VmId IP VCenterHost VCenterUser VCenterPassword 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; $powerCliVmId = $this->powerCliVmId($vmId); $remoteCmd = $isWindows ? $this->windowsPowerShellCommand(self::ASSIGN_DIR, $script, [ $powerCliVmId, $ip, $vcenterHost, $vcenterUser, $vcenterPassword, $guestPassword, ]) : sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s', self::ASSIGN_DIR, $script, $this->warg($powerCliVmId), $this->warg($ip), $this->warg($vcenterHost), $this->warg($vcenterUser), $this->warg($vcenterPassword), $this->warg($guestPassword), ); Log::info('VsphereHostSshService: executing verifyIpInGuest', [ 'vm_id' => $vmId, 'powercli_vm_id' => $powerCliVmId, '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 { $ssh = $this->sshConfig(); // 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($ssh['password']), self::CONNECT_TIMEOUT, $ssh['port'], escapeshellarg($ssh['username']), escapeshellarg($ssh['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 ' . $ssh['host'] . ' failed after ' . self::MAX_RETRIES . ' attempts', ); } /** * @return array{host:string, port:int, username:string, password:string} */ private function sshConfig(): array { $infra = InfraManagement::where('type', 'jumphost') ->where('status', true) ->first(); if (!$infra) { throw new \RuntimeException('No active jumphost configuration found in InfraManagement.'); } return [ 'host' => $infra->host, 'port' => (int) ($infra->meta_data['port'] ?? 22), 'username' => $infra->username, 'password' => SignRSA::DecryptFunc($infra->password), ]; } /** * 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; $powerCliVmId = $this->powerCliVmId($vmId); $remoteCmd = strtoupper($osFamily) === VmProvisionService::OS_WINDOWS ? $this->windowsPowerShellCommand(self::CHANGE_PWD_DIR, $script, [ $powerCliVmId, $newPassword, $vcenterHost, $vcenterUser, $vcenterPassword, $currentGuestPassword, ]) : sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -NonInteractive -File %s %s %s %s %s %s %s', self::CHANGE_PWD_DIR, $script, $this->warg($powerCliVmId), $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; } // ───────────────────────────────────────────────────────────────────────── /** * @param array $arguments */ private function windowsPowerShellCommand(string $workingDirectory, string $script, array $arguments): string { return sprintf( 'cd %s && powershell -ExecutionPolicy Bypass -File %s %s', $workingDirectory, $script, implode(' ', array_map( fn (string $argument): string => $this->warg($argument), $arguments, )), ); } /** * Windows cmd.exe safe argument: wrap in double quotes, escape inner double quotes. */ private function warg(string $value): string { return '"' . str_replace('"', '\\"', $value) . '"'; } /** * vCenter REST returns MoRef strings like "vm-3340"; PowerCLI scripts build * IDs as "VirtualMachine-$vmId", so keep the MoRef intact. */ private function powerCliVmId(string $vmId): string { return $vmId; } }