powerCliVmId($vmId); $remoteCmd = $this->windowsPowerShellCommand(self::SCRIPT_DIR, $script, [ $powerCliVmId, $newPassword, $vcenterHost, $vcenterUser, $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 = $this->windowsPowerShellCommand(self::ASSIGN_DIR, $script, [ $powerCliVmId, $ip, $gateway, $cidr, $vcenterHost, $vcenterUser, $vcenterPassword, $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 = $this->windowsPowerShellCommand(self::ASSIGN_DIR, $script, [ $powerCliVmId, $ip, $vcenterHost, $vcenterUser, $vcenterPassword, $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 phpseclib (no sshpass required). * * @throws \RuntimeException */ private function sshExec(string $remoteCmd): string { $config = $this->sshConfig(); $client = new SSH2($config['host'], $config['port'], self::CONNECT_TIMEOUT); if (!$client->login($config['username'], $config['password'])) { throw new \RuntimeException( "SSH login failed: {$config['host']}:{$config['port']}" ); } $client->setTimeout(0); // PS1 scripts can run 30–90 s; block until done $stdout = $client->exec($remoteCmd); $stderr = $client->getStdError(); Log::info('VsphereHostSshService: SSH executed', [ 'host' => $config['host'], 'port' => $config['port'], 'stdout' => trim((string) $stdout), 'stderr' => trim((string) $stderr), ]); if ($stdout === false) { throw new \RuntimeException( "SSH exec failed on {$config['host']}:{$config['port']}. stderr: " . trim((string) $stderr) ); } // Windows SSH servers sometimes write script output to stderr instead of stdout if (trim($stdout) === '' && trim((string) $stderr) !== '') { return (string) $stderr; } return (string) $stdout; } /** * @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), ]; } /** * Update VM CPU and memory via updateVMConfig.ps1. * * Script path: C:\CLISave\Addon\updateVMConfig.ps1 * Params: $server_name $cpu $memoryGB $vcenter_host $vcenter_user $vcenter_password * Output: "SUCCESS" or "FAIL" * * PS1 must call Connect-VIServer before Get-VM — see updateVMConfig.ps1 on vSphere host. */ public function updateVmConfig( string $serverName, int $cpu, int $memoryGB, string $vcenterHost, string $vcenterUser, string $vcenterPassword, ): string { $remoteCmd = $this->windowsPowerShellCommand(self::ADDON_DIR, self::SCRIPT_UPDATE_VM_CONFIG, [ $serverName, (string) $cpu, (string) $memoryGB, $vcenterHost, $vcenterUser, $vcenterPassword, ]); Log::info('VsphereHostSshService: executing updateVmConfig', [ 'server_name' => $serverName, 'cpu' => $cpu, 'memoryGB' => $memoryGB, 'vcenter_host' => $vcenterHost, ]); $output = $this->sshExec($remoteCmd); Log::info('VsphereHostSshService: updateVmConfig output', [ 'server_name' => $serverName, 'output' => trim($output), ]); return $output; } /** * 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 = $this->windowsPowerShellCommand(self::CHANGE_PWD_DIR, $script, [ $powerCliVmId, $newPassword, $vcenterHost, $vcenterUser, $vcenterPassword, $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( 'powershell -ExecutionPolicy Bypass -NonInteractive -File "%s\\%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; } }