非持久虚拟设备基础设施 (NP-VDI)
NP-VDI 使 IT 团队能够快速配置虚拟桌面和物理设备,所有这些都来自单个统一平台。 内置自动化管理可节省时间、减少手动开销,并在云环境中获得全面的端点控制。
功能工作流程
基于基本映像的代理部署:
-
代理已安装在(未注册)洁净的 VM 上。
-
然后,使用提供的命令行工具将代理手动注册为“基本映像”。
密封基本映像:
-
一旦基本映像代理注册并验证了配置,VM 就会通过命令行“密封”。
生成实例:
-
可以从基本映像中拆分新的 NP-VDI 实例,通常为每个会话生成新的 MAC 地址。
-
Ivanti Neurons 检测并跟踪这些在基本映像下管理的唯一短期端点。
策略管理:
-
可以在基本图像级别进行策略更改。 所有生成的实例在下次签入时继承最新的所应用策略。
步骤 1:准备基本映像
要准备基本映像,请按照以下步骤操作:
- 导航至代理 > 代理部署 > 手动安装。
- 在手动安装选项卡上,选择代理策略和注册密钥。
- 选择 Windows。 然后,选择安装模式并单击下载。
代理安装程序将被下载。此操作将下载两个文件。 保存文件名为“IvantiCloudAgent.exe.options”的安装程序。 此文件将用于安装。
- 安装下载的 NP-VDI 代理。
要安装代理:- 在安装之前重命名关联的 IvantiCloudAgent.exe.options 文件。
- 运行安装程序。 出现激活密钥或 Cookie 选项时,单击取消。
这会在端点上安装代理,而不将其注册到租户。
- 将代理注册为基本映像。 要注册为基本映像,请以管理员身份从代理安装位置运行以下命令:
STAgentCtl.exe register --asBaseImage <Base Image Name> --instanceIdType MacAddress --enrollmentkey "%enrollmentkey%" --baseurl https://agentreg.ivanticloud.com
在上述命令中:
<Base Image Name> 是基本映像设备的名称。
%enrollmentkey% 是 tenantid_activationkey 的组合。
instanceIdType 可以是“MacAddress”、“HostName”或“自定义”。 这用作识别实例的唯一标记。
如果使用“自定义”,则可以指定包含逗号分隔的自定义标记列表的 tagList 选项来识别该代理。 有关自定义标记的更多信息,请参阅自定义标记列表和更新标记部分。
tenantid 和 activationkey 都可以在关联的 .options文件中找到,该文件与代理一起下载,如下例所示:
tenantid = 11111111-1111-1111-1111-4gc02d5b0f0d
activationkey = 9aI7SOQNybweiM2QKI7kcqIG8ufUFU21AvZwJwAEx6C-SAO5UGK62YkEgm89kvC2m-xwLesZA4t1_TBtEjIQMVaBU0KsVe-V60ZLBwWu83YREcUp3diAFvHjBvLtkxT3
cloudhost=https:agentreg.ivanticlouddev.com上述代码的注册密钥为:
%enrollmentkey% = 11111111-1111-1111-1111-4gc02d5b0f0d_9aI7SOQNybweiM2QKI7kcqIG8ufUFU21AvZwJwAEx6C-SAO5UGK62YkEgm89kvC2m-xwLesZA4t1_TBtEjIQMVaBU0KsVe-V60ZLBwWu83YREcUp3diAFvHjBvLtkxT3向名为“Win11”的基本映像的示例租户注册设备的命令行如下:
STAgentCtl.exe register --asBaseImage Win11 --instanceIdType MacAddress --enrollmentkey “11111111-1111-1111-1111-4gc02d5b0f0d_9aI7SOQNybweiM2QKI7kcqIG8ufUFU21AvZwJwAEx6C-SAO5UGK62YkEgm89kvC2m-xwLesZA4t1_TBtEjIQMVaBU0KsVe-V60ZLBwWu83YREcUp3diAFvHjBvLtkxT3" --baseurl https://agentreg.ivanticloud.com
- 要更新基本图像:
- 从 Ivanti 社区门户下载 PowerShell 脚本:Ivanti-Neurons -Non-Persistent-VDI - Agent Script。
在运行 PowerShell 脚本之前,请检查管理员是否具有在基本映像上运行该脚本的相关权限,因为端点上的执行策略可能会阻止该操作:
要进行检查,请使用“Get-ExecutionPolicy”命令。
要更改,请使用“Set-ExecutionPolicy RemoteSigned”命令。
- 如果脚本在没有任何参数的情况下运行,脚本将尝试以下操作:
- 解封代理。
- 运行代理和引擎的更新。
- 再次密封代理。
- 如果代理已解封,更新后应保持解封状态。 传递“DoNotSeal”参数以执行所有相同的步骤,除了密封映像。
- 更新的默认超时为 300 秒。 您可以通过传递“-TimeoutSec”参数来更改时间。
有关其他参考,可在附录部分访问脚本的副本。
- 要准备基本映像:
- 运行“STAgentCtl.exe prepareImage”命令,为 NP-VDI 准备密封、非持久的基本映像。
此命令是强制性的,会停止所有服务,并删除与基本映像的实际代理 ID 相关的任何数据。 当非持久实例启动时,服务将重新启动。代理 ID 将随从基本映像中启动的每个非持久实例而更改。
- 使用以下命令手动密封基本映像(升级脚本之外):
- 要手动密封基础图像,请输入“STAgentCtl.exe seal”。
使用“解封”命令解封映像。 在系统重新启动或使用“密封”命令之前,此操作将保持活动状态。
- 要手动密封基础图像,请输入“STAgentCtl.exe seal”。
- 运行“STAgentCtl.exe prepareImage”命令,为 NP-VDI 准备密封、非持久的基本映像。
- (可选)准备图像以与“sysprep”一起使用。
如果非持久桌面准备包括 sysprep 阶段,它将从证书存储中删除代理证书的私钥。 要解决此问题,请按照以下步骤操作:管理员必须提供自己的证书导出密码;否则,此过程将失败。
- 使用 certutil -store “Ivanti Cloud Agent Agent” 命令查找证书序列号。
这将列出代理证书存储中的证书。 - 在步骤 7 运行命令“stagentctl prepareImage”后,您可以使用以下命令导出代理证书:
certutil -exportPFX -privatekey -p "%pfxpass%" “Ivanti Cloud Agent Agent” %serialnumber% "%ProgramFiles%\Ivanti\Ivanti Cloud Agent\agent.pfx" NoChain,NoRoot
在上述命令中:
%pfxpass% 是管理员提供的密码。
%serialnumber% 是代理证书的序列号。
- 从 Ivanti 的社区网站下载 SetupComplete.cmd 脚本:从导出的 .pfx 文件脚本还原私钥。
- 编辑 SetupComplete.cmd 脚本并更新密码,以匹配用于导出步骤 2 中使用的证书的密码。
- 将脚本存储在黄金映像上的 %WINDIR%\Setup\Scripts\SetupComplete.cmd。
脚本将在 Windows 设置完成后自动运行(这是在 sysprep 后首次启动设备时发生)。这将从导出的 .pfx 文件中还原私钥,然后删除 .pfx 文件和脚本本身,这样密钥就不会泄露到非持久性实例中。
主题为 OU“ST Agent”的证书(例如 "Subject: OU=ST Agent, CN=011472EFB12BABF06122F2903BBE12AC4E32EBFE5082”) 应使用该条目中的相应序列号导出。
- 使用 certutil -store “Ivanti Cloud Agent Agent” 命令查找证书序列号。
步骤 2:创建和测试 NP-VDI 实例
要从基本映像中执行新实例,请按照以下步骤操作:
-
确保为每个新实例分配新的 MAC 地址(以确保 instanceIdType MacAddress 选项有效)。
或者,确保使用不同的主机名或标记。 -
启动虚拟机实例并确认实例作为唯一的 NP-VDI 端点显示在 Neurons UI 中。 转到 Ivanti Neurons 界面的代理 > 代理管理部分以执行此操作。
新的 VDI 类型列可用,指示设备是基本映像设备、非持久设备还是持久设备。 -
每个代理端点现在从代理管理页面链接到设备页面。 单击代理端点名称以启动在设备视图中显示设备的新窗口。
-
单击链接实例选项以显示从此基本映像中执行的所有实例。
在设备视图中,非持久实例将标记为非持久。
步骤 3:使用 NP-VDI 重新分配策略
要直接从 Ivanti Neurons 界面将策略重新分配给基本映像,请按照以下步骤操作:
-
导航到代理 > 代理策略。
-
选择基本图像,然后从操作菜单中选择重新分配策略。
-
选择要重新分配的策略,然后单击保存。
这将重新分配策略。 将策略重新分配给基本映像后,所有非持久 VDI 实例将在下次签入时继承策略(默认为每 200 分钟签入一次)。 -
使用 Ivanti Neurons 代理或代理管理界面,或使用命令行“Stagentctl status”从非持久性映像本身验证新实例在签入后继承更新的策略。
无法对 NP-VDI 实例执行重新启动操作。 这会将设备重置为其初始状态,可能导致用户失去在会话期间完成的工作。
此外,设备视图中可用于非持久实例的操作不包含重新启动操作。
其他 STAgentCtle.exe 命令
您可以使用以下附加命令:
-
要打开基础代理进行更新,请输入“Stagentctl.exe unseal”命令。
-
要强制命令行等到更新完成后再返回控件,请输入“Stagentctl.exe update -wait”命令。
“等待”命令可能会在代理更新时遇到问题,因为它需要代理可执行文件运行,这导致更新后需要重新启动。 为了避免此问题,Ivanti 提供了 PowerShell 脚本,允许执行同步更新命令。
-
要关闭更新的基本代理,请输入“Stagentclt.exe seal”命令。
自定义标签列表
自定义标签列表是用户定义的标签集合,可以分配给 Ivanti Neurons 中的端点,包括 NP-VDI 实例。 自定义标签有助于根据业务、操作或技术要求对设备进行分类、组织和识别。
例如,使用标记“Sales-[MacAddress]”,通过用设备的实际 MAC 地址替换 [MacAddress],为每个设备生成一个唯一的标记。 此功能对于为非持久 VDI 实例生成唯一标签特别有用。
使用以下语法引用设备属性和环境变量:
-
要引用 MAC 地址,请使用“[MacAddress]”
-
要引用主机名,请使用“[HostName]”
-
要引用环境变量,请使用“[env:<name>]”
其中 <name> 是环境变量的区分大小写名称。
更新标记
在基本映像注册期间分配的标记对于所有相关实例保持静态。 如果需要更新标签,请在 VM 部署后重新注册/密封基本映像,或使用自动化根据当前标准指派标签。
附录
.SYNOPSIS
更新非持久代理,等待更新完成,然后可以选择密封代理。
.PARAMETER TimeoutSec
总超时秒数。
.PARAMETER UpgradeCode
正在更新的代理的升级代码。
.PARAMETER DoNotSeal
此参数指定更新后不密封代理。
#>
参数
(
[int] $TimeoutSec = 300,
[guid] $UpgradeCode = "{0C5D7E9C-0424-462D-A570-2A7469EE3739}", # Neurons Agent
[switch] $DoNotSeal
)
函数 Check-UpdatersComplete
{
$updaters = Get-Process -Name "STAgentUpdater" -ErrorAction SilentlyContinue
return ($updaters.Count -eq 0)
}
函数 Wait-UpdatersComplete([DateTime] $timeoutDateTime, [int] $checkIntervalSec)
{
while ((Get-Date) -lt $timeoutDateTime)
{
if (Check-UpdatersComplete)
{
返回
}
Start-Sleep -Seconds $checkIntervalSec
}
引发“等待所有更新程序完成超时。”
}
函数 Get-AgentVersionFromManifest([string] $manifestPath, [string] $platformKey)
{
if (-not (Test-Path $manifestPath))
{
引发“无法在 '$manifestPath' 中找到清单。”
}
[xml]$manifestXml = Get-Content $manifestPath
$agentFile = $manifestXml.manifest.files.file | Where-Object { $_.key -eq $platformKey }
if (-not ($agentFile))
{
引发“未找到具有 key='$platformKey' 的代理条目。”
}
if (-not ($agentFile.version))
{
引发“无法在清单中找到代理版本。”
}
return [version]::Parse($agentFile.version)
}
函数 Get-CurrentAgentVersion([guid] $agentUpgradeCode)
{
$agentProductCode = Get-ProductCode -UpgradeCode $agentUpgradeCode
$agentObject = Get-WmiObject -Class Win32_Product | Where-Object { [guid]::Parse($_.IdentifyingNumber) -eq $agentProductCode }
if (-not ($agentObject))
{
引发“未找到已安装的代理”。
}
if (-not ($agentObject.Version))
{
引发“未找到已安装的代理版本。”
}
return [version]::Parse($agentObject.Version)
}
函数 Check-AgentUpToDate([string] $manifestPath, [guid] $agentUpgradeCode, [string] $platformKey)
{
$manifestVersion = Get-AgentVersionFromManifest -ManifestPath $manifestPath -PlatformKey $platformKey
$currentVersion = Get-CurrentAgentVersion -AgentUpgradeCode $agentUpgradeCode
return ($currentVersion -ge $manifestVersion)
}
函数 Wait-AgentUpdated([DateTime] $timeoutDateTime, [int] $checkIntervalSec, [string] $manifestPath, [guid] $agentUpgradeCode, [string] $platformKey)
{
while ((Get-Date) -lt $timeoutDateTime)
{
if (Check-AgentUpToDate -ManifestPath $manifestPath -AgentUpgradeCode $agentUpgradeCode -PlatformKey $platformKey)
{
返回
}
Start-Sleep -Seconds $checkIntervalSec
}
引发“等待代理更新超时。”
}
函数 Update-Agent([DateTime] $timeoutDateTime, [string] $manifestPath, [guid] $agentUpgradeCode, [string] $agentCtlPath, [string] $platformKey)
{
$timeoutSec = [math]::Ceiling(($timeoutDateTime - (Get-Date)).TotalSeconds)
if ($timeoutSec -le 0)
{
引发“由于超时较小,无法运行签入。”
}
& $agentCtlPath update --checkin --wait $timeoutSec
if (-not $?)
{
引发“签入操作失败”。
}
& $agentCtlPath update --all
if (-not $?)
{
引发“更新操作失败”。
}
Wait-AgentUpdated -TimeoutDateTime $timeoutDateTime -CheckIntervalSec 1 -ManifestPath $manifestPath -AgentUpgradeCode $agentUpgradeCode -PlatformKey $platformKey
Wait-UpdatersComplete -TimeoutDateTime $timeoutDateTime -CheckIntervalSec 1
$timeoutSec = [math]::Ceiling(($timeoutDateTime - (Get-Date)).TotalSeconds)
if ($timeoutSec -le 0)
{
引发“由于超时较小,无法运行最终更新。”
}
& $agentCtlPath update --all --wait $timeoutSec
if (-not $?)
{
引发“最终更新操作失败”。
}
}
函数 Get-ProductCode([guid] $upgradeCode)
{
$upgradeCodeStr = $upgradeCode.ToString("B") # Format as {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
$wmiFilter = "Property='UpgradeCode' AND Value='$upgradeCodeStr'"
$productCode = @(Get-WmiObject -Class Win32_Property -Filter $wmiFilter).ProductCode
if ($productCode.Count -eq 0)
{
引发“未找到升级代码 '$upgradeCode'。”
}
if ($productCode.Count -gt 1)
{
引发“找到多个具有相同升级代码 '$upgradeCode' 的产品。”
}
return [guid]::Parse($productCode)
}
函数 Get-AgentEnvironment([string] $installDirPath)
{
$configPath = Join-Path $installDirPath "AgentEnvironment.config"
if (-Not (Test-Path $configPath))
{
引发“未在 $configPath 中找到配置文件”。
}
[xml]$agentEnvironment = Get-Content $configPath
return $agentEnvironment
}
函数 Get-InstallLocation([guid] $productCode)
{
$productObject = Get-WmiObject -Class Win32_Product | Where-Object { [guid]::Parse($_.IdentifyingNumber) -eq $productCode }
if ($null -eq $productObject)
{
引发“未找到产品 GUID。”
}
return $productObject.InstallLocation
}
$timeoutDateTime = (Get-Date).AddSeconds($TimeoutSec)
写入主机“正在收集信息...”
尝试
{
$agentProductCode = Get-ProductCode -UpgradeCode $UpgradeCode
$installDirPath = Get-InstallLocation -ProductCode $agentProductCode
写入主机“安装目录:$installDirPath”
$agentEnvironment = Get-AgentEnvironment -InstallDirPath $installDirPath
$dataDirPath = $agentEnvironment.agentEnvironment.agentDataDirectory
写入主机“数据目录:$dataDirPath”
$regPath = Join-Path "HKLM:" $agentEnvironment.agentEnvironment.registryPath
写入主机“注册表项:$regPath”
$platformKey = Get-ItemProperty -Path $regPath -Name "PlatformManifestKey" -ErrorAction Stop | Select-Object -ExpandProperty "PlatformManifestKey" -ErrorAction Stop
写入主机“平台密钥:$platformKey”
$agentCtlPath = Join-Path $installDirPath "STAgentCtl.exe"
$manifestPath = Join-Path $dataDirPath "manifest.xml"
}
捕获
{
写入错误“错误:$_”
exit -1
}
if ((Get-Date) -gt $timeoutDateTime)
{
写入错误“错误:代理更新开始之前超时。”
exit -1
}
写入主机“正在解封代理...”
& $agentCtlPath unseal
if (-not $?)
{
写入错误“错误:无法执行解封操作”。
exit -1
}
写入主机“正在更新代理...”
$result = -1
尝试
{
Update-Agent -TimeoutDateTime $timeoutDateTime -ManifestPath $manifestPath -AgentUpgradeCode $UpgradeCode -AgentCtlPath $agentCtlPath -PlatformKey $platformKey
$result = 0
}
捕获
{
写入错误“错误:$_”
}
if ($DoNotSeal)
{
if ($result -eq 0)
{
写入主机“代理已成功更新”。
}
其他
{
写入错误“错误:代理更新失败”。
}
exit $result
}
写入主机“密封代理...”
& $agentCtlPath seal
if (-not $?)
{
写入错误“错误:无法密封代理!”
exit -1
}
if ($result -eq 0)
{
写入主机“代理已成功更新和密封”。
}
exit $result
(@ECHO OFF
set pw=supersecret
set logfile=C:\ProgramData\Ivanti\Ivanti Cloud Agent\Logs\agentkeyrestore.log
set certFile=C:\Program Files\Ivanti\Ivanti Cloud Agent\agent.pfx
set scriptPath=%0%
ECHO %date% %time% 代理密钥还原已启动 >>“%logfile%”
IF EXIST "%certFile%" (
certutil -f -p "%pw%" -privatekey -importPFX "Ivanti Cloud Agent Agent" "%certFile%" NoCert,NoExport >> "%logfile%" 2>&1
IF %ERRORLEVEL% NEQ 0 (
ECHO 无法导入代理密钥 >>“%logfile%”
)
ECHO 删除代理密钥文件 >>“%logfile%”
del "%certFile%" >> "%logfile%" 2>&1
IF %ERRORLEVEL% NEQ 0 (
ECHO 无法删除代理密钥文件 >>“%logfile%”
)
ECHO %date% %time% 代理密钥恢复完成 >>“%logfile%”
) ELSE (
ECHO %date% %time% 代理密钥恢复失败:未找到代理密钥文件 >>“%logfile%”
)
ECHO 删除脚本 >>“%logfile%”
del "%scriptPath%" >> "%logfile%" 2>&1
“REM 此时无法执行其他命令,因为脚本文件已删除”