如何将自定义PowerShell模块导入远程会话?

41
我正在开发一个自定义的 PowerShell 模块,希望能在与不同计算机上的远程会话中使用。下面的代码(显然不能工作)解释了我想要实现的目标:
import-module .\MyCustomModule.psm1
$session = new-pssession -computerName server01
invoke-command -session $session -scriptblock { 
  <# use function defined in MyCustomModule here #> 
}

第一个问题是是否有可能实现这种情况? 我的意思是我只想让我的自定义模块在我的本地机器上物理存在,而不是在远程服务器上。

我找到了这个帖子,但是我没能使它工作 - 它不允许从远程机器创建一个会话返回到本地机器。可能,我遇到了该帖子评论中提到的配置限制...此外,作者还提到了对我的解决方案至关重要的性能影响。

如果可能的话,怎么做?

PowerShell的版本目前不是限制因素 - 如果解决方案仅适用于PS 3.0 - 我可以接受。


重复问题,参见:https://dev59.com/aXE85IYBdhLWcg3wVR6g? - David Brabant
@DavidBrabant,嗯,是的,它非常接近。但是,我无法使那里的解决方案起作用,并且我明确引用了该线程以指示我尝试了该选项并寻求替代方案 :) - Yan Sklyarenko
真的需要成为一个模块吗?我非常支持使用模块,但是Invoke-Command -FilePath将传输单个脚本文件到远程计算机以供执行。当然,如果脚本文件尝试点源或以其他方式调用其他脚本文件,则它们必须在远程计算机上,因为Invoke-Command不会自动复制它们。 - Keith Hill
@KeithHill,嗯,是的...我是说,不是... :) 你在 PowerShell 宇宙还没有成熟之前就搞乱了它!除非我们谈论可扩展性、结构良好、模块化和易于重用的代码,否则它不必成为一个模块。但我理解你的观点,并将在我的特定情况下权衡利弊。实际上,我正在(首先是原型制作,然后)创建一个部署框架来部署我们的应用程序,并将实用函数和 cmdlet 拆分成模块似乎是我要走的路。你会如何处理这个任务?在目标上要求一些先决条件是否可靠? - Yan Sklyarenko
冒着进一步干扰您的PowerShell宇宙的风险,是否有某些原因使函数无法在不被放入模块的情况下实现可扩展性、良好结构化、模块化和易于重用? - mjolinor
显示剩余3条评论
6个回答

47

这个问题有一些非常好的评论,我花了一些时间研究了解决该问题的各种方法。

首先,我最初提出的要求是不可能实现的。我的意思是,如果你采用模块方式,那么模块必须在目标计算机上物理存在才能够导入模块到远程会话中。

为了更进一步的抽象我的问题,我正在尝试创建一个可重复使用的基于PowerShell的产品部署框架。它将是一种推式部署方式,这意味着我们鼓励人们在本地计算机上运行一些脚本来部署到某些远程服务器。就我所调查的领域而言,有两种可能符合常识的方法。

模块方法

需要遵循的过程:

  • 将每个逻辑上不同的功能放入PowerShell模块(*.psm1)中
  • 将模块分发到远程计算机并扩展PSModulePath变量以包括新模块的位置
  • 在客户端计算机上,创建一个新会话到远程服务器,并使用Invoke-Command -Session $s -ScriptBlock {...}
  • 在脚本块中从Import-Module CustomModule开始 - 它将在远程计算机上搜索CustomModule,并显然会找到它

优点

以下是喜欢这种方法的原因:

  • 传统模块角色的结果 - 促进可重用库的创建
  • 根据伟大的书Windows PowerShell in Action,“模块可以用于创建特定领域的应用程序”。就我所理解的而言,它可以通过组合模块嵌套和混合脚本/二进制模块来暴露特定于某个领域的直观界面来实现。基本上,这是我最看重的基于PowerShell的部署框架目标

缺点

需要考虑以下几点:

  • 你必须找到一种方式将自定义模块传递到远程机器。我尝试过 NuGet,但不确定它是否适合这项任务,但也有其他选择,例如 MSI 安装程序或来自共享文件夹的简单 xcopy。此外,传递机制应支持升级/降级和(最好)多实例安装,但这更与我的任务相关,而不是一般问题。

脚本方法

需要遵循以下步骤:

  • 将每个逻辑上不同的功能部分放在一个单独的 PowerShell 脚本 (*.ps1) 中
  • 在客户端机器上,创建一个新会话到远程服务器,并使用 Invoke-Command -Session $s -FilePath .\myscript.ps1 将脚本中定义的函数加载到远程会话中
  • 使用另一个 Invoke-Command -Session $s -ScriptBlock {...} 并引用你的自定义函数 - 它们将在会话中存在

优点

以下是这种方法的优点:
  • 它很简单 - 您不需要了解模块的特殊性。只需编写普通的PowerShell脚本即可
  • 您不必将任何东西传递到远程计算机 - 这使得解决方案更加简单,维护时也更少出错

缺点

当然,它并不完美:

  • 对于解决方案的控制力较小:例如,如果您“导入”一组函数到会话中,则所有这些函数都会被“导入”并对用户可见,因此没有“封装”等。 我相信许多解决方案可以应对这个问题,所以不要仅根据这一点来决定
  • 每个文件中的功能都必须是自包含的 - 从那里进行的任何点源或模块导入都将搜索远程计算机,而不是本地计算机

最后,我应该说远程计算机仍然需要准备好远程操作。这就是我的意思:

执行策略应该被更改为某种形式,因为默认情况下受到限制:Set-ExecutionPolicy Unrestricted 应启用PowerShell远程操作: Enable-PSRemoting 应该将脚本运行的帐户添加到远程服务器的本地管理员中
如果您计划在远程会话中访问文件共享,请确保您了解多级身份验证并采取适当措施 确保您的杀毒软件是可靠的,不会将您发送到PowerShell hell

1
有人知道在过去的三年里是否有什么变化使这更容易了吗? - Justin Helgerson
看起来模块化方法的主要缺点是:它会打开一个冗余的潘多拉魔盒(每个目标上的模块/脚本版本不一致)。 - Hicsy
@JustinHelgerson 说句实话,四年后的今天,我添加了一个答案,展示了如何在PS 5.0中完成它,而不需要共享目录,在远程机器上预安装模块,或者动态重新创建模块。 - JonoB

7
这里还有另一种方法:在远程会话中重新创建模块,而不复制任何文件。
我没有试图解决模块之间的依赖关系,但这对于简单的自包含模块似乎可以正常工作。它依赖于本地会话中可用的模块,因为这使得确定导出项更容易,但是通过一些额外的工作,它也可以与模块文件一起使用。
function Import-ModuleRemotely([string] $moduleName,[System.Management.Automation.Runspaces.PSSession] $session)
{
    $localModule = get-module $moduleName;
    if (! $localModule) 
    { 
        write-warning "No local module by that name exists"; 
        return; 
    }
    function Exports([string] $paramName, $dictionary) 
    { 
        if ($dictionary.Keys.Count -gt 0)
        {
            $keys = $dictionary.Keys -join ",";
            return " -$paramName $keys"
        }
    }
    $fns = Exports "Function" $localModule.ExportedFunctions;
    $aliases = Exports "Alias" $localModule.ExportedAliases;
    $cmdlets = Exports "Cmdlet" $localModule.ExportedCmdlets;
    $vars = Exports "Variable" $localModule.ExportedVariables;
    $exports = "Export-ModuleMember $fns $aliases $cmdlets $vars;";

    $moduleString= @"
if (get-module $moduleName)
{
    remove-module $moduleName;
}
New-Module -name $moduleName {
$($localModule.Definition)
$exports;
}  | import-module
"@
    $script = [ScriptBlock]::Create($moduleString);
    invoke-command -session $session -scriptblock $script;
}

3

我不相信这个功能可以直接支持而不需要任何“黑客”手段。明智的做法可能是将模块放在公共位置,如文件服务器上,并在需要时在服务器上导入它。例如:

$session = new-pssession -computerName server01
invoke-command -session $session -scriptblock {
    #Set executionpolicy to bypass warnings IN THIS SESSION ONLY
    Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
    #Import module from public location
    Import-Module \\fileserver\folders\modulelocation...


    <# use function defined in MyCustomModule here #> 
}

1
这种方法的问题将是克服双跳问题。 - BenH

2

自从PS 5.0以来,我认为现在有另一种更干净的方法:

利用Copy-Item的ToSession参数将本地模块复制到远程计算机。

这不涉及先将模块复制到远程计算机的缺点:

  • 无需事先将模块复制到远程计算机
  • 没有共享文件夹或动态重新创建模块:

示例用法:

$s = New-PSSession MyTargetMachine
Get-Module MyLocalModule | Import-LocalModuleToRemoteSession -Session $s -Force
# Show module is loaded
Invoke-Command $s -ScriptBlock { Get-Module }

将本地模块导入远程会话的函数

请注意,它不会加载模块的依赖项。

<#
    .SYNOPSIS
        Imports a loaded local module into a remote session
        
    .DESCRIPTION 
        This script copies a module's files loaded on the local machine to a remote session's temporary folder and imports it, before removing the temporary files.
                
        It does not require any shared folders to be exposed as it uses the default Copy-To -ToSession paramter (added in PS 5.0). 
#>
function Import-LocalModuleToRemoteSession
{
    [CmdletBinding()]
    param(
        # Module to import
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName,Mandatory)]
        [System.Management.Automation.PSModuleInfo]$ModuleInfo,

        # PSSession to import module to
        [Parameter(Mandatory)]
        [System.Management.Automation.Runspaces.PSSession]
        $Session,

        # Override temporary folder location for module to be copied to on remote machine 
        [string]
        $SessionModuleFolder=$null,

        [switch]
        $Force,

        [switch]
        $SkipDeleteModuleAfterImport

    )

    begin{
        function New-TemporaryDirectory {
            $parent = [System.IO.Path]::GetTempPath()
            [string] $name = [System.Guid]::NewGuid()
            New-Item -ItemType Directory -Path (Join-Path $parent $name)
        }
    }

    process{
        
        if( [string]::IsNullOrWhiteSpace($SessionModuleFolder) ){
            Write-Verbose "Creating temporary module folder"
            $item = Invoke-Command -Session $Session -ScriptBlock ${function:New-TemporaryDirectory} -ErrorAction Stop
            $SessionModuleFolder = $item.FullName
            Write-Verbose "Created temporary folder $SessionModuleFolder"
        }

        $directory = (Join-Path -Path $SessionModuleFolder -ChildPath $ModuleInfo.Name)
        Write-Verbose "Copying module $($ModuleInfo.Name) to remote folder: $directory"
        Copy-Item `
            -ToSession $Session `
            -Recurse `
            -Path $ModuleInfo.ModuleBase `
            -Destination $directory
        
        Write-Verbose "Importing module on remote session @ $directory "

        try{
            Invoke-Command -Session $Session -ErrorAction Stop -ScriptBlock `
            { 
                Get-ChildItem (Join-Path -Path ${Using:directory} -ChildPath "*.psd1") `
                    | ForEach-Object{ 
                        Write-Debug "Importing module $_"
                        Import-Module -Name $_ #-Force:${Using:Force}
                    }
                
                    if( -not ${Using:SkipDeleteModuleAfterImport} ){
                        Write-Debug "Deleting temporary module files: $(${Using:directory})"
                        Remove-Item -Force -Recurse ${Using:directory}
                    }
            }
        }
        catch
        {
            Write-Error "Failed to import module on $Session with error: $_"
        }
    }
}

0

感谢这个帖子,它很有帮助...

但我实际上重写了这个函数。

请注意,无论是此帖子中的原始函数还是此重写函数都不包括模块清单数据。因此,您不能依赖于模块的版本检查。

function Import-ModuleRemotely {
    Param (
        [string] $moduleName,
        [System.Management.Automation.Runspaces.PSSession] $session
    )

    Import-Module $moduleName

    $Script = @"
    if (get-module $moduleName)
    {
        remove-module $moduleName;
    }

    New-Module -Name $moduleName { $($(Get-Module $moduleName).Definition) } | Import-Module
"@

    Invoke-Command -Session $Session -ScriptBlock {
        Param($Script)
        . ([ScriptBlock]::Create($Script))
        Get-Module 
    } -ArgumentList $Script
}

0

将自定义函数制作成脚本块,并使用 Invoke-Command 将其发送到目标服务器,这个怎么样?

Import-module YourModule
$s = [scriptblock]::Create($(get-item Function:\Your-ModuleFunction).Definition)

Invoke-Command -ScriptBlock $s -Computername s1,s2,sn

无法工作,因为模块可以引用自己的函数。 - majkinetor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接