Table of Contents

Для чего это всё

Данный скрипт предназначен для массового апдейта Citrix VDA (в моем случае с версии 7.8 до 7.15) на виртуальных (Machine Creation Service) либо физических машинах.
Разработка и тестирование этого скрипта проводились в сети с жесткими политиками безопасности, где запрещены файловые шары, невозможно использование RPC (в том числе psexec.exe - на машинах выключена служба “Сервер”), невозможно использование удаленного powershell, запрещена даже установка пакетов MSU с помощью wusa.exe. Есть только учетные данные локального администратора. Все манипуляции на удаленных машинах производятся с помощью вызова методов WMI.
В процессе тестирования были выявлены проблемы, связанные с библиотеками С++ (ошибки 1603, 1157 и 1723). Решение этой проблемы - ТУТ.

Как работает

  1. Скрипт берет из указанного пула машины, версия VDA на которых не соответствует целевой.
  2. Затем обрабатывает каждую машину в отдельном job. Переводит машину в Maintenance Mode, включает ее, дожидается включения, затем проверяет версию установленного VDA средствами WMI (на случай, если нужная версия уже установлена, но машина не регистрируется и на DDC новая версия не видна).
  3. Затем скачивает с http-сервера VDA CleanUp Utility и запускает его. Выполняется удаление VDA (с нужным количеством перезагрузок).
  4. Затем c http-сервера скачивается дистрибутив VDA, распаковывается, устанавливаются prerequisites, устанавливается нужный апдейт(ы) (пакет MSU).
  5. Затем устанавливается VDA и проверяется его версия, а также возможность регистрации на DDC.
  6. Скрипт способен выполняться параллельно на заданном числе машин. Сведения о проделанной работе сваливаются в файлик.
#$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 ' '
}