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

Раз в год выходит обновление ломающее привычную работу. 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 "

Windows Update error report. $ClientName


Total $TotalErrors errors.

" | Out-String Write-Verbose 'definiting CSS' $head = @' '@ $Date = Get-Date if ($SendReport -eq $true) { Write-Verbose 'SendEmail' $encoding = [System.Text.Encoding]::UTF8 $body = ConvertTo-HTML -head $head -PostContent $frag1 -PreContent "

Windows Update error report. Client $ClientName. Date:$Date

" | 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 кб), чтобы не копипастить скачайте его отсюда
6a29f48cc51c4e81993571673b18a0ec.jpg
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 = "

завершена установка обновлений на компьютер $CompName,

вероятно проблемные обновления

" foreach ($upd in $updates) { $body += "
$upd
" } $body += "
они добавлены в список плохих и больше устанавливаться не будут. проверьте их предварительно, возможно среди них обновление одобренное вчера или сегодня" 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. (кстати можете этот способ применять для запуска активаторов и всего что должно работать с наивысшей привелегией)
96dcf7a552e7427b8f547c6e32dbae2d.jpg

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

© Habrahabr.ru