Предыстория
До недавнего времени я работал эникейщиком в крупной российской компании, имеющей множество офисов по всей стране. В моём ведении были одиннадцать площадок, находящихся в разных городах Дальнего востока (это важно). В каждом из этих офисов была своя, не связанная с другими, сетевая инфраструктура — свой домен AD, своя подсеть и т.д.
Однажды руководство поставило мне задачу организовать процесс автоматического обновления ОС и программ от MS и разрешило развернуть на подотчётных площадках WSUS.
Проблема
После того, как WSUS был развернут, политики привязки компьютеров к группам WSUS настроены, синхронизация каталогов содержимого с серверами MS проведена и т.п., возник вопрос: а кто же, и, главное, как, будет одобрять обновления?
Представьте себе: 11 городов, связь с которыми настолько «быстра» и «стабильна», что редкие минуты, когда задержки ICMP-ответов составляют всего 800 мс, воспринимаются, как неслыханное везение. И в каждом необходимо проводить тестирование всех обновлений перед развертыванием. При этом доступ к серверам на площадках был только по RDP, т.е. нужно было еженедельно (распоряжение устанавливало именно такую периодичность) подключаться к серверу каждого города и вручную одобрять использование новых обновлений в тестовых группах и, соответственно, переводить испытанные обновления в промышленную эксплуатацию:
Зачем изобретать велосипед
Читатель, имеющий опыт работы со WSUS, наверняка, спросит: а в чем же, собственно, проблема? В Microsoft всё предусмотрели: существует механизм репликации, при котором «нижестоящий» (подчиненный) сервер, называемый «репликой», скачивает как сами файлы обновлений, так и информацию о том, какие апдейты были одобрены для использования в тех или иных группах.
Действительно, это так. Однако в рассматриваемом случае использовать штатный механизм не получилось, т.к. для его работы требуется достаточно широкий канал между корневым сервером и его репликами. У меня такого канала не было. Зато было указание — ни при каких обстоятельствах не увеличивать расходы на ИТ.
Идея
«Нет, так нет!» — подумал я и начал прикидывать, как можно выкрутиться из этой ситуации. Во-первых, каждый из серверов WSUS так или иначе имел доступ либо к серверам WindowsUpdate от MS, либо к локальным серверам WSUS провайдеров, либо к серверам WSUS соседей по офису и т.д. Т.е. им было, откуда скачивать файлы обновлений, требовалось только указать, какие именно обновления требуется загрузить. Во-вторых, мне разрешили использовать Powershell.
Задумка была проста: необходимо реализовать механизм импорта / экспорта данных о том, какие обновления были одобрены. По старой традиции, сформировавшейся еще во времена обучения в университете, экспортировать я решил в простой XML-файл. Структура этого файла получилась примерно такой:
<Groups>
<!-- Имя группы компьютеров, к которой относятся указанные ниже обновления -->
<gWSUS_wks_test>
<!-- Первое обновление (Update) -->
<Update>
<!-- Продукт, к которому относится данное обновление -->
<UpdateProduct>Windows XP</UpdateProduct>
<!-- Номер статьи, посвященной данному обновлению, в базе знаний MS -->
<UpdateKB>815021</UpdateKB>
<!-- Тестовое описание обновления -->
<UpdateDescription>An identified security vulnerability in Microsoft® Windows NT® 4.0, Windows 2000, and Windows XP could allow an attacker to take control of the computer. This issue is most likely to affect computers used as web servers. You can help protect your computer from this and other identified issues by installing this update from Microsoft. After you install this item, you may have to restart your computer.</UpdateDescription>
<!-- Уникальный идентификатор обновления -->
<UpdateID>4aed463b-3a3b-4ad8-976e-17baf6434da2</UpdateID>
<!-- Было ли обновление отменено -->
<UpdateIsDeclined>False</UpdateIsDeclined>
<!-- Может ли клиент отказаться от этого обновления -->
<UpdateApprovalIsOptional>False</UpdateApprovalIsOptional>
<!-- Дата окончания установки (после её наступления установка этого обновления выполняться не будет) -->
<UpdateApprovalDeadLine>12/31/9999 23:59:59</UpdateApprovalDeadLine>
<!-- Действие, которое необходимо одобрить (в данном случае - установка) -->
<UpdateApprovalAction>Install</UpdateApprovalAction>
</Update>
</gWSUS_wks_test>
</Groups>
Сценарий использования означенного механизма можно представить следующим образом:
- Администратор (или его доверенный помощник) в своей сети одобряет некоторые обновления для использования в определенных группах, убеждается, что всё хорошо, и выгружает файл с информацией о том, какие из них нужно применить к тем или иным группам компьютеров, например, на внутренний WEB-сайт;
- Эникейщик на месте (я, например) настраивает скрипт, который скачивает сформированный администатором в п.1 файл и загружает его содержимое в локальную базу WSUS;
- При наличии нескольких несвязанных серверов WSUS в зоне ответственности эникейщика — п.2 выполняется несколько раз
Что это даёт в итоге? Во-первых, эникейщик не принимает решение о том, какие обновления одобрить, а какие — нет: это решение принимает квалифицированный системный администратор, способный обоснованно спрогнозировать влияние каждого обновления на используемые в работе кривые оригинальные разработки, по всей видимости, сильно завязанные на конкретные глюки недокументированные возможности ОС, SQL-сервера и пакета MS Office.
Во-вторых, эникейщик не может ошибиться (забыть одобрить нужное обновление, случайно одобрить ненужное и т.п.)
В-третьих, администратор получает гарантию того, что во всех сетях компании одобрены к установке одни и те же обновления (т.е. вопрос «А какая версия офиса используется у нас в Замкадске?» теряет актуальность).
Кроме экспорта одобрений требовался механизм переноса прошедших испытания в тестовых группах обновлений на «боевые». Желательно, одной кнопкой / командой — «одобрить всё, что не запрещено и одобрено для использования в тестовой группе». Это нужно для того, чтобы исключить попадание непротестированных обновлений в продакшн в следствие ошибки исполнителя («ой, не то одобрил, не туда нажал!»).
Реализация
Код
Потратив некоторое время на поиски готового решения, и не найдя такового, я углубился в изучение документации по WSUS API, Powershell и .NET.
Сначала пришлось разобраться, как вообще получить доступ к объектам, позволяющим манипулировать сервером WSUS. Для этого нужно загрузить соответствующую сборку:
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")|out-null
После этого — создать функцию, которая возвращала бы объект, представляющий сервер WSUS, установленный локально:
<#
.SYNOPSIS
Возвращает экземпляр объекта - сервера WSUS
.DESCRIPTION
Для создания объекта используется локальный (тот, на котором запущен сценарий) сервер WSUS
.OUTPUTS
Объект типа IUpdateServer, представляющий локальный сервер WSUS
#>
Function Get-WsusServerInstance()
{
#Глобальная переменная $Script:WSUS используется для "кэширования" результата.
$WSUSServer = $Script:WSUS
#Получаем ссылку на текущий экземпляр сервера WSUS.
if ($WSUSServer -eq $null)
{
$WSUSServer =[Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer();
}
Return $WSUSServer
}
Поскольку постановка задачи предполагает работу с группами WSUS, необходимо научиться получать их представление. Я использовал примерно такой код:
<#
.SYNOPSIS
Возвращает объект - группу компьютеров WSUS по её имени.
.PARAMETER Name
Имя группы WSUS, представление которой нужно получить.
.OUTPUTS
Интерфейс IComputerTargetGroup, представляющий указанную группу WSUS, если группа найдена. $null - в противном случае.
#>
Function Get-WsusGroupByName([STRING]$Name)
{
$wsus = $null
$wsus = Get-WsusServerInstance
$Groups = $wsus.GetComputerTargetGroups()
Foreach ($Group in $Groups)
{
if ($Group.Name -eq $Name)
{
Return $Group
}
}
Return $null
}
Теперь, когда в нашем распоряжении есть механизм доступа к представлению группы WSUS, несложно получить список обновлений, одобренных для заданной группы:
<#
.SYNOPSIS
Возвращает массив обновлений, одобренных для заданной группы.
.DESCRIPTION
Возвращает массив объектов, представляющих обновления,
заданные для указанной группы. Каждый объект содержит сведения об обновлении (ID)
и его подтверждении.
.PARAMETER GroupName
Имя группы, для которой нужно получить список одобрений
.OUTPUTS
Массив объектов. Каждый объект содержит два поля: objUpdate (обновление) и objApproval (результат одобрения)
#>
Function Get-UpdatesApprovedToGroup([STRING]$GroupName)
{
#Обнуляем результирующую переменную
$ApprovedUpdates = @()
#Создаём объект, представляющий сервер WSUS
$wsus = Get-WsusServerInstance
#Получаем представление заданной группы
$Group = Get-WsusGroupByName -Name $GroupName
Write-Host "Querying updates info from WSUS server..."
#Получаем информацию обо всех обновлениях, известных данному серверу
$WsusUpdates = $wsus.GetUpdates()
$TotalWsusUpdates = $WsusUpdates.Count
$i = 0
#Перебираем обновления в цикле, определяя для каждого из них отношение к указанной группе компьютеров
foreach ($Update in $WsusUpdates)
{
$i++
#KBID (номер статьи в БЗ MS) обновления
[STRING]$UpdateKB = $Update.KnowledgebaseArticles
Write-Host "Gathering approvals for update. Last porcessed is KB$UpdateKB"
#Проверяем, было ли принято решение об одобрении текущего обновления для использования в заданной группе.
$CurrentApprovals = $update.GetUpdateApprovals($group)
#Если по обновлению было принято решение (одобрение, запрет), создаем соответствующий
#объект и добавляем его в результирующий список.
if ($CurrentApprovals.Count -gt 0)
{
foreach ($Approval in $CurrentApprovals)
{
#Создаем объект, инкапсулирующий свойства обновления и его подтверждения.
$Record = New-Object Object
Add-Member -InputObject $Record -MemberType NoteProperty -Value $Update -Name "objUpdate" | Out-Null
Add-Member -InputObject $Record -MemberType NoteProperty -Value $Approval -Name "objApproval" | Out-Null
$ApprovedUpdates = $ApprovedUpdates + $Record
}
}
}
Return $ApprovedUpdates
}
Полученные в результате работы этой функции сведения можно сохранить в файл, а потом импортировать на другом сервере WSUS, например, с помощью такой функции:
<#
.SYNOPSIS
Импортирует данные об одобрениях обновлений WSUS из файла
.DESCRIPTION
Импортируются не только сведения об одобрениях (Approval), но и сведения о запретах (Decline)
.PARAMETER FilePath
XML-файл, в котором содержатся сведения об одобрениях обновлений WSUS.
.OUTPUTS
$null (данная функция не возвращает значения).
#>
Function Import-Approvals([STRING]$FilePath)
{
#Получаем представление локального сервера WSUS
$WSUS = Get-WsusServerInstance
#Получаем список групп WSUS, информация о которых содержится в файле.
$XML = [xml](get-content $FilePath)
$Groups = $XML.Groups
$Groups = $Groups.ChildNodes
#Для каждой группы просматриваем список одобрений в файле
Foreach ($Group in $Groups)
{
#Пропускаем комментарии XML
if ($Group.get_NodeType() -ne $XML_TYPE_COMMENT)
{
$GroupName = $Group.Name
Write-Host "Importing updates for WSUS group: $GroupName"
#Создаём представление группы
$objGroup = Get-WsusGroupByName -Name $GroupName
#Перебираем в цикле все обновления, указанные в файле для данной группы
$Updates = $Group.ChildNodes
$TotalUpdates = $Updates.Count
$i = 0
Foreach ($Update in $Updates)
{
$i++
$UpdateGUID = $Update.UpdateID
if ($UpdateGUID -ne "")
{
$UpdateKB = $Update.UpdateKB
Write-Host "Importing updates for $GroupName. Last processed is KB$UpdateKB"
#Находим Update с таким ID на сервере:
$UpdateGUID = [System.Guid]$UpdateGUID
$objUpdateID = [Microsoft.UpdateServices.Administration.UpdateRevisionId]$UpdateGUID
$objUpdate = $WSUS.GetUpdate($objUpdateID)
#Получаем свойства обновления и его одобрения из XML-ки, преобразовываем все это к нужным типам
[DateTime]$UpdateApprovalDeadline = Get-XmlValue -XML $Update.UpdateApprovalDeadLine
[Microsoft.UpdateServices.Administration.UpdateApprovalAction]$UpdateApprovalAction = Get-XmlValue -XML $Update.UpdateApprovalAction
if ($Update.UpdateIsDeclined -eq "true")
{
$UpdateIsDeclined = $true
} else
{
$UpdateIsDeclined = $false
}
#Подтверждаем обновление
$objUpdate.Approve($UpdateApprovalAction,$objGroup,$UpdateApprovalDeadline) | Out-Null
#Ставим статус "Отменен" в соответствие с тем, что указано в файле
$objUpdate.IsDeclined = $UpdateIsDeclined
}
}
}
Write-host -Message "Import finished for group: $GroupName"
}
}
В общем-то, всё довольно просто. Экспорт осуществляется аналогично импорту (желающие могут найти соответствующую функцию в полном тексте сценария).
Интерфейс пользователя
Так как сценарием должен был пользоваться не только я, но и мои коллеги — такие же эникейщики в других регионах, было решено снабдить его графическим интерфейсом пользователя:
Я постарался соблюсти минимализм в облике основного окна и избегал включения параметров работы сценария в интерфейс, только хардкор хардкод. Поскольку WPF установлен далеко не на всех компьютерах, на которых используется это сценарий, графический интерфейс реализован с использованием System.Windows.Forms, а не более подходящего для таких задач XAML.
Разумеется, решение на Powershell, созданное для автоматизации рутинных операций, должно иметь интерфейс командной строки, иначе пользователь не сможет задействовать его в своих скриптах (или хотя бы запускать по расписанию).
Описываемый сценарий принимает следующие параметры:
- DoImportFromFile — Путь к файлу, из которого нужно импортировать информацию об одобрениях. Если этот параметр пуст, импорт не происходит;
- DoExportToFile — Путь к файлу, в который следует выгрузить информацию об одобрениях. Аналогично, при отсутствии значения этого параметра, экспорт не выполняется;
- ServersCopyTestApprovals — Одобрить все обновления, разрешенные для использования в тестовой группе серверов, для использования в на «боевых» серверах (см. далее);
- WorkstationsCopyTestApprovals — Аналогичен предыдущему, но для рабочих станций;
- PathToLog — Путь к файлу журнала (по умолчанию создается во временной папке "%Temp%")
Мероприятия по внедрению
Для того, чтобы успешно использовать описываемый сценарий, пришлось выполнить ряд подготовительных мероприятий.
Первоначально в каждой сети была выделена, по меньшей мере, одна тестовая рабочая станция и хотя бы один тестовый сервер. По возможности, мы старались включать в тестовые группы некритичные для основного бизнеса узлы. Прежде, чем обновления будут одобрены для использования по всей сети, они несколько дней (на усмотрение системного администратора) проверяются на тестовых компьютерах. Те обновления, которые вызывают какие-либо проблемы, исключаются (decline), остальные, по окончании тестирования, одобряются для использования в боевых группах. Для копирования одобрений используется описываемый сценарий.
На всех серверах WSUS, где предполагалось использовать описываемый сценарий, были созданы следующие группы компьютеров:
- gWSUS_srv_test — тестовые серверы;
- gWSUS_wks_test — тестовые рабочие станции;
- gWSUS_srv_prod — «боевые» серверы;
- gWSUS_wks_prod — критичные для бизнеса рабочие станции;
Имена групп жестко закодированы в сценарии и не могут быть изменены без его правки. На каждом сервере WSUS был настроен запуск скрипта в режиме копирования одобрений с тестовых групп на «боевые» по расписанию раз в две недели (было решено, что такой промежуток времени позволит, с одной стороны, успеть исключить «проблемные» обновления из списка а, с другой, не будет слишком сильно откладывать доставку апдейтов на компьютеры).
Раз в несколько дней (на усмотрение системного администратора) специально назначенный эникейщик в тестовой сети выполняет одобрение новых (еще не одобренных и не отмененных) обновлений. После получения разрешения от системного администратора он выгружает список одобрений в виде XML-файла на специальный сайт внутри сети.
Раз в сутки на каждом сервере WSUS выполняется простенький батник, скачивающий текущую версию списка обновлений с этого сайта и выполняющий импорт одобрений:
REM Переходим в папку со скриптом
cd /d d:WSUS_script
REM Удаляем устаревший файл одобрений
del /f .WSUS_updates_approvals.waf
REM Скачиваем новый файл одобрений
wget --tries=100 --retry-connrefused --continue http://server.network.lan/wsus_approvals/WSUS_updates_approvals.waf
REM Импортируем одобрения из файла
powershell -command -ExecutionPolicy Bypass ".wsus_admin_tool.ps1 -DoImportFromFile 'c:WSUS_updates_approvals.waf'"
Заключение
Разумеется, описанное решение не лишено определённых недостатков: сценарий не обеспечивает проверку востребованности загружаемых одобрений (а нужно ли одобрение сервиспака для SQL в сети, если этой СУБД там нет?), сценарий требует наличия доступа к вышестоящему серверу WSUS (или серверу WindowsUpdate), да и сам код, наверняка, далёк от идеала.
Однако поставленная задача была успешно решена, а практика, как известно, — критерий истины. Надеюсь, описанное решение пригодится еще кому-нибудь.
P.s. Полный код сценария можно доступен на PasteBin.
Автор: hdablin