Private/RemoteExecution.ps1
|
function Invoke-TbRemoteExecution { <# .SYNOPSIS Executes a script block on a remote computer. .DESCRIPTION Handles remote execution with support for WinRM, SSH, and localhost optimization. Manages sessions, credentials, and protocol selection. .PARAMETER Computer Target computer name or IP address. .PARAMETER ScriptBlock Script block to execute remotely. .PARAMETER ArgumentList Arguments to pass to the script block. .PARAMETER Credential PSCredential for remote authentication. .PARAMETER Protocol Remote protocol to use (WinRM, SSH, Auto). .PARAMETER TimeoutSeconds Execution timeout in seconds. .PARAMETER UseSSL Use SSL for WinRM connections. .EXAMPLE Invoke-TbRemoteExecution -Computer 'Server1' -ScriptBlock {Get-Service} -Credential $cred .OUTPUTS PSCustomObject with execution results. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Computer, [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [Parameter()] [object[]]$ArgumentList, [Parameter()] [System.Management.Automation.PSCredential]$Credential, [Parameter()] [ValidateSet('WinRM', 'SSH', 'Auto')] [string]$Protocol = 'Auto', [Parameter()] [int]$TimeoutSeconds = 60, [Parameter()] [switch]$UseSSL ) try { # Check if localhost $isLocalhost = Test-IsLocalhost -Computer $Computer if ($isLocalhost) { Write-Verbose "Executing locally on $Computer" return Invoke-LocalExecution -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList } # Determine protocol if ($Protocol -eq 'Auto') { $Protocol = Get-OptimalProtocol -Computer $Computer } Write-Verbose "Executing on $Computer using $Protocol protocol" # Execute based on protocol switch ($Protocol) { 'WinRM' { return Invoke-WinRMExecution -Computer $Computer -ScriptBlock $ScriptBlock ` -ArgumentList $ArgumentList -Credential $Credential ` -TimeoutSeconds $TimeoutSeconds -UseSSL:$UseSSL } 'SSH' { return Invoke-SSHExecution -Computer $Computer -ScriptBlock $ScriptBlock ` -ArgumentList $ArgumentList -Credential $Credential ` -TimeoutSeconds $TimeoutSeconds } default { throw "Unsupported protocol: $Protocol" } } } catch { return [PSCustomObject]@{ Success = $false Output = $null Error = $_ Protocol = $Protocol IsLocalhost = $isLocalhost } } } function Test-IsLocalhost { <# .SYNOPSIS Determines if a computer name refers to the local machine. #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [string]$Computer ) $localhostNames = @( 'localhost', '127.0.0.1', '::1', $env:COMPUTERNAME, ([System.Net.Dns]::GetHostName()), '.' ) return $Computer -in $localhostNames } function Get-OptimalProtocol { <# .SYNOPSIS Determines the optimal remoting protocol for a computer. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string]$Computer ) # For Windows PowerShell or Windows OS, prefer WinRM if ($PSVersionTable.PSVersion.Major -le 5 -or $IsWindows) { return 'WinRM' } # For PowerShell Core on non-Windows, prefer SSH if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { return 'SSH' } # Default to WinRM return 'WinRM' } function Invoke-LocalExecution { <# .SYNOPSIS Executes a script block locally (optimized path). #> [CmdletBinding()] param( [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [Parameter()] [object[]]$ArgumentList ) try { $result = if ($ArgumentList -and $ArgumentList.Count -gt 0) { # If ArgumentList contains a single hashtable, wrap in a script block that splats if ($ArgumentList.Count -eq 1 -and $ArgumentList[0] -is [hashtable]) { $params = $ArgumentList[0] # Invoke with splatting - need to access the hashtable directly & $ScriptBlock @params } else { # Pass as positional arguments & $ScriptBlock $ArgumentList } } else { & $ScriptBlock } return [PSCustomObject]@{ Success = $true Output = $result Error = $null Protocol = 'Local' IsLocalhost = $true } } catch { return [PSCustomObject]@{ Success = $false Output = $null Error = $_.Exception.Message Protocol = 'Local' IsLocalhost = $true } } } function Invoke-WinRMExecution { <# .SYNOPSIS Executes a script block via WinRM. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Computer, [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [Parameter()] [object[]]$ArgumentList, [Parameter()] [System.Management.Automation.PSCredential]$Credential, [Parameter()] [int]$TimeoutSeconds = 60, [Parameter()] [switch]$UseSSL ) try { # Build session options $sessionOptions = New-PSSessionOption -OperationTimeout ($TimeoutSeconds * 1000) ` -NoMachineProfile # Build Invoke-Command parameters $invokeParams = @{ ComputerName = $Computer ScriptBlock = $ScriptBlock ErrorAction = 'Stop' SessionOption = $sessionOptions } if ($ArgumentList) { $invokeParams.ArgumentList = $ArgumentList } if ($Credential) { $invokeParams.Credential = $Credential } if ($UseSSL) { $invokeParams.UseSSL = $true } # Execute $output = Invoke-Command @invokeParams return [PSCustomObject]@{ Success = $true Output = $output Error = $null Protocol = 'WinRM' IsLocalhost = $false } } catch { return [PSCustomObject]@{ Success = $false Output = $null Error = $_ Protocol = 'WinRM' IsLocalhost = $false } } } function Invoke-SSHExecution { <# .SYNOPSIS Executes a script block via SSH (PowerShell 7+). #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Computer, [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [Parameter()] [object[]]$ArgumentList, [Parameter()] [System.Management.Automation.PSCredential]$Credential, [Parameter()] [int]$TimeoutSeconds = 60 ) # Check if PowerShell 7+ (SSH remoting requires it) if ($PSVersionTable.PSVersion.Major -lt 7) { throw "SSH remoting requires PowerShell 7 or higher" } try { # Build Invoke-Command parameters for SSH $invokeParams = @{ HostName = $Computer ScriptBlock = $ScriptBlock ErrorAction = 'Stop' } if ($ArgumentList) { $invokeParams.ArgumentList = $ArgumentList } if ($Credential) { $invokeParams.UserName = $Credential.UserName # Note: SSH typically uses key-based auth, password auth requires additional setup } # Execute $output = Invoke-Command @invokeParams return [PSCustomObject]@{ Success = $true Output = $output Error = $null Protocol = 'SSH' IsLocalhost = $false } } catch { return [PSCustomObject]@{ Success = $false Output = $null Error = $_ Protocol = 'SSH' IsLocalhost = $false } } } function New-TbPSSession { <# .SYNOPSIS Creates a new PSSession with proper configuration. .DESCRIPTION Creates and configures a PSSession for reuse scenarios. .PARAMETER Computer Target computer name. .PARAMETER Credential PSCredential for authentication. .PARAMETER Protocol Protocol to use (WinRM or SSH). .PARAMETER UseSSL Use SSL for WinRM connections. .EXAMPLE $session = New-TbPSSession -Computer 'Server1' -Credential $cred .OUTPUTS System.Management.Automation.Runspaces.PSSession #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Computer, [Parameter()] [System.Management.Automation.PSCredential]$Credential, [Parameter()] [ValidateSet('WinRM', 'SSH')] [string]$Protocol = 'WinRM', [Parameter()] [switch]$UseSSL ) try { $sessionParams = @{ ErrorAction = 'Stop' } if ($Protocol -eq 'SSH') { if ($PSVersionTable.PSVersion.Major -lt 7) { throw "SSH sessions require PowerShell 7 or higher" } $sessionParams.HostName = $Computer if ($Credential) { $sessionParams.UserName = $Credential.UserName } } else { $sessionParams.ComputerName = $Computer if ($Credential) { $sessionParams.Credential = $Credential } if ($UseSSL) { $sessionParams.UseSSL = $true } } $session = New-PSSession @sessionParams Write-Verbose "Created PSSession to $Computer using $Protocol" return $session } catch { Write-Error "Failed to create PSSession to $Computer : $_" throw } } |