Для чего это всё
Данный скрипт предназначен для массового апдейта Citrix VDA (в моем случае с версии 7.8 до 7.15) на виртуальных (Machine Creation Service) либо физических машинах.
Разработка и тестирование этого скрипта проводились в сети с жесткими политиками безопасности, где запрещены файловые шары, невозможно использование RPC (в том числе psexec.exe - на машинах выключена служба “Сервер”), невозможно использование удаленного powershell, запрещена даже установка пакетов MSU с помощью wusa.exe. Есть только учетные данные локального администратора. Все манипуляции на удаленных машинах производятся с помощью вызова методов WMI.
В процессе тестирования были выявлены проблемы, связанные с библиотеками С++ (ошибки 1603, 1157 и 1723). Решение этой проблемы - ТУТ.
Как работает
- Скрипт берет из указанного пула машины, версия VDA на которых не соответствует целевой.
- Затем обрабатывает каждую машину в отдельном job. Переводит машину в Maintenance Mode, включает ее, дожидается включения, затем проверяет версию установленного VDA средствами WMI (на случай, если нужная версия уже установлена, но машина не регистрируется и на DDC новая версия не видна).
- Затем скачивает с http-сервера VDA CleanUp Utility и запускает его. Выполняется удаление VDA (с нужным количеством перезагрузок).
- Затем c http-сервера скачивается дистрибутив VDA, распаковывается, устанавливаются prerequisites, устанавливается нужный апдейт(ы) (пакет MSU).
- Затем устанавливается VDA и проверяется его версия, а также возможность регистрации на DDC.
- Скрипт способен выполняться параллельно на заданном числе машин. Сведения о проделанной работе сваливаются в файлик.
#$ErrorActionPreference='SilentlyContinue'
$catalogName='Developers' # Citrix Machine Catalog name
$MachinesPerRound=15 # Number of simultaneously upgraded machines
$TargetAgentVersion='7.15.0.15097' # Verision of VDA, showed in Citrix Studio and Director
$Citrix_DDC='ddc.domain.local' # FQDN of Citrix Delivery Controller
$timeout=1500 # Timeout for jobs in seconds
Asnp Citrix*
$jobContent={
########################################### Job Block ############################################################
$update_server='http://updates.domain.local' # HTTP Resource from which updates to be downloaded
$VDA_sourcefile='VDAWorkstationSetup_7.15.exe' # VDA distr file name on update server
$VDA_destfile='c:\windows\temp\VDAWorkstationSetup_7.15.exe' # VDA distr file name on target machine
$VDA_CleanUp_source_file='VDACleanupUtility.exe' # VDA CleanUpUtility file name on update server
$VDA_CleanUp_dest_file='c:\windows\temp\VDACleanupUtility.exe' # VDA CleanUpUtility file name on target machine
$VDA_ExtractDir='c:\windows\temp\VDA_Install\' #
$path_to_VDASetup='\Extract\Image-full\x64\XenDesktop Setup\XenDesktopVdaSetup.exe' # Path to XenDesktopVDASetup.exe file
$setupargs='/quiet /components VDA /controllers "ddc.domain.local ddc2.domain.local" /remotepc /optimize /virtualmachine /enable_hdx_ports /enable_real_time_transport'
$ApplicationName = 'Citrix Virtual Desktop Agent - x64'
$TargetVersion = '7.15.0.82' ### Version of $ApplicationName (Citrix Virtual Desktop Agent - x64) component
$Citrix_DDC='ddc.domain.local'
$logfile='c:\temp\VDA_Update_Results.txt'
$username='.\Администратор'
$password='P@ssw0rd'
$password = ConvertTo-SecureString $password -asplaintext -force
$credentials = New-Object -Typename System.Management.Automation.PSCredential -argumentlist $username,$password
$MachineName=$args[0]
$Machine=Get-Brokermachine -MachineName $MachineName -AdminAddress $Citrix_DDC
$node=$Machine.DNSName
############################################### Functions ##################################################
function DownloadFile ($node, $credentials, $update_server, $source_file, $destination_file) {
$link=$update_server+'/'+$source_file
$posh_string = 'powershell -nop -exec bypass -c (New-Object Net.WebClient).DownloadFile(' + '''' + $link + '''' + ', ' + '''' + $destination_file + '''' + ')'
Write-Host "Starting download file" $link to $destination_file
$proc=Invoke-WmiMethod -Class win32_process -Name Create -Argumentlist $posh_string -Credential $credentials -Computername $node
$duration=Measure-Command {
do {
Start-Sleep -Seconds 1
}
until ((Get-WMIObject -Class Win32_process -Credential $credentials -Computername $node | where {$_.ProcessID -eq $proc.ProcessId}) -eq $null)
}
$query='Select * From CIM_datafile Where Name = ' + ''''+($destination_file -replace '\\', '\\')+''''
$file=Get-WmiObject -query $query -Credential $credentials -Computername $node
If ( $file -ne $null ) {
Write-Host "Download completed in" $([math]::Round($duration.TotalSeconds,1)) "Seconds"
return 'OK' }
else {
Write-Host "Download Failed"
return 'DWNLD_FAILED' }
}
function VDACleanUp ($node, $credentials, $cleanuputility) {
$runstring = 'cmd /c "'+ $cleanuputility + ' /unattended && mkdir c:\windows\temp\vdacleanup_ok || mkdir c:\windows\temp\reboot_needed"'
$removerebootflag = 'cmd /c "rmdir /Q /S c:\windows\temp\reboot_needed"'
$executable = Split-Path $cleanuputility -leaf
do {
if ((Get-WMIObject win32_bios -Credential $credentials -Computername $node) -ne $null) {
if ((Get-WmiObject -Class Win32_Directory -Credential $credentials -Computername $node -Filter "Name='c:\\windows\\temp\\vdacleanup_ok'") -eq $null) {
if ((Get-WmiObject -Class Win32_Directory -Credential $credentials -Computername $node -Filter "Name='c:\\windows\\temp\\reboot_needed'") -eq $null) {
if ((Get-WMIObject -Class Win32_process -Credential $credentials -Computername $node | where {$_.Name -eq $executable}) -eq $null) {
Write-Host -NoNewLine 'VDA CleanUp in progress...'
$proc=Invoke-WmiMethod -Class win32_process -Name Create -Argumentlist $runstring -Credential $credentials -Computername $node
Start-Sleep -Seconds 10
} else { (Write-Host -NoNewLine "."), (Start-Sleep -Seconds 3) }
} else {
Write-Host ""
Write-Host "Rebooting..."
Invoke-WmiMethod -Class win32_process -Name Create -Argumentlist $removerebootflag -Credential $credentials -Computername $node | Out-Null
Start-Sleep -Seconds 4
if ((Get-WmiObject -Class Win32_Directory -Credential $credentials -Computername $node -Filter "Name='c:\\windows\\temp\\reboot_needed'") -eq $null) {
$Win32OS=Get-WMIObject Win32_OperatingSystem -computername $node -Credential $credentials -EnableAllPrivileges
$Win32OS.win32shutdown(6) | Out-Null }
else { Write-Host "Cannot remove c:\windows\temp\reboot_needed..." }
Start-Sleep -Seconds 30
}
}
else {
Write-Host "CleanUp Completed"
$cleanup='OK'
Invoke-WmiMethod -Class win32_process -Name Create -Argumentlist 'cmd /c "rmdir /Q /S c:\windows\temp\vdacleanup_ok"' -Credential $credentials -Computername $node | Out-Null
$setup_string='cmd /c "del /F /Q /S '+$cleanuputility+'"'
Invoke-WmiMethod -class Win32_Process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
return $cleanup
}
}
else {Start-Sleep -Seconds 15}
}
while ($cleanup -ne 'OK')
}
Function ExtractVDA ($node, $credentials, $VDA_distr, $VDA_ExtractDir) {
$executable = Split-Path $VDA_distr -leaf
Write-Host -NoNewLine "Extracting VDA..."
$setup_string = 'cmd.exe /c "mkdir ' + $VDA_ExtractDir +'"'
Invoke-WmiMethod -Class win32_process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
$setup_string = $VDA_distr + ' /extract '+ $VDA_ExtractDir
Invoke-WmiMethod -Class win32_process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
Start-Sleep -Seconds 1
do {(Write-Host -NoNewLine ".."), (Start-Sleep -Seconds 1)}
while ((Get-WMIObject -Class Win32_process -Credential $credentials -Computername $node | where { $_.Name -eq $executable }) -ne $null)
Write-Host "VDA extraction completed!"
$setup_string='cmd /c "del /F /Q /S '+$VDA_distr+'"'
Invoke-WmiMethod -class Win32_Process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
return 'OK'
}
function InstallVDA ($node, $credentials, $VDA_ExtractDir, $path_to_VDASetup, $setupargs, $ApplicationName) {
$executable = Split-Path $path_to_VDASetup -leaf
$setup_string = $VDA_ExtractDir + '\' + $path_to_VDASetup + ' ' + $setupargs
$iteration=0
do {
Write-Host -NoNewLine "Setting up VDA... Running" $executable
Invoke-WmiMethod -class Win32_Process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
Start-Sleep -Seconds 2
do {(Write-Host -NoNewLine "."), (Start-Sleep -Seconds 1)}
while ((Get-WMIObject -Class Win32_process -Credential $credentials -Computername $node | where { $_.Name -eq $executable }) -ne $null)
$Win32OS=Get-WMIObject Win32_OperatingSystem -computername $node -Credential $credentials -EnableAllPrivileges
$Win32OS.win32shutdown(6) | Out-Null
Write-Host ""
Write-Host -NoNewLine "Rebooting..."
Start-Sleep 15
do { (Write-Host -NoNewLine "."), (Start-Sleep 15) }
While ((Get-WMIObject win32_bios -Credential $credentials -Computername $node) -eq $null)
$iteration++
$VDA_Present=Get-WMIObject -Class Win32_Product -Credential $credentials -Computername $node | Where-Object { $_.Name -like "*"+$ApplicationName+"*" }
}
While ( $VDA_Present -eq $null -and $iteration -lt 3 )
if ( $VDA_Present -ne $null ) { (Write-Host "VDA Update Completed !!!")
$status='OK' }
else { $status='VDA_Not_Installed' }
$setup_string='cmd /c "rmdir /Q /S '+$VDA_ExtractDir +'"'
Invoke-WmiMethod -class Win32_Process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
Return $status
}
function InstallPrerequisite ($node, $credentials, $VDA_ExtractDir, $path_to_setup) {
$executable = Split-Path $path_to_setup -leaf
$setup_string = $VDA_ExtractDir + $path_to_setup + ' /q /norestart'
Write-Host -NoNewLine 'Setting up ' $setup_string
Invoke-WmiMethod -class Win32_Process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
Start-Sleep -Seconds 2
do {(Write-Host -NoNewLine "."), (Start-Sleep -Seconds 1)}
while ((Get-WMIObject -Class Win32_process -Credential $credentials -Computername $node | where { $_.Name -eq $executable }) -ne $null)
Write-Host "Complete!"
}
function Install_MSU ($node, $credentials, $update_server, $MSU_KBName) {
$link=$update_server+'/'+$MSU_KBName
$destination_file='c:\windows\temp\'+$MSU_KBName
$posh_string = 'powershell -nop -exec bypass -c (New-Object Net.WebClient).DownloadFile(' + '''' + $link + '''' + ', ' + '''' + $destination_file + '''' + ')'
$proc=Invoke-WmiMethod -Class win32_process -Name Create -Argumentlist $posh_string -Credential $credentials -Computername $node
do {Start-Sleep -Seconds 1}
until ((Get-WMIObject -Class Win32_process -Credential $credentials -Computername $node | where {$_.ProcessID -eq $proc.ProcessId}) -eq $null)
$query='Select * From CIM_datafile Where Name = ' + ''''+($destination_file -replace '\\', '\\')+''''
$file=Get-WmiObject -query $query -Credential $credentials -Computername $node
if ( $file -ne $null ){
Write-Host -NoNewLine "Start installation of " $MSU_KBName "...."
$MSU_ExtractDir='c:\windows\temp\'+($destination_file -replace '\.[^.\\/]+$').split('\')[-1]
$setup_string = 'cmd.exe /c "mkdir ' + $MSU_ExtractDir +'"'
$proc=Invoke-WmiMethod -Class win32_process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
do {Start-Sleep -Seconds 1}
until ((Get-WMIObject -Class Win32_process -Credential $credentials -Computername $node | where {$_.ProcessID -eq $proc.ProcessId}) -eq $null)
$setup_string='wusa.exe '+$destination_file+' /extract:'+$MSU_ExtractDir
$proc=Invoke-WmiMethod -class Win32_Process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
do {Start-Sleep -Seconds 1}
until ((Get-WMIObject -Class Win32_process -Credential $credentials -Computername $node | where {$_.ProcessID -eq $proc.ProcessId}) -eq $null)
$cabname=($destination_file -replace '\.[^.\\/]+$').split('\')[-1] +'.cab'
$setup_string='cmd.exe /c "dism.exe /online /add-package /PackagePath:'+$MSU_ExtractDir+'\'+$cabname+'"'
$proc=Invoke-WmiMethod -class Win32_Process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
do {Start-Sleep -Seconds 1}
until ((Get-WMIObject -Class Win32_process -Credential $credentials -Computername $node | where {$_.ProcessID -eq $proc.ProcessId}) -eq $null)
Start-Sleep -Seconds 15
}
else { (Write-Host "Download " $MSU_KBName "failed"), ($status='DWNLD_FAILED') }
if ((Get-WMIObject -Class Win32_QuickFixEngineering -Credential $credentials -Computername $node | Where-Object { $_.HotFixID -like $cabname.split('-')[-2] }) -ne $null){(Write-Host 'Installation of ' $MSU_KBName 'completed!'), ($status='OK')}
else { (Write-Host 'Installation of ' $MSU_KBName 'NOT completed!'), ($status='INSTALLATION_NOT_COMPLETED') }
$setup_string='cmd /c "del /F /Q /S '+$destination_file+'"'
Invoke-WmiMethod -class Win32_Process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
$setup_string='cmd /c "rmdir /Q /S '+$MSU_ExtractDir+'"'
Invoke-WmiMethod -class Win32_Process -Name Create -Argumentlist $setup_string -Credential $credentials -Computername $node | Out-Null
return $status
}
function CheckVersion ($node, $credentials, $ApplicationName, $TargetVersion) {
$App = Get-WMIObject -Class Win32_Product -Credential $credentials -Computername $node | Where-Object { $_.Name -like "*"+$ApplicationName+"*" }
If (($App.Version).Equals($TargetVersion)) {
Write-Host 'Installed version of the' $ApplicationName 'match target version'
return 'OK' }
Else { Write-Host 'Installed version of the' $ApplicationName 'NOT match target version'
return 'VersionBAD'}
}
function CheckFreeSpace ($node, $credentials){
$Disk=get-WmiObject win32_logicaldisk -Credential $credentials -Computername $node | Where { $_.DeviceId -eq 'C:' }
If ($Disk.FreeSpace -gt 1080000000) { return 'OK'}
else { return 'LowDiskSpace'}
}
######################################## Script ########################################################
$duration=Measure-Command {
$Result=$node+' '
if ($args[0].SessionState -ne 'Active') {
$InitialMaintenanceMode=(Get-BrokerMachine -MachineName $MachineName).InMaintenanceMode
$InitialPowerState=(Get-BrokerMachine -MachineName $MachineName).PowerState
#Enable MaintenanceMode
Set-BrokerMachineMaintenanceMode -InputObject $MachineName $true
if ((Get-BrokerMachine -MachineName $MachineName | fl InMaintenanceMode) -ne $null) {
if ((Get-WMIObject win32_bios -Credential $credentials -Computername $node) -eq $null) {
#PowerOn Machine
New-BrokerHostingPowerAction -Action "TurnOn" -MachineName $MachineName
Write-Host -NoNewLine 'Starting Machine ' $node
do { (Start-Sleep 5), (Write-Host -NoNewLine '.') }
while ((Get-WMIObject win32_bios -Credential $credentials -Computername $node) -eq $null)
}
Start-Sleep 30
#CheckVersion
if ( (CheckVersion -node $node -credentials $credentials -ApplicationName $ApplicationName -TargetVersion $TargetVersion) -ne 'OK' ) {
#Check FreeSpace on disk
if ((CheckFreeSpace -node $node -credentials $credentials) -eq 'OK') {
if ((DownloadFile -node $node -credentials $credentials -update_server $update_server -source_file $VDA_CleanUp_source_file -destination_file $VDA_CleanUp_dest_file) -eq 'OK') {
if ((VDACleanUp -node $node -credentials $credentials -cleanuputility $VDA_CleanUp_dest_file) -eq 'OK') {
if ((DownloadFile -node $node -credentials $credentials -update_server $update_server -source_file $VDA_sourcefile -destination_file $VDA_destfile) -eq 'OK') {
ExtractVDA -node $node -credentials $credentials -VDA_distr $VDA_destfile -VDA_ExtractDir $VDA_ExtractDir
InstallPrerequisite -node $node -credentials $credentials -VDA_ExtractDir $VDA_ExtractDir -path_to_setup '\Extract\Image-Full\Support\VcRedist_2013_RTM\vcredist_x86.exe'
InstallPrerequisite -node $node -credentials $credentials -VDA_ExtractDir $VDA_ExtractDir -path_to_setup '\Extract\Image-Full\Support\VcRedist_2013_RTM\vcredist_x64.exe'
InstallPrerequisite -node $node -credentials $credentials -VDA_ExtractDir $VDA_ExtractDir -path_to_setup '\Extract\Image-Full\Support\VcRedist_2015\vc_redist.x86.exe'
InstallPrerequisite -node $node -credentials $credentials -VDA_ExtractDir $VDA_ExtractDir -path_to_setup '\Extract\Image-Full\Support\VcRedist_2015\vc_redist.x64.exe'
if ((Install_MSU -node $node -credentials $credentials -update_server $update_server -MSU_KBName 'Windows6.1-KB2999226-x64.msu') -eq 'OK') {
if ((InstallVDA -node $node -credentials $credentials -VDA_ExtractDir $VDA_ExtractDir -path_to_VDASetup $path_to_VDASetup -setupargs $setupargs -ApplicationName $ApplicationName) -eq 'OK') {
if ((CheckVersion -node $node -credentials $credentials -ApplicationName $ApplicationName -TargetVersion $TargetVersion) -eq 'OK'){ $result=$result+'VDA Installed. Version Checked.' }
else { $result=$result+'VDA Present, But Version Mismatch.'}
} else { $result=$result+'VDA NOT Installed.' }
} else { $result=$result+'MSU Install failed.' }
} else { $result=$result+'VDA Download Failed.' }
} else { $result=$result+'VDA CleanUp Failed.' }
} else { $result=$result+'VDA CleanUp Utility Download Failed.' }
} else { $result=$result+'Not enough disk free space.'}
} else { $result=$result+'No need to upgrade VDA.' }
} else { $result=$result+'Can not enable maintenance mode.' }
#Check if Machine registered successfully
if ((Get-BrokerMachine -MachineName $MachineName -RegistrationState 'Registered') -ne $null) {
$result=$result+' Machine Registered.'
#Revert to Initial MaintenanceMode
Set-BrokerMachineMaintenanceMode -InputObject $MachineName $InitialMaintenanceMode
}
else {
$result=$result+' Machine UnRegistered. Leave Machine in Maintenance Mode.'
New-BrokerHostingPowerAction -Action "ShutDown" -MachineName $MachineName
}
}
#Revert to Initial Power State
if ( $InitialPowerState -eq 'Off' ) { New-BrokerHostingPowerAction -Action "ShutDown" -MachineName $MachineName }
$result=$result+' Initial Maintenance Mode - '+ $InitialMaintenanceMode + "Job completed in" $([math]::Round($duration.TotalSeconds,1)) "Seconds."
$Result >> $logfile
}
}########################################## End of Job #######################################################
###################################################################################################################
$Filter='CatalogName -like "'+$catalogName+'" -and AgentVersion -ne "'+$TargetAgentVersion+'"'
Get-BrokerMachine -Filter $Filter -ReturnTotalRecordCount -AdminAddress $Citrix_DDC | out-null
$machinescount = $error[0].TotalAvailableResultCount
$rounds=[math]::floor($machinescount / $MachinesPerRound)
for ( $round=0; $round -le $rounds; $round++) {
Write-Host 'Round - ' $round
Get-Brokermachine -Filter $Filter -Skip ($round*$MachinesPerRound) -MaxRecordCount $MachinesPerRound -AdminAddress $Citrix_DDC | Select MachineName
Get-Brokermachine -Filter $Filter -Skip ($round*$MachinesPerRound) -MaxRecordCount $MachinesPerRound -AdminAddress $Citrix_DDC | ForEach-Object {
$jobname='VDA_Install_'+$_.HostedMachineName
if ((Get-Job -name $jobname | Where { $_.State -eq "Running" } ) -eq $null) {
Start-Job -name $jobname -initializationScript { Asnp Citrix* } -ArgumentList $_.MachineName -scriptblock $jobContent | out-null }
}
$timer=0
Do {
Start-Sleep 10
Write-Host -NoNewline '.'
$timer=$timer + 10
}
while ((Get-Job | Where { $_.State -eq "Running" }) -ne $null -and $timer -lt $timeout)
If ($timer -ge $timeout) {
Write-Host 'Timeout for Jobs:'
Get-Job | Where { $_.State -eq "Running" } | Select Name
Get-Job | Where { $_.State -eq "Running" } | Stop-Job
}
Write-Host ' '
}
Discussion