Last active
November 10, 2023 15:44
-
-
Save jdhitsolutions/5978a209cc912132ea2052a33a855327 to your computer and use it in GitHub Desktop.
The framework of a PowerShell advanced function designed to run cross-platform that PowerShell remoting.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#requires -version 5.1 | |
<# | |
This is a function template that utilizes remoting to get or do something from | |
remote computers. The framework is written to support both Windows PowerShell | |
and remoting with SSH in PowerShell 7. It defines dynamic parameters for | |
PwoerShell 7. You may decide to re-define them as normal parameters. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED. | |
#> | |
#TODO: DEFINE A VALID COMMAND NAME | |
Function Verb-Noun { | |
#TODO: Create help documentation for your command | |
[cmdletbinding(DefaultParameterSetName = "computer")] | |
#TODO: Add and modify parameters as necessary | |
Param( | |
[Parameter( | |
ParameterSetName = "computer", | |
Mandatory, | |
Position = 0, | |
ValueFromPipeline, | |
ValueFromPipelineByPropertyName, | |
HelpMessage = "Enter the name of a computer to query. The default is the local host." | |
)] | |
[ValidateNotNullOrEmpty()] | |
[Alias("cn")] | |
[string[]]$ComputerName, | |
[Parameter( | |
ParameterSetName = "computer", | |
HelpMessage = "Enter a credential object or username." | |
)] | |
[Alias("RunAs")] | |
[PSCredential]$Credential, | |
[Parameter(ParameterSetName = "computer")] | |
[switch]$UseSSL, | |
[Parameter(ParameterSetName = "computer")] | |
[ValidateSet("Default", "Basic", "Credssp", "Digest", "Kerberos", "Negotiate", "NegotiateWithImplicitCredentialqhel")] | |
[string]$Authentication = "Default", | |
[Parameter( | |
ParameterSetName = "session", | |
ValueFromPipeline | |
)] | |
[ValidateNotNullOrEmpty()] | |
[System.Management.Automation.Runspaces.PSSession[]]$Session, | |
[ValidateScript( {$_ -ge 0})] | |
[int32]$ThrottleLimit = 32 | |
) | |
DynamicParam { | |
#Add an SSH dynamic parameter if in PowerShell 7 | |
if ($isCoreCLR) { | |
$paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary | |
#a CSV file with dynamic parameters to create | |
#this approach doesn't take any type of parameter validation into account | |
$data = @" | |
Name,Type,Mandatory,Default,Help | |
HostName,string[],1,,"Enter the remote host name." | |
UserName,string,0,,"Enter the remote user name." | |
Subsystem,string,0,"powershell","The name of the ssh subsystem. The default is powershell." | |
Port,int32,0,,"Enter an alternate SSH port" | |
KeyFilePath,string,0,,"Specify a key file path used by SSH to authenticate the user" | |
SSHTransport,switch,0,,"Use SSH to connect." | |
"@ | |
$data | ConvertFrom-Csv | ForEach-Object -begin { } -process { | |
$attributes = New-Object System.Management.Automation.ParameterAttribute | |
$attributes.Mandatory = ([int]$_.mandatory) -as [bool] | |
$attributes.HelpMessage = $_.Help | |
$attributes.ParameterSetName = "SSH" | |
$attributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] | |
$attributeCollection.Add($attributes) | |
$dynParam = New-Object -Type System.Management.Automation.RuntimeDefinedParameter($_.name, $($_.type -as [type]), $attributeCollection) | |
$dynParam.Value = $_.Default | |
$paramDictionary.Add($_.name, $dynParam) | |
} -end { | |
return $paramDictionary | |
} | |
} | |
} #dynamic param | |
Begin { | |
#capture the start time. The Verbose messages can display a timespan. | |
$start = Get-Date | |
#the first verbose message uses a pseudo timespan to reflect the idea we're just starting | |
Write-Verbose "[00:00:00.0000000 BEGIN ] Starting $($myinvocation.mycommand)" | |
#a script block to be run remotely | |
Write-Verbose "[$(New-TimeSpan -start $start) BEGIN ] Defining the scriptblock to be run remotely." | |
#TODO: define the scriptblock to be run remotely | |
$sb = { | |
param([string]$VerbPref = "SilentlyContinue", [bool]$WhatPref) | |
$VerbosePreference = $VerbPref | |
$WhatIfPreference = $WhatPref | |
#TODO: add verbose messaging | |
#the timespan assumes an accurate clock on the remote computer | |
Write-Verbose "[$(New-TimeSpan -start $using:start) REMOTE ] Doing something remotely on $([System.Environment]::MachineName)." | |
"[$([System.Environment]::MachineName)] Hello, World" | |
} #scriptblock | |
#parameters to splat to Invoke-Command | |
Write-Verbose "[$(New-TimeSpan -start $start) BEGIN ] Defining parameters for Invoke-Command." | |
#TODO: Update arguments as needed. This framework assumes any arguments are NOT coming through the pipeline and will be the same for all remote computers | |
#TODO: You will need to handle parameters like -WhatIf that you want to pass remotely | |
$icmParams = @{ | |
Scriptblock = $sb | |
Argumentlist = $VerbosePreference, $WhatIfPreference | |
HideComputerName = $False | |
ThrottleLimit = $ThrottleLimit | |
ErrorAction = "Stop" | |
Session = $null | |
} | |
#initialize an array to hold session objects | |
[System.Management.Automation.Runspaces.PSSession[]]$All = @() | |
If ($Credential.username) { | |
Write-Verbose "[$(New-TimeSpan -start $start) BEGIN ] Using alternate credential for $($credential.username)." | |
} | |
} #begin | |
Process { | |
Write-Verbose "[$(New-TimeSpan -start $start) PROCESS] Detected parameter set $($pscmdlet.ParameterSetName)." | |
Write-Verbose "[$(New-TimeSpan -start $start) PROCESS] Detected PSBoundParameters:`n$($PSBoundParameters | Out-String)" | |
$remotes = @() | |
if ($PSCmdlet.ParameterSetName -match "computer|ssh") { | |
if ($pscmdlet.ParameterSetName -eq 'ssh') { | |
$remotes += $PSBoundParameters.HostName | |
$param = "HostName" | |
} | |
else { | |
$remotes += $PSBoundParameters.ComputerName | |
$param = "ComputerName" | |
} | |
foreach ($remote in $remotes) { | |
$PSBoundParameters[$param] = $remote | |
$PSBoundParameters["ErrorAction"] = "Stop" | |
Try { | |
#create a session one at a time to better handle errors | |
Write-Verbose "[$(New-TimeSpan -start $start) PROCESS] Creating a temporary PSSession to $remote." | |
#save each created session to $tmp so it can be removed at the end | |
#TODO: If your function will add parameters they will need to be removed from $PSBoundParamters or you will need to adjust the the command to create the New-PSSession | |
$all += New-PSSession @PSBoundParameters -OutVariable +tmp | |
} #Try | |
Catch { | |
#TODO: Decide what you want to do when the new session fails | |
Write-Warning "Failed to create session to $remote. $($_.Exception.Message)." | |
#Write-Error $_ | |
} #catch | |
} #foreach remote | |
} | |
Else { | |
#only add open sessions | |
foreach ($sess in $session) { | |
if ($sess.state -eq 'opened') { | |
Write-Verbose "[$(New-TimeSpan -start $start) PROCESS] Using session for $($sess.ComputerName.toUpper())." | |
$all += $sess | |
} #if open | |
} #foreach session | |
} #else sessions | |
} #process | |
End { | |
$icmParams["session"] = $all | |
Try { | |
Write-Verbose "[$(New-TimeSpan -start $start) END ] Querying $($all.count) computers." | |
Invoke-Command @icmParams | ForEach-Object { | |
#TODO: PROCESS RESULTS FROM EACH REMOTE CONNECTION IF NECESSARY | |
$_ | |
} #foreach result | |
} #try | |
Catch { | |
Write-Error $_ | |
} #catch | |
if ($tmp) { | |
Write-Verbose "[$(New-TimeSpan -start $start) END ] Removing $($tmp.count) temporary PSSessions." | |
$tmp | Remove-PSSession | |
} | |
Write-Verbose "[$(New-TimeSpan -start $start) END ] Ending $($myinvocation.mycommand)" | |
} #end | |
} #close function |
I've created a PowerShell snippet of this function for Visual Studio Code at https://gist.github.com/jdhitsolutions/111ba054133bde290592d7436af30fa5
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See https://jdhitsolutions.com/blog/powershell/7458/a-powershell-remote-function-framework/ for more information