Для чего это всё
Данный скрипт предназначен для массового апдейта 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