Раз в год выходит обновление ломающее привычную работу. 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 при работе скрипт будет автоматический выбрасывать это обновление из вариантов на установку
Поделиться с друзьями
-->

Комментарии (16)


  1. tsklab
    02.08.2016 06:09
    -1

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


    1. pak-nikolai
      02.08.2016 06:53
      +2

      Принцип красив и правилен, но по разному бывает.

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


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

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

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


    1. kvaps
      02.08.2016 09:54

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


      1. pak-nikolai
        02.08.2016 10:15
        +1

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

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


  1. kvaps
    02.08.2016 09:22

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


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


    1. pak-nikolai
      02.08.2016 09:31

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

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

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

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


  1. kvaps
    02.08.2016 09:46

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


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


    1. pak-nikolai
      02.08.2016 10:05

      Согласен. Ну это что критиковать, думаю логика есть какаято. У МС 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 в год может вызвать проблемы
      • при всей сложности системы она досточно стабильна, юзабельна, и стандартизирована
      • офисные продукты очень быстрые (либер не может сравнится по скорости с эксель даже близко)
      • огромное количество софта


    1. zedalert
      02.08.2016 10:07

      Мне на домашнем ноуте так и не удалось побороть пожирание ресурсов при установке/проверке обновлений, пришлось полностью их отключить, иначе почти всё время svhosts -netsvc что-то делает, или TrustedInstaller лопатит весь диск. В общем: чем дальше в дебри администрирования Windows залезаешь, тем меньше он нравится.


      1. pak-nikolai
        02.08.2016 10:13

        вроде на вин 8.1 систему обновлений переделали. она поумнее и пошустрее стала работать. можно выставить задержку на запуск TrustedInstaller.

        если у вас есть выход в инет жить совсем без обнов плохо, хотя бы секурити патчи надо ставить


    1. AlexeevEugene
      02.08.2016 10:08
      +1

      Вроде как Linux, в обновлениях целое ядро подсовывает вместо заплаток безопасности. А Windows, приходится пересчитывать все зависимости.


      1. pak-nikolai
        02.08.2016 10:11
        +1

        Да и именно поэтому так долго. Чем больше апдейтов тем больше вариантов.

        на win7 х64 есть 231 апдейт, установка на машине с 1 процессором и 3 Гб памяти займет 2 (!) дня. А если он еще не может поставить какойто 1 и делает потом циклический откат 3 раза, то его поиск правращается в маленький ад.


        1. ErshoFF
          02.08.2016 10:49

          Для свежеустановленных ОС можно попробовать что-то типа этого (не ради рекламы, никак с ресурсом не связан)
          _http://forum.oszone.net/thread-257198-75.html

          Времени занимает меньше чем два дня.


          1. pak-nikolai
            02.08.2016 11:53

            это если сбойных нет


        1. navion
          02.08.2016 14:33

          На виртуалке полное обновление занимает 6-8 часов, а с KB3125574 устанавливает ~70 патчей за 2-4 часа.

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


  1. JTProg
    08.08.2016 01:12

    ИМХО довольно интересный вариант для небольшого парка в 80-100 машин. Прямо взял на заметку! Автор, спасибо тебе!