当前位置:网站首页 > 更多 > 玩电脑 > 正文

[玩转系统] PowerShell远程功能框架

作者:精品下载站 日期:2024-12-14 07:58:25 浏览:18 分类:玩电脑

PowerShell远程功能框架


前几天,我分享了一个 PowerShell 函数,用于查询远程计算机上的注册表以查找已安装的 PowerShell 版本。该函数利用 PowerShell 远程处理,灵活地使用具有可选凭据或现有 PSSession 的计算机名称。我想得越多,我就越意识到该结构可以重新用于我想在远程计算机上运行的任何命令。我认为这是使用 PowerShell 的绝佳方式。经过一些修改,我想出了所谓的远程函数框架。

该函数的大部分参数都基于您用于创建 PSSession 的参数。该函数使用会话远程运行脚本块。因为我想支持可以使用 SSH 进行远程处理的 PowerShell 7,所以我想添加必要的参数,例如主机名。这些参数仅在 PowerShell 7 上可用,因此我将它们定义为具有自己的参数集的动态参数。不过,您可以轻松地在 Param 块中定义参数。

该函数根据需要一次创建一个临时会话。我这样做是为了更好地处理异常。代码还必须考虑参数传递的值或来自管道的值。为了简化操作,我删除了 $Computername 的默认值并将其设为强制值。

要从我的框架创建您自己的命令,您真正需要做的就是定义脚本块以及您可能需要的任何可选参数。

框架功能在Github上。

RemoteFunctionFramework.ps1:

#requires -version 5.1



#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

我留下了一些#TODO 评论来指导您需要进行的更改。作为概念证明,这里有一个终止远程计算机上进程的函数。

Stop-RemoteProcess.ps1:

#requires -version 5.1

Function Stop-RemoteProcess {

    #TODO: Create help documentation

    [cmdletbinding(DefaultParameterSetName = "computer")]
    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 = "session",
            ValueFromPipeline
            )]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.Runspaces.PSSession[]]$Session,

        [ValidateScript( {$_ -ge 0})]
        [int32]$ThrottleLimit = 32,

        [Parameter(Mandatory,HelpMessage = "Specify the process to stop.")]
        [ValidateNotNullOrEmpty()]
        [string]$ProcessName,

        [Parameter(HelpMessage = "Write the stopped process to the pipeline")]
        [switch]$Passthru,

        [Parameter(HelpMessage = "Run the remote command with -WhatIf")]
        [switch]$WhatIfRemote
    )
    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"

        $sb = {
            param([string]$ProcessName,[bool]$Passthru,[string]$VerbPref = "SilentlyContinue", [bool]$WhatPref)

            $VerbosePreference = $VerbPref
            $WhatIfPreference = $WhatPref

            Try {
                Write-Verbose "[$(New-TimeSpan -start $using:start) REMOTE ] Getting Process $ProcessName on $([System.Environment]::MachineName)"
                $procs = Get-Process -Name $ProcessName -ErrorAction stop
                Try {
                     Write-Verbose "[$(New-TimeSpan -start $using:start) REMOTE ] Stopping $($procs.count) Processes on $([System.Environment]::MachineName)"
                    $procs | Stop-Process -ErrorAction Stop -PassThru:$Passthru
                }
                Catch {
                    Write-Warning "[$(New-TimeSpan -start $using:start) REMOTE ] Failed to stop Process $ProcessName on $([System.Environment]::MachineName). $($_.Exception.message)."
                }
            }
            Catch {
                Write-Verbose "[$(New-TimeSpan -start $using:start) REMOTE ] Process $ProcessName not found on $([System.Environment]::MachineName)"
            }

        } #scriptblock

        #parameters to splat to Invoke-Command
        Write-Verbose "[$(New-TimeSpan -start $start) BEGIN  ] Defining parameters for Invoke-Command"

        #remove my parameters from PSBoundparameters because they can't be used with New-PSSession
        $myparams = "ProcessName","WhatIfRemote","passthru"
        foreach ($my in $myparams) {
            if ($PSBoundParameters.ContainsKey($my)) {
                [void]($PSBoundParameters.remove($my))
            }
        }

        $icmParams = @{
            Scriptblock      = $sb
            #I added the relevant parameters from the function
            Argumentlist     = @($ProcessName,$Passthru,$VerbosePreference,$WhatIfRemote)
            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

当您查看代码时,您可以看到我如何处理远程脚本块所需的参数。包括对 -WhatIf 的支持。按照我写的方式,$PSBoundParameters 会被分配到 New-PSSession,因此您需要删除已添加的任何不属于的内容。您可以在代码中看到我是如何做到这一点的。

几分钟之内我就得到了一个功能齐全的命令。

[玩转系统] PowerShell远程功能框架

[玩转系统] PowerShell远程功能框架

它也是这样工作的。

[玩转系统] PowerShell远程功能框架

我很期待看到我还能用这个框架做些什么。我很想听听你如何使用它。享受。

您需要 登录账户 后才能发表评论

取消回复欢迎 发表评论:

关灯