Автоматизация установки обновлений на клиентскую машину с отсевом ошибочных обновлений

    Раз в год выходит обновление ломающее привычную работу. Windows пытается поставить его 3 раза, и если 3 раза был откат то она загружается без установки. Пользователи с утра начинают звонить. Если ничего не предпринять то на следующее утро ситуация повторится.
    • На WSUS сервере нет смысла перемещать его в Unapproved т.к. патч уже скачан на клиенскую машину и находится в папке SoftwareDistribution, надо удалять вручную
    • глючных патчей может быть много
    • через пол годика админ может по запарке его снова одобрить
    • если одобряем раз в полгода-год, отсев плохих от хороших займет до 1 дня работы на 1 машину (класс машин)
    • обновления одной конфигурации могут не подходить для другой конфигурации даже если ОС одна
    • состояние обновления скрытое будет сброшено если удалить SoftwareDistribution
    • SoftwareDistribution надо скидывать если глюкнула база, слишком долго идет поиск, а лучше делать это периодически

    Это все неудобно, поэтому админ почти всегда сделает одно и то же — он отключит обновления на проблемной машине. А через годик или два он начнет накатывать обновления и столконется с этой же проблемой.

    Т.к. глюк обновлений вызывает аварийный режим, жрет неимоверное количество времени персонала на абсолютно бессмысленную фигню, и с целью преехода к умной инфраструктуре было решено попробовать автоматизировать этот процесс. Попутно был получен скрипт который может установить все обновления на свежеустановленую машину в автоматическом режиме.

    Сразу оговорюсь — данный скрипт не умеет ремонтировать систему которая не загружается вообще, он умеет сам ставить обновления, в автоматическом режиме выкидывать обновы вызывающих циклическую перезагрузку, имеет возможность вручную добавить плохие обноления в список и сигнализировать админу о возникших неполадках с установкой.


    Как это все запустить, быстрый старт


    исходники к статье одним файлом
    0. Разрешите исполнение скриптов в вашей системе. Set-ExecitionPolicy Unrestricted или подпишите скрипты вашим сертификатом.
    1. скопируйте скрипт WUErrorreporting на диск контроллируемой машины и добавьте его в планировщик задач например 12:30 дня. Если кто не знает как создавать задания по расписанию жмите сюда
    WUErrorreporting.ps1
    <#
    2016.05.23
    Windows Update Error reporter
        
    симафорит о возных проблемах на клиенте. Осматривает журнал установки обновлений и в случае если
    есть событие 20 формирует административное письмо.
    
    Pak N.V.     
    #>
    
    #############
    # Variables #
    #############
    
    # количество дней за который будет просматриваться журнал
    [int]$DaysBefore = 5
    
    # сохранять ли список не установившихся обновлений и куда
    $SaveLog = $false
    $LogPath = 'c:\WUError.txt'
    
    # сохранять отчет HTML
    [boolean]$SaveReport = $False
    [string]$ReportPath = 'C:\WUErrorReport.html'
    
    # отправлять HTML отчет
    [boolean]$SendReport = $true
    [string]$ReportMail1 = 'admin@test.local'
    [string]$from = 'bot_abormot@test.local'
    [string]$SMTPServer = 'mail.test.local'
    
    #############################################################################
    
    # получаем события за последние сутки
    $Events = Get-EventLog -LogName System -EntryType Error -After (Get-Date).AddDays(-$DaysBefore) -InstanceId 20 -ErrorAction SilentlyContinue
    
    # если были обшики установки то продолажем обработку иначе выход
    if ($Events.Count -eq 0)
    {
        Write-Host 'ошибок установки обновлений нет, завершаем сценарий' -ForegroundColor Green
        Exit
    }
    
    # контейнер для событий
    $Log = @()
    
    # переберием события и вытаскиваем номера обновлений с ошибками установки
    foreach ($Event in $Events)
    {
        $regex = $Event.Message -match "KB\d+"
        $KB = $matches[0]
        
        $params = [ordered]@{ 'KB'=$KB
                              #'EntryType'=$Event.EntryType
                              'Index'=$Event.Index
                              'MachineName'=$Event.MachineName
                              'Message'=$Event.Message
                              'Source'=$Event.Source
                              'TimeGenerated'=$Event.TimeGenerated
                              'TimeWritten'=$Event.TimeWritten
                              'UserName'=$Event.UserName
                            }
    
        $obj = New-Object -TypeName PSObject -Property $params
    
        $Log += $obj
    }
    
    if ($SaveLog -eq $true)
    {
        # сохраняем список обновлений с ошибками
        Add-Content -Path $LogPath -Value ''
        $Log | select -ExpandProperty KB | Add-Content -Path $LogPath
    }
    
    
    #отработали, ниже отправка письма
    ################################# красивый отчет #####################################
    Write-Verbose 'HTML fragment producing'
    
    $ClientName = $env:COMPUTERNAME
    $TotalErrors = $log.Count
    $frag1 = $Log | ConvertTo-Html -As table -Fragment -PreContent "<h2>Windows Update error report. $ClientName </h2><br><h3>Total $TotalErrors errors.</h3>" | Out-String
    
    Write-Verbose 'definiting CSS'
    $head = @'
    <style>
    body { background-color:#ffffff;
               font-family:Tahoma;
    	   font-size:12pt; }
    td, th { border:1px solid black;
               border-collapse:collapse; }
    th { color:white;
               background-color:black; }
               table, tr, td, th { padding: 2px; margin: 0px }
    table {
    font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
    font-size: 14px;
    border-radius: 10px;
    border-spacing: 0;
    text-align: center;
    }
    th {
    background: #BCEBDD;
    color: white;
    text-shadow: 0 1px 1px #2D2020;
    padding: 10px 20px;
    }
    th, td {
    border-style: solid;
    border-width: 0 1px 1px 0;
    border-color: white;
    }
    th:first-child, td:first-child {
    text-align: left;
    }
    th:first-child {
    border-top-left-radius: 10px;
    }
    th:last-child {
    border-top-right-radius: 10px;
    border-right: none;
    }
    td {
    padding: 10px 20px;
    background: #F8E391;
    }
    tr:last-child td:first-child {
    border-radius: 0 0 0 10px;
    }
    tr:last-child td:last-child {
    border-radius: 0 0 10px 0;
    }
    tr td:last-child {
    border-right: none;
    }
    </style>
    '@
    
    $Date = Get-Date
    
    if ($SendReport -eq $true)
    {
        Write-Verbose 'SendEmail'
    
        $encoding = [System.Text.Encoding]::UTF8
        $body = ConvertTo-HTML -head $head -PostContent $frag1 -PreContent "<h1>Windows Update error report. Client $ClientName. Date:$Date</h1>" | Out-String
        
        $params = @{'To'=$ReportMail1
                   'From'=$from
                   'Subject'="$ClientName. Windows Update error $Date"
                   'Body'=$Body
                   'BodyAsHTML'=$True
                   'SMTPServer'=$SMTPServer}
    
        Send-MailMessage @params -Encoding $encoding
    }
    

    2. Выставье параметры оповещения о ошибках обновлений, для этого измените параметры
    $ReportMail1 = 'admin@test.local' на вашу почту
    $SMTPServer = 'mail.test.local' на ваш почтовый сервер.

    3. скопировать скрипт сброса конфигурации на нужную машину и запустить его. Он создаст каталог с начальными настройками, если каталог есть сбросит их в начальное состояние.
    Rearm_Install-ClientUpdate.ps1
    <#
    сбрасывает переменные автоматов для первого использования
    #>
    
    # путь до директории где лежат рабочие файлы модуля
    $Path = Split-Path ($MyInvocation.MyCommand.Path) -Parent
    
    if ((Test-Path -Path "$Path\BadUpdates") -eq $false)
    {
        New-Item -Path "$Path\BadUpdates" -ItemType Directory -Force
    }
    
    # последние установленные обновления
    $LastInstalledUpdatesFile = "$Path\BadUpdates\LastInstalledUpdates.txt"
    # список точно плохих обновлений
    $BadUpdatesFile = "$Path\BadUpdates\BadUpdates.txt"
    # FastInstaller
    $KAFUFile = "$Path\BadUpdates\KAFU.txt"
    # Скорость установки обновлений за раз
    $KAFUDeltaFile = "$Path\BadUpdates\KAFUDelta.txt"
    # Fastinstaller watchdog
    $KAFUWatchDogFile = "$Path\BadUpdates\KAFUWatchDog.txt"
    # Fastinstaller update installer watchdog
    $KAFUWatchDog2File = "$Path\BadUpdates\KAFUWatchDog2.txt"
    # SlowInstaller
    $KASUFile = "$Path\BadUpdates\KASU.txt"
    # файл лог
    $WorkLogFile = "$Path\BadUpdates\log.txt"
    # общий автомат
    $KAFile = "$Path\BadUpdates\KA.txt"
    
    
    Set-Content $LastInstalledUpdatesFile -Value ''
    Set-Content $KAFUFile -Value 0
    Set-Content $KAFUDeltaFile -Value 5
    Set-Content $KAFUWatchDogFile -Value 0
    Set-Content $KAFUWatchDog2File -Value 0
    Set-Content $KASUFile -Value 0
    Set-Content $KAFile -Value 99
    Set-Content $BadUpdatesFile -Value ''
    


    4. в каталог с предыдущим скриптом скопировать скрипт обновлений (внимание, ниже большой скрипт, 85кб), чтобы не копипастить скачайте его отсюда

    Install-ClientUpdate.ps1
    <#
    2016.06.14
    ver 2
    
    устанавливает обновления на клиентских машинах пытаясь определить обновления вызывающие
    тормоза при загрузке. Делалось для снижения стресса на отдел системных администраторов.
    
    может
        устнававливать быстро много обновлений пачками (FastUpdate) при быстром проходе ставит сначала по 30%
            за раз, если не получилось то по 25%, если не получилось то по 5 за раз.
        ставить по одному обновленияю, и после прохода по всем те что не встали добавит в список плохих,
            если добавит в список плохих то при следующей работе эти обновления будут игнорироваться не зависимо
            от того одобрены они на вышестоящем сервере или нет
        сбрасывает настройки агента всус на машине на каждом запуске, позволяет сбросить глюки, или кривые апдейты
    
    для использования как робота обновлений
    1. выберите каталог куда запишете сценарий
    2. запишите в файл "\BadUpdates\KA.txt" число больше 9, например 11
    3. создайте в планировщике заданий от админа задание с правами система
        триггер - запуск системы, подождать 6 часов
        действие - запуск программы powershell.exe -file "ваш путь до сценария\Install-ClientUpdate.ps1"
    4. разрешите выполнение сценариев в вашей среде
    5. пропишите в сценарии ваши параметры
        установите $RebootEnabled = $False
        на какие адреса посылать оповещение о плохих обновлениях и нужно ли вообще отправлять
    
    
    для автоматической установки большого количества обновлений
    1. выберите каталог куда запишете сценарий
    2. запишите в файл "\BadUpdates\KA.txt" число больше 9, например 11
    3. создайте в планировщике заданий от админа задание с правами система
        триггер - при запуск (при включении компьютера), отложить на 10 минут
        действие - запуск программы powershell.exe -file "ваш путь до сценария\Install-ClientUpdate.ps1"
    4. разрешите выполнение сценариев в вашей среде
    5. пропишите в сценарии ваши параметры
        установите $RebootEnabled = $true
        на какие адреса посылать оповещение о плохих обновлениях и нужно ли вообще отправлять
    
    
    глюки:
        в некоторых случая не удаляется папка SoftwareDistribution. происходит это из за того что какойнибудь процесс держит папку
            в основном он создает каталог в подпапке Plugins. в таком случае нужно перезагрузится и один раз очистить папку вручную.
            в коде сделана поправка чтобы сценарий не останавливался в таком случае, просто очистит ее по максимум и перезапустит службы
            без завершения.
        Юзер иногда жмакает при завершении работы на "Установить обновления и завершить работу" что вызывает установку вообще всех
            обновлений. Нужно объяснить что достаоточно только завершить работу и показать как. Иначе нужно дописать часть
            устанавливающую IsHidden=1 для всех обновлений кроме тех что выбраны на установку в этой сессии.
    
    	возможно зависание поиска обновлений если стоит настройка "4. автоматически устанавливать обновления и перезагружать компьютер".
    		нужно поставить другую настройку.
        возможен глюк на win 7 если в качестве источника обновлений стоит мир. Скрипт никогда не может закончить получение обновлений
            стабильно работает с локального всус
    
    
    требует:
        powershell 2.0
    
    Pak Nikolay
    GEOM, Aqtobe
    
    проверен на win 8.1/2012R2; win 7; win 2008R2
    #>
    
    
    #############
    # Variables #
    #############
    
    # Allow reboot. установка этого флага вызывает перезагрузку после каждого срабатывания. нужно если вам надо поставить
    # много обновлений на машину. установите в планировщике время после запуска 10 минут, и разрешите ребуты. сценарий будет сам
    # устанавливать обновления для вас.
    #$RebootEnabled = $true
    $RebootEnabled = $False
    # Fast install если больше этого порога то переходим к быстрой установке
    $FastInstallLimit = 45
    
    
    
    
    # путь до директории где лежат рабочие файлы модуля
    $Path = Split-Path ($MyInvocation.MyCommand.Path) -Parent
    # последние установленные обновления
    $LastInstalledUpdatesFile = "$Path\BadUpdates\LastInstalledUpdates.txt"
    # список точно плохих обновлений
    $BadUpdatesFile = "$Path\BadUpdates\BadUpdates.txt"
    # FastInstaller
    $KAFUFile = "$Path\BadUpdates\KAFU.txt"
    # Скорость установки обновлений за раз
    $KAFUDeltaFile = "$Path\BadUpdates\KAFUDelta.txt"
    # Fastinstaller watchdog
    $KAFUWatchDogFile = "$Path\BadUpdates\KAFUWatchDog.txt"
    # Fastinstaller update installer watchdog
    $KAFUWatchDog2File = "$Path\BadUpdates\KAFUWatchDog2.txt"
    # SlowInstaller
    $KASUFile = "$Path\BadUpdates\KASU.txt"
    # файл лог
    $WorkLogFile = "$Path\BadUpdates\log.txt"
    # общий автомат
    $KAFile = "$Path\BadUpdates\KA.txt"
    
    
    
    ########################## Report params ###################################
    [boolean]$SaveReport = $true
    [string]$ReportPath = 'C:\Report-InstallUpdates.html'
    [boolean]$SendReport = $true
    [string]$From = 'bot_abormot@test.local'
    [string]$SMTP = 'mail.test.local'
    [string]$ReportMail1 = 'admin1@test.local'
    [string]$ReportMail2 = 'admin2@test.local'
    [string]$ReportMail3 = 'admin3@test.local'
    
    function report
    {
        Param ( [string]$Text = 'test' )
    
        $Date = Get-Date
        if ($SaveReport = $true)
        {
            $Text | Out-File $ReportPath
        }
    
        if ($SendReport = $true)
        {
            $encoding = [System.Text.Encoding]::UTF8
            $body = $Text
            $Subj = "install-updates script report $Date"
        
            $params = @{'To'=$ReportMail1
                       'From'=$From
                       'Subject'=$Subj
                       'Body'=$Body
                       'BodyAsHTML'=$True
                       'SMTPServer'=$SMTP}
    
        Send-MailMessage @params -Encoding $encoding
    
            $params = @{'To'=$ReportMail2
                   'From'=$From
                    'Subject'=$Subj
                   'Body'=$Body
                   'BodyAsHTML'=$True
                   'SMTPServer'=$SMTP}
    
        Send-MailMessage @params -Encoding $encoding
    
            $params = @{'To'=$ReportMail3
                   'From'=$From
                    'Subject'=$Subj
                   'Body'=$Body
                   'BodyAsHTML'=$True
                   'SMTPServer'=$SMTP}
    
        Send-MailMessage @params -Encoding $encoding
        }
    }
    ########################## Report params ###################################
    
    function Log
    {
    	PARAM ( [parameter(Mandatory = $true)]
    		    [string]$Message
    	      )
    
    	$Date = Get-Date -Format "yyyy.MM.dd HH:mm:ss"
    	[string]$Msg = $Date + "`t" + $Message
    	Out-File -FilePath $WorkLogFile -InputObject $Msg -Append # -encoding unicode
    }
    
    function Clear-Log
    {
    	$Date = Get-Date -Format "yyyy.MM.dd HH:mm:ss"
    	$Msg = $Date + "`t" + 'лог очищен вызовом Clear-Log'
        Set-Content -Path $WorkLogFile -Value $Msg
    }
    
    function Trim-Log
    {
        # если лог больше 5 мегабайт кильнуть лог
        if ( (Get-Item -Path $WorkLogFile).Length -gt 5mb )
        {
            Clear-Log
        }
    }
    
    <#
    сбрасываем папку SoftwareDistribution
    часто служба обновлений не может обновится, выдает ошибку, или криво работает
    помогает сброс службы WUAUSRV в частности стереть папку с ее настройками.
    
    при старте она ее создаст снова
    #>
    function ResetSoftwareDistribution
    {
        $WUServices = 'BITS','wuauserv'
        $windows = 0x24
    
        Log 'call ResetSoftwareDistribution'
        Log 'stopping WU services'
        Write-Host "сбрасываем SoftwareDistribution" -ForegroundColor Green
        Write-Host "-------------------------------" -ForegroundColor Green
        Write-Host "останавливаем сервисы" -ForegroundColor Green
        $WUServices | Stop-Service -Verbose
        Start-Sleep -Seconds 20
        
        # если службы остановились то продолжаем
        if ( ((Get-Service 'BITS').Status -eq 'stopped' ) -and ((Get-Service 'wuauserv').Status -eq 'stopped') )
        {
            Log 'services stopped sucsessful'
            Write-Host 'службы остановлены продолжаем' -ForegroundColor Green
            $Folders = Get-ChildItem ( (New-Object -ComObject Shell.Application).Namespace( $windows ).Self.Path) -Directory | where { $_.Name -like '*SoftwareDistribution*' }
            Write-Host "получены папки службы WU" -ForegroundColor Green
            $Folders
            Write-Host "удаляем ..."
            $Folders | Remove-Item -Recurse -Force -Verbose -Confirm:$false
    
            # проверка удаления
            $Folders = Get-ChildItem ( (New-Object -ComObject Shell.Application).Namespace( $windows ).Self.Path) -Directory | where { $_.Name -like '*SoftwareDistribution*' }
            if ($Folders -eq $null)
            {
                Log 'SoftwareDistribution folder deleted successful'
                Write-Host "удаление SoftwareDistribution прошло успешно" -ForegroundColor Green
            }
            else
            {
                Log 'SoftwareDistribution folder deleted NOT successful'
                Write-Host "SoftwareDistribution не была удалена!" -ForegroundColor Red
                # если вы раскомментируете оператор ниже то возможен завис если какаянибудь фигня блокирует удаление Softwaredistrib
                #Exit
            }
        }
        Log 'starting WU services'
        Write-Host "запускаем сервисы" -ForegroundColor Green
        $WUServices | Start-Service -Verbose
    }
    
    <#
    скачивает и устанавливает указанные обновления
    обновления на установку передаются списоком номеров KB
    ищет только обновления имеющие флаг IsHidden равный 0, если вы сбросите Softwaredistribution то
    сбросятся все Hidden обновления.
    
    Теоретически для красоты надо докрутить чтобы он выставлять IsHidden = 1 для всех обновлений
    кроме тех что ставтся на данный момент
    
    пример вызова
    '2976978', '3156418' | install-updates -Verbose
    #>
    function install-updates
    {
        [CmdletBinding()] 
        PARAM ( 
        [Parameter( Position=0, 
                    Mandatory=$true, 
                    ValueFromPipeline=$true,
                    ValueFromPipelineByPropertyName=$true)
                    ]
                    [string[]]$KB
        )
        
        BEGIN
        {
            Log 'call install-updates'
            $session = New-Object -ComObject Microsoft.Update.Session
            $searcher = $session.CreateUpdateSearcher()
            $Updates = $searcher.Search("IsInstalled=0 and Type='Software' and ISHidden=0")
            
            # взводим флаг наличия обновления
            if ($Updates.Updates.Count -eq 0) { $NoUpdates = $true } else { $NoUpdates = $false }
    
            # диагностический вывод
            Write-Verbose '---------------------------------------------------------------------'
            Write-Verbose "install-updates: состояние переменной NoUpdates = $NoUpdates"
    
            if ($NoUpdates -eq $false)
            {
                Write-Verbose "найдены обновления ожидающие установки"
                $temp1 = $Updates.updates | select Title
                $temp1 | Write-Verbose
                $Count = $Updates.Updates.Count
                Write-Verbose "$Count обновлений ожидают установки"
    
                Log "$Count обновлений ожидают установки"
                foreach ( $temp in $temp1 ) {  Log "     $temp"  }
            }
            else
            {
                Log "install-updates: обновления на установку не найдены "
                Write-Verbose "install-updates: обновления на установку не найдены "
            }    
            Write-Verbose '---------------------------------------------------------------------'
    
        }
        
        PROCESS
        {
            
            Write-Verbose "install-updates: передан параметр в блок process $KB"
    
            if ($NoUpdates -eq $true) { break }
            if ($KB -match ' ' ) { break }
    
            $HotFix = $Updates.Updates | where { $_.Title -like "*$KB*" }
            if ($HotFix -eq $null)
            {
                Write-Verbose "переданное на установку обновление не найдено"
                Log "переданное на установку обновление не найдено"
                break
            }
            else
            {
                $Title = $HotFix.Title
                Write-Verbose "найдено обновление на установку $Title"
                Log "найдено обновление на установку $Title"
            }
    
    
            # скачиваем обновления
            $downloads = New-Object -ComObject Microsoft.Update.UpdateColl
            $downloads.Add( $HotFix )
    
            # в этом месте известная ошибка 80070005. Эту операцию можно запустить только с правами SYSTEM
            # если вы видите эту ошибку то создайте планировщике задач задание на запуск этого скрипта с правами систем
            # и вызывайте само само задание
            Write-Verbose 'скачиваем обновление'
            Log 'download update'
            $downloader = $session.CreateUpdateDownLoader()
            $downloader.Updates = $downloads
            $downloader.Download()
            if ($HotFix.IsDownloaded)
            {
                Log 'download successfull'
                Write-Verbose 'обновление скачано. (находится в папке downloads)'
            }
    
    
            $installs = New-Object -ComObject Microsoft.Update.UpdateColl
            if ($HotFix.IsDownloaded)
            {
                $installs.Add( $HotFix ) | Out-Null
            }
            log 'start instal'
            # в этом месте известная ошибка 80070005. Эту операцию можно запустить только с правами SYSTEM
            # если вы видите эту ошибку то создайте планировщике задач задание на запуск этого скрипта с правами систем
            # и вызывайте само само задание
            $installer = $session.CreateUpdateInstaller()
            $installer.Updates = $installs
            $installresult = $installer.Install()
            $installresult
            log 'installing successfull'
        }
    
        END
        {
            Write-Verbose 'install-updates: отработали установку обновлений'
            log 'install-updates: отработали установку обновлений'
            log '--------------------------------------------------------------'
        }
    }
    
    <#
    применяйте эту функцию если вам нужно поставить больше 20-30 обновлений
    быстро ставит кучу обновлений. не делает контроль обновлений.
    предназначена для быстрого накатывания обновлений когда нужно установить много обновлений
    и отсеить хорошие от плохих. имеет свои переменные
    Пользователь не должен долго находится под работой этой функции, т.к. накатывание 10-20 обновлений
    и откат в случае ошибки не даст ему работать час или полтора после каждого срабатывания.
    ее применять можно только один раз, и желательно объяснить пользователю почему по утрам ему придется
    долго ждать отката обновлений.
    
    перед входом в процедуру нужно установить $KAFUWatchDog в ноль
    если будет 0 то значит при проходе с шагом 1/3 в каждой порции было по ошибочному обновелению
    если будет 1 то значит при проходе с шагом 1/4 в каждой порции было по ошибочному обновелению
    если будет 2 то значит при проходе с шагом 5 штук в каждой порции было по ошибочному обновелению
    #>
    # Состояния
    [int]$KAFU = Get-Content $KAFUFile
    # Скорость установки обновлений за раз
    [int]$KAFUDelta = Get-Content $KAFUDeltaFile
    function Fast-Update
    {
        function Write-KAFU
        {
        PARAM ( $State )
            Set-Content $KAFUFile -Value $State
        }
    
    
        Log "--- Fast-Update ---------------------------------------------------------"
        Log "Fast-Update запуск функции быстрой установки"
        switch ($KAFU)
        {
            0 {
                Log "вошли в состояние 0"
                
                # вычисляем с какой скоростью будем делать проход
                $session = New-Object -ComObject Microsoft.Update.Session
                $searcher = $session.CreateUpdateSearcher()
                $temp = $searcher.Search("IsInstalled=0 and Type='Software'" )
                $Count = $temp.Updates.Count
                
                # смотрим ватч дог, если проход не первый значит был фейл и надо понизить скорость установки обновлений
                $KAFUWatchDog = Get-Content $KAFUWatchDogFile
                log "KAFUWatchDog = $KAFUWatchDog"
    
                switch ($KAFUWatchDog)
                {
                    0 { # дефолтный параметр, делим количество обновлений на 2 и берем число как шаг
                        $KAFUDelta = [Math]::Truncate( $Count / 3 )
                        Log "первый вход, устанавливаем шаг 1/3 от количества и равно $KAFUDelta"
                      }
    
                    1 { # делим количество обновлений на 4 и берем число как шаг
                        $KAFUDelta = [Math]::Truncate( $Count / 4 )
                        Log "первый вход, устанавливаем шаг 1/4 от количества и равно $KAFUDelta"
                      }
    
                    2 { # у нас конкретные глюки - в каждой из четырех пачек было по ошибке обновлений
                        # ставим по 5 обновлений за раз
                        $KAFUDelta = 5
                        log 'у нас конкретные глюки - в каждой из четырех пачек было по ошибке обновлений ставим по 5'
                      }
    
                    default { Log 'неизвестное состояние ватч дога, сбрасываем в ноль'
                              $KAFUWatchDog = 0
                              $KAFUDelta = [Math]::Truncate( $Count / 3 )
                              Set-Content $KAFUWatchDogFile -Value $KAFUWatchDog
                            }
                }
    
                # сбрасываем списки перед работой
                Set-Content $LastInstalledUpdatesFile -Value ''
                Set-Content $KAFUDeltaFile -Value $KAFUDelta
                Set-Content $KAFUWatchDog2File -Value 0
    
                Write-KAFU 1
                break
              }
            1   {
                    Log "enter in state 1"
    
                    $LastInstalled = Get-Content $LastInstalledUpdatesFile
                    $BadUpdates = Get-Content $BadUpdatesFile
    
                    log 'получаем список обновлений с вышестоящего сервера'
                    $session = New-Object -ComObject Microsoft.Update.Session
                    $searcher = $session.CreateUpdateSearcher()
                    $temp = $searcher.Search("IsInstalled=0 and Type='Software'" )
                    $Updates = $temp.Updates | select title
    
                    # делаем список обновлений на установку по номерам
                    [string[]]$HotFixs = ''
                    foreach ($temp in $Updates)
                    {
                        $regex = $temp.Title -match "KB\d+"
                        $KB = $matches[0]
                        $HotFixs += $KB
                        Log "найдено обновление на установку: $KB"
                    }
                    $Count = $HotFixs.Count
                    Log "всего обновлений на установку $Count"
    
                    log 'чистим список'
                    $Upd = @()
                    foreach ($temp in $HotFixs) { if ($temp -ne '') { $Upd += $temp } }
                    $Upd
    
                    # выбросить те что пробовали ставить и заведомо плохие
                    if ($BadUpdates.Count -ne 0)
                    {
                        $temp = Compare-Object -ReferenceObject $HotFixs -DifferenceObject $BadUpdates
                        $temp = $temp | where { $_.Sideindicator -like '<=' } | select -ExpandProperty InputObject
                        $HotFixs = $temp
    
                        $Upd = @()
                        foreach ($temp in $HotFixs) { if ($temp -ne '') { $Upd += $temp } }
                        $HotFixs = $Upd
                        $temp = $HotFixs
    
                        Log "после исключения плохих обновлений остались:"
                        foreach ( $temp1 in $temp ) { Log "после исключения плохих обновлений остались: $temp1" }
                        $Count = $temp.Count
                        Log "всего: $Count штук"
                    }
                    else
                    {
                        Log "заведомо плохих обновлений нет"
                    }
    
                    if ($LastInstalled.Count -ne 0)
                    {
                        $temp = Compare-Object -ReferenceObject $HotFixs -DifferenceObject $LastInstalled
                        $temp = $temp | where { $_.Sideindicator -like '<=' } | select -ExpandProperty InputObject
                        $HotFixs = $temp
                        Log "после исключения тех что пробовали остались:"
    
                        $Upd = @()
                        foreach ($temp in $HotFixs) { if ($temp -ne '') { $Upd += $temp } }
                        $HotFixs = $Upd
                        $temp = $HotFixs
    
                        foreach ( $temp1 in $temp ) { Log "после исключения тех что пробовали остались: $temp1" }
                        $Count = $temp.Count
                        Log "всего: $Count штук"
                    }
                    else
                    {
                        Log "еще никаких не устанавливали до этого"
                    }
    
                    # проверяем ватч дог инсталлятора
                    [int]$KAFUWatchDog2 = Get-Content $KAFUWatchDog2File
                    if ($KAFUWatchDog2 -gt 20)
                    {
                        Log "сработал ватч дог инсталлятора, похоже мы зациклились в состоянии 1, принудительный сброс: $KAFUWatchDog2"
                        Write-KAFU 2
                        Set-Content $KAFUWatchDog2File -Value 0
                        break
                    }
    
                    if ($HotFixs.Count -eq 0)
                    {
                        # проход закончен
                        log 'проход закончен, обновлений не осталось. переходим в состояние 2'
                        Write-KAFU 2
                        break
                    }
                    else
                    {
                        # выбираем обновления на установку
                        $temp = $HotFixs | select -First $KAFUDelta         
                        $Count = $temp.Count
                        if ($Count -eq 0)
                        {
                            log 'не осталось обновлений. переходим в состояние 2'
                            Write-KAFU 2
                            break
                        }
                        foreach ( $temp1 in $temp ) { Log "выбраны на установку обновления: $temp1" }
                        Log "всего: $Count штук"
                        
                        $HotFixs = $temp
                        # сохраняем список
                        Add-Content -Path $LastInstalledUpdatesFile -Value ''
                        Add-Content -Path $LastInstalledUpdatesFile -Value $HotFixs
                        @(Get-Content $LastInstalledUpdatesFile) -match '\S'  | out-file $LastInstalledUpdatesFile
                        LOG "сохранили список обновлений для установки"
                        # отправляем на установку
                        log 'начинаем устанавливать обновления'
                        $HotFixs | install-updates
                    }
    
                    Set-Content $KAFUWatchDog2File -Value ($KAFUWatchDog2 + 1)
                    break
                }
            2   {   # анализируем проход если постоянная ошибка снижаем скорость и пробуем снова
                    Log 'вошли в состояние 2.'
                    $KAFUWatchDog = Get-Content $KAFUWatchDogFile
                    $KAFUDelta = Get-Content $KAFUDeltaFile
                    $LastInstalled = Get-Content $LastInstalledUpdatesFile
                    log "KAFUWatchDog = $KAFUWatchDog"
    
                    switch ($KAFUWatchDog)
                    {
                        0   {
                                # первое из состояний. Проход был успешным? Получаем список не установленых обновлений
                                $session = New-Object -ComObject Microsoft.Update.Session
                                $searcher = $session.CreateUpdateSearcher()
                                $temp = $searcher.Search("IsInstalled=0 and Type='Software'" )
                                $Updates = $temp.Updates
    
                                # количество оставшихся обновлений меньше чем дельта * 3 - 2
                                $Count1 = $Updates.Count
                                if ($KAFUDelta -eq 0) {  $KAFUDelta = 5  }
                                $Count2 = $KAFUDelta * 3 - 2
                                
                                
                                if ($Count2 -lt $Count1)
                                {
                                    # не установилось ниодно из обновлений. Снижаем скорость установки обновлений
                                    $KAFUWatchDog = 1
                                    Set-Content $KAFUWatchDogFile -Value 1
                                    Write-KAFU 0
                                    Log 'не установилось ниодно из обновлений. вач дог = 1. переходим в состояние 0'
                                    break
                                }
                                else
                                {
                                    log 'быстрая установка прошла успешно WatchDog = 0'
                                    log 'переходим в состояние 3'
    
                                    Write-KAFU 3
                                    break
                                }
                            }
    
                        1   {
                                # проверяем сколько чего наставили
                                $session = New-Object -ComObject Microsoft.Update.Session
                                $searcher = $session.CreateUpdateSearcher()
                                $temp = $searcher.Search("IsInstalled=0 and Type='Software'" )
                                $Updates = $temp.Updates | select title
    
                                # количество оставшихся обновлений меньше чем дельта * 3 - 2
                                $Count1 = $Updates.Count
                                $Count2 = $KAFUDelta * 4 - 2
    
                                if ($Count2 -gt $Count1)
                                {
                                    # у нас эпичная лажа - 4 плохих хотфикса равномерно размазаных в каждом из 25% обновлений
                                    # снижаем скорость установки обновлений до 5
                                    $KAFUWatchDog = 2
                                    Set-Content $KAFUWatchDogFile -Value 2
                                    Write-KAFU 0
                                    Log 'не установилось ниодно из обновлений. вачдог = 2. переходим в состояние 0'
                                    break
                                }
                                else
                                {
                                    log 'быстрая установка прошла успешно WatchDog = 1'
                                    log 'переходим в состояние 3'
                                    Write-KAFU 3
                                    break
                                }
                            }
    
                        2   { # мы отработали установку по 5 обновлений за раз
                                log 'быстрая установка прошла успешно WatchDog = 2'
                                log 'переходим в состояние 3'
    
                                Write-KAFU 3
                                break
                            }
    
                        default { Log 'неизвестное состояние сбрасываем вачдог на 0' 
                                  Set-Content $KAFUWatchDogFile -Value 0
                                  Write-KAFU 0
    
                                  break
                                } 
                    }
                }
            3   { # закончили, циклимся
                    Write-KAFU 3
                    break
                }
            default { 
                        Set-Content $KAFUWatchDogFile -Value 0
                        Write-KAFU 0
                        Log "warning!!! неизвестное состояние КА, сброшен на 0. KAFU = $KAFU"
                        break
                    }
        }
        Log "--- Fast-Update -- отработала-------------------------------------------------------"
    }
    
    <################################################################################################
    можно дописать два обновления за проход.
    наиболее щадящяя функция по отношению к пользователю.
    Применяется для ускорения установки обновлений, ставит по паре за один проход.
    если обновлений будет 48 то на установку всех уйдет 24 дня по 2 в день, что займет
    1 месяц. При небольших обновлениях для пользователя займет 3-6 минут в день, если обновы 
    большие то до 30-40 минут в случае сбоя.
    ################################################################################################>
    
    
    <#
    устанавливает обновления по одному с выкидываем в лист плохих обновлений
    центровая фишка вокруг которой все крутится
    именно благодаря этой функции мы можем сделать работу почти автоматической
    #>
    
    # Состояния
    [int]$KASU = Get-Content $KASUFile
    function Slow-update
    {
        function Write-KASU
        {
        PARAM ( $State )
            Set-Content $KASUFile -Value $State
        }
    
        Log "--- Slow Update ---------------------------------------------------------"
        Log "Slow Update запуск"
    
        switch ($KASU)
        {
            0   {
                    # устанавливаем одно обновление
                    log "Вошли в состояние 0, делаем первоначальные настройки"
    
                    # сбрасываем списки перед работой
                    Set-Content $LastInstalledUpdatesFile -Value ''
                    Set-Content $KAFUWatchDog2File -Value 0
                    Set-Content $KAFUWatchDog2File -Value 0
    
                    Write-KASU 1
                    break
                }
            1   {
                    log "Вошли в состояие 1"
    
                    $LastInstalled = Get-Content $LastInstalledUpdatesFile
                    $BadUpdates = Get-Content $BadUpdatesFile
                    
                    log 'получаем список обновлений с вышестоящего сервера'
                    $session = New-Object -ComObject Microsoft.Update.Session
                    $searcher = $session.CreateUpdateSearcher()
                    $temp = $searcher.Search("IsInstalled=0 and Type='Software'" )
                    $Updates = $temp.Updates | select title
    
                    # делаем список обновлений на установку по номерам
                    [string[]]$HotFixs = ''
                    foreach ($temp in $Updates)
                    {
                        $regex = $temp.Title -match "KB\d+"
                        $KB = $matches[0]
                        $HotFixs += $KB
                        Log "найдено обновление на установку: $KB"
                    }
                    $Count = $HotFixs.Count
                    Log "всего обновлений на установку $Count"
    
                    $Upd = @()
                    foreach ($temp in $HotFixs) { if ($temp -ne '') { $Upd += $temp } }
                    $PotentialBad = $Upd
    
                    # выбросить те что пробовали ставить и заведомо плохие
                    if ($BadUpdates.Count -ne 0)
                    {
                        $temp = Compare-Object -ReferenceObject $HotFixs -DifferenceObject $BadUpdates
                        $temp = $temp | where { $_.Sideindicator -like '<=' } | select -ExpandProperty InputObject
                        $HotFixs = $temp
                        Log "после исключения плохих обновлений остались:"
    
                        $Upd = @()
                        foreach ($temp in $HotFixs) { if ($temp -ne '') { $Upd += $temp } }
                        $HotFixs = $Upd
                        $temp = $HotFixs
    
                        foreach ( $temp1 in $temp ) { Log "после исключения плохих обновлений остались: $temp1" }
                        $Count = $temp.Count
                        Log "всего: $Count штук"
                    }
                    else
                    {
                        Log "заведомо плохих обновлений нет"
                    }
    
                    $Upd = @()
                    foreach ($temp in $HotFixs) { if ($temp -ne '') { $Upd += $temp } }
                    $HotFixs = $Upd
    
                    if ($LastInstalled.Count -ne 0)
                    {
                        $temp = Compare-Object -ReferenceObject $HotFixs -DifferenceObject $LastInstalled
                        $temp = $temp | where { $_.Sideindicator -like '<=' } | select -ExpandProperty InputObject
                        $HotFixs = $temp
                        Log "после исключения тех что пробовали остались:"
    
                        $Upd = @()
                        foreach ($temp in $HotFixs) { if ($temp -ne '') { $Upd += $temp } }
                        $HotFixs = $Upd
                        $temp = $HotFixs
    
                        foreach ( $temp1 in $temp ) { Log "после исключения тех что пробовали остались: $temp1" }
                        $Count = $temp.Count
                        Log "всего: $Count штук"
                    }
                    else
                    {
                        Log "еще никаких не устанавливали до этого"
                    }
    
                    $Upd = @()
                    foreach ($temp in $HotFixs) { if ($temp -ne '') { $Upd += $temp } }
                    $HotFixs = $Upd
    
                    if ($HotFixs.Count -eq 0)
                    {
                        # проход закончен
                        log 'проход закончен, обновлений не осталось. все не установленные потенциально плохие'
    
                        if ($PotentialBad -ne 0 )
                        {
                            foreach ( $Bad in $PotentialBad )
                            {
                                log "   вероятно плохое обновление: $Bad"
                            }
                            
                            # выходим отсюда т.к. функция не стандалон, репорт и добавление в плохие должен сделать вышестоящий
                            #Add-Content -Path $BadUpdatesFile -Value ''
                            #Add-Content -Path $BadUpdatesFile -Value $LastInstalled
                        }
    
                        Write-KASU 2
                        break
                    }
                    else
                    {
                        # выбираем обновления на установку
                        $temp = $HotFixs | select -First 1        
                        $Count = $temp.Count
                        if ( $Count -eq 0 )
                        {
                            log 'переходим в состояние 2'
                            Write-KASU 2
                            break
                        }
                        foreach ( $temp1 in $temp ) { Log "выбраны на установку обновления: $temp1" }
                        Log "всего: $Count штук"
                        
                        $HotFixs = $temp
                        # сохраняем список
                        Add-Content -Path $LastInstalledUpdatesFile -Value ''
                        Add-Content -Path $LastInstalledUpdatesFile -Value $HotFixs
                        @(Get-Content $LastInstalledUpdatesFile) -match '\S'  | out-file $LastInstalledUpdatesFile
                        LOG "сохранили список обновлений для установки"
                        # отправляем на установку
                        log 'начинаем устанавливать обновления'
                        Write-Host $HotFixs -ForegroundColor Green
                        $HotFixs | install-updates
                    }
    
                    Set-Content $KAFUWatchDog2File -Value ($KAFUWatchDog2 + 1)
                    break
                }
            2   {
                    # завершили работу, циклимся
                    Write-KASU 2
                    break
                }
            default {
                        # неизвестное состояние
                        log "неизвестное состояние"
                        Write-KASU 0
                    }
        }
        Log "--- Slow-Update -- отработала-------------------------------------------------------"
    }
    
    
    [int]$KA = Get-Content $KAFile
    
    function Write-KA
    {
        PARAM ( $State )
        Set-Content $KAFile -Value $State
    }
    
    
    Log ' '
    Log " "
    Log ' '
    Log ' '
    Log ' '
    Log '##############################'
    Log "### --- Мы запустились --- ###"
    Log '##############################'
    
    switch ($KA)
    {
        0   { # мы запустились
                log( 'КА = 0. Сбрасываем все переменные и переходим в состояние ожидания обновлений' )
                Set-Content $LastInstalledUpdatesFile -Value ''
                Set-Content $KAFUWatchDogFile -Value 0
                Set-Content $KASUFile -Value 0
                Set-Content $KAFUFile -Value 0
    
                Write-KA 9
                break
            }
    
        1   { # ставим обновления
                Log 'Состояние КА 1. Переходим быстрой установке обновлений'
                [int]$KAFU = Get-Content $KAFUFile
                if ($KAFU -eq 3)
                    {
                        # быстрая установка закончилась
                        log 'быстрая установка отработала, переходим к медленной установке'
                        Write-KA 2
                        Set-Content $KASUFile -Value 0
                        break
                    }
                else
                    {
                        log 'вызываем быстрый устновщик'
                        ResetSoftwareDistribution
                        Fast-Update
                        Trim-Log
                        break
                    }
            }
    
        2   { # отработали
                log 'состояние 2, медленная установка'
    
                [int]$KASU = Get-Content $KASUFile
    
                if ($KASU -eq 2)
                {
                    # отработали медленную установку
                    # делаем репорт админу что отработка закончилась и нужно обновления проверить и возможно перенсти в группу плохих
                    Trim-Log
                    ResetSoftwareDistribution
                    log( 'КА = 2. Отработали медленную установку обновлений' )
                    $BadUpdates = Get-Content $BadUpdatesFile
              
                    $session = New-Object -ComObject Microsoft.Update.Session
                    $searcher = $session.CreateUpdateSearcher()
                    $temp = $searcher.Search("IsInstalled=0 and Type='Software'" )
                    $Updates = $temp.Updates | select title
    
                    # делаем список обновлений на установку по номерам
                    [string[]]$HotFixs = ''
                    foreach ($temp in $Updates)
                    {
                        $regex = $temp.Title -match "KB\d+"
                        $KB = $matches[0]
                        $HotFixs += $KB
                        Log "найдено обновление на установку: $KB"
                    }
                    $Count = $HotFixs.Count
                    Log "всего обновлений на установку $Count"
    
                    $PotentialBad = $HotFixs
    
                    # выбросить заведомо плохие
                    if ($BadUpdates.Count -ne 0)
                    {
                        $temp = Compare-Object -ReferenceObject $HotFixs -DifferenceObject $BadUpdates
                        $temp = $temp | where { $_.Sideindicator -like '<=' } | select -ExpandProperty InputObject
                        $HotFixs = $temp
                        Log "после исключения плохих обновлений остались:"
                        foreach ( $temp1 in $temp ) { Log "после исключения плохих обновлений остались: $temp1" }
                        $Count = $temp.Count
                        Log "всего: $Count штук"
                    }
                    else
                    {
                        Log "заведомо плохих обновлений нет"
                    }
        
                    $Updates = @()
                    foreach ($temp in $HotFixs)
                    {
                        if ($temp -ne '')  {  $Updates += $temp  }
                    }
    
                    if ($Updates.Count -ne 0)
                    {
                        # отправляем административное оповещение
                        log 'отправляем административное оповещение и добавляем обновления с писок плохих'
    
                        Add-Content -Path $BadUpdatesFile -Value ''
                        Add-Content -Path $BadUpdatesFile -Value $Updates
    
                        $Date = Get-Date
                        $CompName = $env:COMPUTERNAME
                        $body = "<H1>завершена установка обновлений на компьютер $CompName,<br><br> вероятно проблемные обновления</H1><h3>"
                        foreach ($upd in $updates)
                        {    $body += "<br> $upd<br>"   }
    
                        $body += "<br>они добавлены в список плохих и больше устанавливаться не будут. проверьте их предварительно, возможно среди них обновление одобренное вчера или сегодня"
    
                        report -text $body
                    }
    
                    Write-KA 9
                    break
                }
                else
                {
                    log 'обновления еще не доставились ставим одно обновление и засыпаем'
                    ResetSoftwareDistribution
                    Trim-Log
                    Slow-update
                    break
                }
            }
        9   { # ждем когда появятся обновления
    
                log( 'КА = 9. Ждем появления обновлений на установку' )
                Trim-Log
                ResetSoftwareDistribution
    
                $BadUpdates = Get-Content $BadUpdatesFile
                
                log 'получаем обновления с вышестоящего сервера'
    
                $session = New-Object -ComObject Microsoft.Update.Session
                $searcher = $session.CreateUpdateSearcher()
                $temp = $searcher.Search("IsInstalled=0 and Type='Software'" )
                $Updates = $temp.Updates | select title
    
                # делаем список обновлений на установку по номерам
                [string[]]$HotFixs = ''
                foreach ($temp in $Updates)
                {
                    $regex = $temp.Title -match "KB\d+"
                    $KB = $matches[0]
                    $HotFixs += $KB
                    Log "найдено обновление на установку: $KB"
                }
                $Count = $HotFixs.Count
                Log "всего обновлений на установку $Count"
    
                $PotentialBad = $HotFixs
    
                # выбросить заведомо плохие
                if ($BadUpdates.Count -ne 0)
                {
                    $temp = Compare-Object -ReferenceObject $HotFixs -DifferenceObject $BadUpdates
                    $temp = $temp | where { $_.Sideindicator -like '<=' } | select -ExpandProperty InputObject
                    $HotFixs = $temp
                    Log "после исключения плохих обновлений остались:"
                    
                    $Upd = @()
                    foreach ($temp in $HotFixs) { if ($temp -ne '') { $Upd += $temp } }
                    $HotFixs = $Upd
                    $temp = $HotFixs
                    
                    foreach ( $temp1 in $temp ) { Log "после исключения плохих обновлений остались: $temp1" }
                    $Count = $temp.Count
                    Log "всего: $Count штук"
                }
                else
                {
                    Log "заведомо плохих обновлений нет"
                }
    
                log 'чистим список'
                $Updates = @()
                foreach ($temp in $HotFixs)
                {
                    if ($temp -ne '')
                    {  $Updates += $temp   }
                }
    
                $Count = $Updates.Count
                Log "Всего обновлений $Count переключаем установщик"
                # если обновлений больше чем $FastInstallLimit запускаем быструю установку
                if ($Updates.Count -gt $FastInstallLimit)
                {
                    # ставим обновления быстро
                    log "updates more than $FastInstallLimit. Go to FastInstall"
                    Write-KA 1
                    break
                }
                
                if ($Updates.Count -gt 0)
                {
                    log "Go to Slow Install"
                    Write-KA 2
                    Set-Content $KASUFile -Value 0
                    break
                }
    
                if ($Updates.Count -eq 0)
                {
                    log 'новых обновлений нет ждем обновлений'
                    Write-KA 9
                    break
                }
               
            }
        default { # неизвестное состояние
                    log( 'неизвестное состояние главного автомата. переходим в состояние 0' )
                    Write-KA 0 
                }
    }   
    
    #ResetSoftwareDistribution
    #Fast-Update
    #Trim-Log
    #Slow-update
    
    Log '##############################'
    Log "### --- Мы отработали  --- ###"
    Log '##############################'
    
    if ( $RebootEnabled -eq $true )
    {
        Restart-Computer -Force
        Log 'перезагрузка >>>'
    }
    
    
    
    <#
    
    schtasks /run /tn "\My_Tasks\PSWindowsUpdate"
    
    
    [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("cp866")
    
    #>
    


    5. создать задание в планировщике с правами SYSTEM на запуск 1 раз в день. Известная проблема — установка обновлений может быть инициирована только под правами SYSTEM. (кстати можете этот способ применять для запуска активаторов и всего что должно работать с наивысшей привелегией)


    хорошо себя показала задержка после старта 6 часов


    Все, сценарий должен работать.

    Первые 3 раза его можно запустить вручную для проверки. Запустите задание из планировщика и проверьте содержимое файла \BadUpdates\log.txt он должен содержать лог работы сценария. Если он пуст проверьте разрешена ли политика исполнения скриптов, доступен ли сервер обновлений, попробуйте вручную поискать обновления.

    Если обновлений будет больше чем 45 (можете менять это значение) сценарий будет пытаться устанавливать обновления пачками. При этом первый раз он попытается поставить по 1\3 за раз, если ни одна из 3х пачек обновлений не установится, шаг будет изменен на 1/4, если ниодна из пачек не встанет то алгоритм перейдет на установку по 5. Данный алгоритм родился из жизни, когда встретилось на одной машине 6 неподходящих обновлений равномерно размазаных, при этом устанавливать по одному почти 90 обновлений не вариант (90 дней по одной обновке).

    Происходить будет следующее:
    1. каждый день WUErrorreporting будет проверять журнал на ошибку 20, если ошибка есть то будет отправлять оповещение на почту с выборкой какое обновление и когда не установилось.
    2. каждый день будет происходить сброс папки SoftwareDistribution
    3. каждый день скрипт установки обновлений будет проверять наличие обновлений. Если обновления есть то:
    4. если их больше чем 45 автомат переключится в состояние быстрых обновлений и «уснет» до перезагрузки
    • на следующий день сбросит автомат быстрых обновлений, вычислено сколько обновлений ставить за раз, при первом проходе 1/3, при втором быстром проходе 1/4, при третьем по 5
    • каждый день пока работает быстрая установка будет получатся список обновлений из него будут удалены обновления которые пробовали ставить ранее и обновления из списка точно плохих обновлений
    • из оставшихся будут выбраны некоторое количество обновлений, они будут отправлены на установку
    • обновления отправленные на установку будут записаны в файл BadUpdates\LastInstalledUpdates.txt и будут исключены из списка при следующем проходе
    • После завершения быстрого прохода произойдет автоматический переход к установке по одному обновлению за раз

    5. если их меньше 45 автомат переключится в режим установки обновлений по одному
    6. в режиме установки по одному
    • сначала произойдет сброс переменных для установки по одному обновлению
    • будет получен список обновлений без тех что пробовали и точно плохих, если обновлений не осталось то не установленные обновления будут добавлены в список точно плохих и потправлены оповещением на почту. Если еще есть то начнется установка

    7. После отработки автомат перейдет в состояние ожидания обновлений

    Если в логе видно что не удаляется папка SoftwareDistribution проверьте папку SoftwareDistribution\Plugins, какаято программа держит ее и не дает очистить.
    Если вам нужно обновить свежеустановленную систему сделайте следующее:
    • установите параметр $RebootEnabled = $true, это разрешит сделать перезагрузку сразу же после работы скрипта
    • установите триггер срабатывания на 10 минут после запуска системы
    • сохраните сценарий и запустите его
    • дальше он сам будет пытаться устанавливать обновления и будет сам перезагружать компьютер, создавать отдельного пользователя, или логинится специально для его работы не нужно.
    • через определенное время контролируйте его заглядывая в лог файл
    • когда он все поставит и перейдет в состояние 9 (ожидание обновлений) установите $RebootEnabled = $false
    • отключите триггер если вам больше не нужно обновляться


    разберем работу

    WUErrorreporting


    1. Установите Ваши переменные
    Наиболее значимым явлется переменная $DaysBefore — за сколько дней будет сканироваться журнал, по умолчанию 5 дней. Если произошла ошибка установки обновления то вы будете получать административное оповещение 5 дней подряд об одной и тойже ошибке. В принципе нормально, ставить 1 или 2 дня, не имеет смысла, можно пропустить репорт если ктото выходил работать на выходные.

    Переменная $SaveLog отвечает за сохранение лога текущего прогона на диск. Полезно, вы можете придти или подключится к клиентской машине и во время работы заглянуть какое обновление не поставилось.

    Последние 4 переменные регулируют кому отправлять отчет. Нужно оставить $SendReport = $true если нужно чтобы приходило оповещение на почту, указать email админа $ReportMail1 = 'admin@test.local' и на какой сервер слать $SMTPServer = 'mail.test.local'. Если нужна аутентификация то добавьте ее в 156 строке

    секция переменных
    #############
    # Variables #
    #############
    
    # количество дней за который будет просматриваться журнал
    [int]$DaysBefore = 5
    
    # сохранять ли список не установившихся обновлений и куда
    $SaveLog = $false
    $LogPath = 'c:\WUError.txt'
    
    # сохранять отчет HTML
    [boolean]$SaveReport = $False
    [string]$ReportPath = 'C:\WUErrorReport.html'
    
    # отправлять HTML отчет
    [boolean]$SendReport = $true
    [string]$ReportMail1 = 'admin@test.local'
    $from = 'bot_abormot@test.local'
    [string]$SMTPServer = 'mail.test.local'
    



    2. Принцип работы очень прост

    сгружаем только ошибки с ID20 из журнала System
    $Events = Get-EventLog -LogName System -EntryType Error -After (Get-Date).AddDays(-$DaysBefore) -InstanceId 20 -ErrorAction SilentlyContinue
    


    Если ошибки есть то формируем объекты отчета

    # контейнер для событий
    $Log = @()
    
    # переберием события и вытаскиваем номера обновлений с ошибками установки
    foreach ($Event in $Events)
    {
        $regex = $Event.Message -match "KB\d+"
        $KB = $matches[0]
        
        $params = [ordered]@{ 'KB'=$KB
                              #'EntryType'=$Event.EntryType
                              'Index'=$Event.Index
                              'MachineName'=$Event.MachineName
                              'Message'=$Event.Message
                              'Source'=$Event.Source
                              'TimeGenerated'=$Event.TimeGenerated
                              'TimeWritten'=$Event.TimeWritten
                              'UserName'=$Event.UserName
                            }
    
        $obj = New-Object -TypeName PSObject -Property $params
    
        $Log += $obj
    }
    

    и отправляем их «правильным» способом
        $encoding = [System.Text.Encoding]::UTF8
        $body = ConvertTo-HTML -head $head -PostContent $frag1 -PreContent "<h1>Windows Update error report. Client $ClientName. Date:$Date</h1>" | Out-String
        
        $params = @{'To'=$ReportMail1
                   'From'=$from
                   'Subject'="$ClientName. Windows Update error $Date"
                   'Body'=$Body
                   'BodyAsHTML'=$True
                   'SMTPServer'=$SMTPServer}
    
        Send-MailMessage @params -Encoding $encoding
    

    применяемый CSS стиль заточен под outlook, а он в качестве HTML движка использует Word, что ведет к неполной поддержке стандарта. Если ктонибудь подберет другой красивый стиль который работал бы в Outlook скиньте и мне.
    CSS стиль отчетов
    $head = @'
    <style>
    body { background-color:#ffffff;
               font-family:Tahoma;
    	   font-size:12pt; }
    td, th { border:1px solid black;
               border-collapse:collapse; }
    th { color:white;
               background-color:black; }
               table, tr, td, th { padding: 2px; margin: 0px }
    table {
    font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
    font-size: 14px;
    border-radius: 10px;
    border-spacing: 0;
    text-align: center;
    }
    th {
    background: #BCEBDD;
    color: white;
    text-shadow: 0 1px 1px #2D2020;
    padding: 10px 20px;
    }
    th, td {
    border-style: solid;
    border-width: 0 1px 1px 0;
    border-color: white;
    }
    th:first-child, td:first-child {
    text-align: left;
    }
    th:first-child {
    border-top-left-radius: 10px;
    }
    th:last-child {
    border-top-right-radius: 10px;
    border-right: none;
    }
    td {
    padding: 10px 20px;
    background: #F8E391;
    }
    tr:last-child td:first-child {
    border-radius: 0 0 0 10px;
    }
    tr:last-child td:last-child {
    border-radius: 0 0 10px 0;
    }
    tr td:last-child {
    border-right: none;
    }
    </style>
    '@
    



    Install-ClientUpdate.ps1


    в скрипте реализованы 3 конечных автомата, для простого администратора сценарий будет достаточно сложным для понимания.

    выставляем нужные перменные:
    $FastInstallLimit выше этого порога будет включатся быстрая установка, меньше этого порога будем ставить обновления по одному.
    $RebootEnabled разрешает перезагрузку после каждого прохода скрипта. Применяется если вам нужно обновить машину. Устанавливаете этот параметр в $true, выставляете время запуска 10 минут и оставляете до утра.

    Чуть ниже идут настройки почты
    ########################## Report params ###################################
    [boolean]$SaveReport = $true
    [string]$ReportPath = 'C:\Report-InstallUpdates.html'
    [boolean]$SendReport = $true
    [string]$From = 'bot_abormot@test.local'
    [string]$SMTP = 'mail.test.local'
    [string]$ReportMail1 = 'admin1@test.local'
    [string]$ReportMail2 = 'admin2@test.local'
    [string]$ReportMail3 = 'admin3@test.local'
    

    Если вам нужно посмотреть есть ли обновления на удаленной или локальной машине используйте следующий код. Можно просто скопипастить в консоль
    $session = New-Object -ComObject Microsoft.Update.Session
    $searcher = $session.CreateUpdateSearcher()
    $temp = $searcher.Search("IsInstalled=0 and Type='Software'" )
    $Count = $temp.Updates.Count
    $Count
    ''
    


    Общая рекомендация: при первом запуске лучше быть на связи с пользователем, т.к. он пугается долгой установки обновлений и последующего отката.
    Также, лучше ему объяснить чтобы он просто отключал компьютер, а не нажимал «обновить и завершить работу» иначе установятся вообще все обновления и логика работы будет нарушена.

    Данный сценарий прошел обкатку на win 8.1; 2012R2; win 7 x64

    дополнение 2016.08.03:
    если у вас есть известное точно плохое обновление запишите его вручную в файлик \BadUpdates\BadUpdates.txt в виде KB121212 при работе скрипт будет автоматический выбрасывать это обновление из вариантов на установку
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 16

      –1
      Всё выше приведённое совершенно не нужно. Обновления, распространяемые через WSUS, нужно проверять до одобрения.
        +2
        Принцип красив и правилен, но по разному бывает.

        • Тестовая среда не может гарантировать что раз в 1-2 года не пройдет обновление парализующее работу целых отделов. И когда приходишь к ним на точку нужно иметь навыки и инструменты чтобы с этой проблемой разбираться.
        • Чаще глючат нотики, системы с нестандартным шифрованием и подменой библиотек (какойнибудь спец софт), вы тоже их тестите, прям все виды что у вас есть?
        • Для тестов нужны мощности, методика и человек, что уже бывает не часто.
        • у меня друзья работают в крупных конторах и тестов никто не делает. Предпочитают просто не обновлять. И им такой инструментарий нужен.
        • Недавно ставили windows embeded на миникомп, отсев глючных обнов вручную занял 2 дня (!) у одного человека, ясно что он чтото еще паралельно делал, но это не нормальная ситуация.
        • Данный скрипт родился как продолжение скрипта разворачивающего автоматически всус, с автоматическим обслуживанием. Логично было написать вторую часть со стороны клиента.


        штука достаточно сложная, родилась из жизни. В этом году когда 3 раза подряд с начала года вышли обновы на битлокер, BCD и на системные либы которые вызвали в нашей инфраструктуре сбой. Результат — звонят с отделов ругаются, мы бегаем ножками и отключаем обновления.
        • писалось 7 дней
        • тестилось 2,5 месяца

        Итог — воткнул на такую машину скрипт и забыл о ней, подключаюсь иногда проверяю что происходит, когда он отработает все скидывает список того что не может встать на эту машину. По моему эффект есть, т.к. это уже навсегда.

        При командировке в другой филиал, если есть такая же проблема не нужно сидеть и целый день тыцкать 100-150 обнов, также поставил на автоматическую установку и пошел делать чтонибудь другое.
          0

          WSUS позволяет контролировать установку обновлений на клиентскую Windows не корпоративного выпуска?

            +1
            да. На клиенте выглядит так как будто этого обновления вообще не существует.

            Но при администрировании сервера всус это обновление будет лезть при фильтрации обнов во время апрува, и можно случайно его апрувнуть
          0

          Интересный получился у вас скрипт. Надо будет обязательно попробовать на досуге, спасибо. Правильно ли я понимаю, что перед его установкой нужно отключить автоматическое обновление?


          И было бы замечательно, если бы вы выложили исходники на GitHub

            0
            спасибо. Это не скрипт даже, это скорее программа написаная на скриптовом языке.
            в ней 3 конечных автомата, 2 ватч дога, и простенький граф переходов. Я осознаю что для среднестатистического админа это довольно крышесносно, но для прогера ничего сложного

            автоматическое обновление отключать не нужно. можете оставить настройку 2 или 3. если поставите 4 (автоматическая установка) то не будет от него смысла.
            При тесте на win7 был получен странный глюк, когда search обновлений зависал если сервер обновлений был microsoft. с локальным wsus работает точно проверено многократно.

            можете развернуть виртуальную машину, закинуть его туда и поставить разрешение на ребут

            у нее не хватает функции когда она скрывает обновления которые не будет ставить в текущей сессии работы, тогда будет гарантированно правильный ребут и выключение и в отработку отправятся только эти обновления. Данное дописывать не стал просто в виду того что мне в продакшене не пригодилось, както само заработало в таком виде.
            0

            Проблема с обновлениями в Windows действительно существует, никогда не понимал почему Microsoft не может сделать уже нормальную систему обновлений?! И есть ведь на кого равняться: Почему в Linux и других unix like системах при использовании пакетных менеджеров такой проблемы просто никогда не было и не наблюдается?
            Система обновлений Windows, та что существует на данный момент, тихий ужас: она отжирает огромное количество ресурсов просто ради установки новых обновлений. Из-за этого во время установки обновлений, на большинстве систем наблюдаются адские тормоза, идет большая нагрузка на ЦП и Жесткий диск. Пользователи сидят, иногда больше получаса наблюдая экран установки обновлений. Почти всегда это сильно тормозит рабочий процесс. Я уж не говорю что папка Windows некоторых систем после обновления может спокойно достигать аж 100 гигов, или про эти неотключаемые обновления до Windows 10...


            Забавно, но иногда действительно создается такое впечатление, что Microsoft через свои обновления бесплатно использует вычислительные мощности вашей машины, для создания огромного и децентрализованного пула ресурсов для своего Azure (шутка конечно)

              0
              Согласен. Ну это что критиковать, думаю логика есть какаято. У МС TrustedInstaller запускается при старте, чем больше обновлений тем дольше идет перебор вариантов, фактически чем больше времени тем тормознутее работают старые версии windows т.к. у них экспоненциально растет количество переборов.

              Полный набор обновлений по сегодня на офис 2013 около 6,43Гб
              Полный набор обновлений по сегодня на офис 2010 около 6,43Гб
              Полный набор обновлений по сегодня на вин7\2008R2 около 2,0 Гб
              Полный набор обновлений по сегодня на вин8.1\2012R2 около 2,64 Гб (плюс еще пак с 8 до 8.1 еще гига 1,5)

              тоесть если вы устанавливаете офис 2010 размером 2 Гб, на него сверху ляжет патчей еще 6,5Гб

              Но есть и плюсы:
              • обновления выкатывают регулярно
              • из тысяч обновлений только 1-2 в год может вызвать проблемы
              • при всей сложности системы она досточно стабильна, юзабельна, и стандартизирована
              • офисные продукты очень быстрые (либер не может сравнится по скорости с эксель даже близко)
              • огромное количество софта
              • UFO just landed and posted this here
                  0
                  вроде на вин 8.1 систему обновлений переделали. она поумнее и пошустрее стала работать. можно выставить задержку на запуск TrustedInstaller.

                  если у вас есть выход в инет жить совсем без обнов плохо, хотя бы секурити патчи надо ставить
                  +1
                  Вроде как Linux, в обновлениях целое ядро подсовывает вместо заплаток безопасности. А Windows, приходится пересчитывать все зависимости.
                    +1
                    Да и именно поэтому так долго. Чем больше апдейтов тем больше вариантов.

                    на win7 х64 есть 231 апдейт, установка на машине с 1 процессором и 3 Гб памяти займет 2 (!) дня. А если он еще не может поставить какойто 1 и делает потом циклический откат 3 раза, то его поиск правращается в маленький ад.
                      0
                      Для свежеустановленных ОС можно попробовать что-то типа этого (не ради рекламы, никак с ресурсом не связан)
                      _http://forum.oszone.net/thread-257198-75.html

                      Времени занимает меньше чем два дня.
                        0
                        это если сбойных нет
                        0
                        На виртуалке полное обновление занимает 6-8 часов, а с KB3125574 устанавливает ~70 патчей за 2-4 часа.

                        Чтобы избежать этого Windows 10 периодически обновляется установкой новой сборки ОС вместо отдельных патчей (особенно заметно на Insider Preview), а Office 2016 идёт в виде App-V для большнства клиентов.
                    0
                    ИМХО довольно интересный вариант для небольшого парка в 80-100 машин. Прямо взял на заметку! Автор, спасибо тебе!

                    Only users with full accounts can post comments. Log in, please.