Вспоминаем Powershell в нескольких частях. Часть 3. Обрабатываем, что получилось
Для ЛЛ: серия пометок по костылям
Вместо предисловия.
Недавно от бабки в поликлинике узнала, что творог опасен. «У знакомой внук дураком сделался через него. Его творогом кормили, от кальция родничок рано зарос, а мозг продолжал расти, и теперь он в ынторнэтах сидит, кнопки нажимает».
Тут много таких, творогом покалеченных
Конечная цель серии постов: написать свой очень маленький и очень кривой WSUS, поскольку развития WSUS больше не предвидится, но я про это писал
Часть 1. База из баз. Теория
Часть 1.1 Зачем ставить обновления на Linux и Windows и куда угодно, если в отделе работают проверенные электроником сотрудники, и все работает?
Часть 1.2 Чем плох WSUS, SCOM, прочее ПО, и факты в Ansible?
Часть 1.3 Почему Powershell, а не Python?
Часть 1.4 Прочие базовые вещи
Часть 1.5 Классы и объекты, для тех, кто пропускал школу
Часть 1.6 К теме обновлений в Windows
Часть 1.7 Как это все хранить и обрабатывать?
Часть 1.8 Давайте начинать. Мой первый класс
Часть 1.9 Немного магии, не очевидной с первого раза
Часть 1.10 Мой первый массив
Часть 1.11 Суй массив в файл. И забирай из файла
Часть 1.12 Теперь все вместе
Часть 2. Windows update
Часть 2.1 Служба обновлений и ее журнал
Часть 2.2 Настраиваем удаленный доступ
Часть 2.3 Ловим исключения
Часть 2.4 Проблема слишком больших прав
Часть 2.5 Разрешение удаленного подключения
Часть 2.6 Параллельная обработка задач, -parallel, powershell jobs, Runspaces
Часть 2.7 И, наконец, получим первый список
Часть 2.8 Итого
Часть 3. Обрабатываем, что получилось
Часть 3.1 Немного про общую логику
Часть 3.2 В предыдущих сериях
Часть 3.3 Обновление списка обновлений
Часть 3.4 Обрабатываем оба списка сразу – список обновлений и список с сервера
Часть 3.5 Осталось только выгрузить в Excel
Часть 3.6 Отладка и наладка
Часть 3.7 Альтернативы?
Часть 3. Обрабатываем, что получилось
Часть 3.1 Немного про общую логику
Есть школьная логика, когда люди считают, что «нам нужен список всех обновлений, чтобы понять, какое обновление установлено! 111
Есть логика «ближе к Agile», когда нам не важно, какое обновление установлено, а важно, установлено ли последнее обновление, максимум предпоследнее.
В чем разница? В случае «школьной логики» последние лет 50 наверное, может больше, я в школе был травмирован абсолютно угашенной на голову преподавательницей химии, которой надо было не решение, правильное или неправильное, а соблюдение ее личной методологии решения. Абсолютно дурная бабка была.
Логика в моем случае в том, чтобы сначала получить MVP, minimum viable product, получить данные «как есть», и уже потом развивать код, добавляя в выгрузку, или в справку нужные мне данные.
Проблемы написать парсер Windows update нет никакой, может уже даже кем-то написан. Но мне не нужен полный список для решения моей частной задачи.
Задача в первом приближении написана не оптимально с точки зрения кода, объемов данных итд. Но опять же, мне не нужно решать глобальную задачу и делать комбайн с вертикальным взлетом, мне траву во дворе скосить, и скосить «сейчас».
Про это будет еще много отсылок в тексте.
Использование функций.
Использование функций дело, безусловно, полезное и правильное, с точки зрения читаемости кода. Проблема с Powershell в том, что в нем обработка ошибок в функции весьма странная и неудобная с непривычки. Поэтому функции я, конечно, использую, но в этом примере их будет меньше, чем стоило бы.
Например, получение списка обновлений NewKeinArray можно было спокойно вынести в функцию с параметром, или без.
Чистка кода от старого и закомментированного кода.
Для читаемости кода, конечно, было бы неплохо вычищать старый код.
Для прослеживания логики роста кода, чтобы спустя месяц было понятно, что было сделано и почему так было сделано, закомментированный код можно оставлять. Оба варианта сгодятся. Поскольку это учебная статья, то закомментированный код будет оставлен, для примеров и чтобы на него ссылаться.
Часть 3.2 В предыдущих сериях
В первой серии мы получили:
1) Файл nur-eine-datei.xml c массивом( System.Array) из класса MeineErsteKlasse и генератор (mainefirstclass4pikabu .ps1) для создания этого файла.
Почему эта функция вынесена? Потому что генерация – это очень простой механизм, и его имеет смысл сделать отдельно. Можно ли сделать эту задачу как функцию в основном скрипте? Можно, но зачем? Это простая генерация файла со списком. Ниже попробую пояснить, почему именно так удобно именно мне.
2) Файл dieparole.txt с логином и паролем для удаленного сервера. Так делать не надо, хранение логинов и паролей в тексте – плохая практика. Нормальные люди разворачивают Vault.
3) Некорректно настроенный, так делать не надо, но для демонстрации сойдет, сервер с Windows. WinRM надо грамотно настраивать для безопасной работы!
4) Выгруженный список обновлений с этого сервера.
5) И код из части 2.
Что сделано не очень удобно, и, может, не очень правильно? Файл nur-eine-datei.xml и оба скрипта должны лежать в одной папке, и называться одинаково в обоих исполняемых файлах. Можно было сделать запуск с параметром «путь к файлу», но, на мой взгляд, это только усложнит использование.
Первым делом переформируем список серверов для проверки.
В примере (2) есть строка
$MeineServerliste = @("192.168.211.150","192.168.211.151")
Первый сервер из нее доступен, второй – не существует.
В примере (2) есть еще одна ошибка, но я ее исправлю потом.
В достаточно большой инфраструктуре мы, зачастую, не знаем – доступен ли сервер, не доступен, но нам нужна общая картина – всего, из них доступно, из них не доступно.
Можно оптимизировать задачу как «сделать только список что доступно и что на них», можно оптимизировать задачу «все числится, из них не доступно». Это влияет на исполняемую логику в коде. Мне удобнее видеть ситуацию в виде общей таблицы.
Часть 3.3 Обновление списка обновлений
Перепишем первый скрипт, формирующий список обновлений, в следующем виде:
Class Updates{
[string]$OS
[string]$UpdateType
[string]$KB
[string]$KBDate
[string]$Other}<#Для какого продукта он предназначен: Microsoft Server Operating System
Для какой версии продукта он предназначен: 24H2 (это Windows server 2025)
Номер: KB article numbers: 5063878
Дата выхода: 8/12/2025 #>
$Skriptversion = "12 from 05.09.2025"$Ar2 = @()
$UPD =[Updates]::new()
$UPD.OS = "Server 2025 24H2" ; $UPD.UpdateType = "OS" ; $UPD.KB = "KB5063878" ; $UPD.KBDate = "12.08.2025" ; $Ar2 += $UPD ;
# $MeinErstklassigesBeispiel | Format-Table –AutoSize
$PfadZurSpeicherdatenbank = $PSScriptRoot
$MeineErsteSicherungsdatei = $PfadZurSpeicherdatenbank + "\" + "nur-eine-datei-part12.xml"
Export-Clixml -Path $MeineErsteSicherungsdatei -InputObject $Ar2# notepad $MeineErsteSicherungsdatei
# $NewKeinArray = Import-Clixml -Path $MeineErsteSicherungsdatei
# $NewKeinArray
Зачем? Во первых, нам не нужны проверки «через блокнот», сделанные для демонстрации «что там внутри». Во вторых, чтобы строка
$UPD.OS = "Server 2025 24H2" ; $UPD.UpdateType = "OS"
помещалась на экран.
Зачем так ?
Допустим, вы по каким-то причинам не перешли с сервера 2012R2 на сервер 2025, и вам нужно следить не только за тем, чтобы было или не было обновление на сервере 2025, но и за 2012.
При этом, поскольку подписки на расширенные обновления для 2012R2 у вас нет, то последнее обновление у вас будет от 10 октября 2023 года.
2023-10 Security Monthly Quality Rollup for Windows Server 2012 R2 for x64-based Systems (KB5031419)
Хотя, если бы вам на самом деле была нужна безопасность, то вы бы оформили подписку на обновления, и получали
2023-11 Security Monthly Quality Rollup for Windows Server 2012 R2 for x64-based Systems (KB5032249)
2023-12 Security Monthly Quality Rollup for Windows Server 2012 R2 for x64-based Systems (KB5033420)
И так далее, вплоть до
2025-08 Security Monthly Quality Rollup for Windows Server 2012 R2 for x64-based Systems (KB5063950)
2025-08 Security Monthly Quality Rollup for Windows Server 2012 for x64-based Systems (KB5063906)
В таком случае достаточно дописать одну строку в код:
$UPD.OS = "Server 2012 R200" ; $UPD.UpdateType = "OS" ; $UPD.KB = "KB5031419" ; $UPD.KBDate = "10.2023" ; $Ar2 += $UPD ;
Читаемость сохраняется, думать не надо.
Да, конечно «правильнее в вакууме» было бы один раз взять и потратить два дня и распарсить весь список обновлений, или поискать готовый список, вдруг кто-то найдет.
Для рабочей задачи, которую я решаю, достаточно ручного добавления одной строки. Раз в месяц, может быть два раза в месяц, в случае выхода внепланового обновления.
В коде выше не специально, но была допущена ошибка. Найти ее и устранить предлагается самостоятельно.
Часть 3.4 Обрабатываем оба списка сразу – список обновлений и список с сервера
В переменной $DataRemote1 есть массив обновлений, а в $MeineServerliste[0] – имя сервера. Окей, и что с этим делать? Очевидно, создать новый класс, но сначала сделать то, чего делать нельзя.
Мне, не знаю как вам, не нужно обрабатывать весь список обновлений. Обновления в Windows последних (2016, 19, 22, 25) бывают:
Обновление стека обслуживания, например servicing stack update (KB5063666)
Кумулятивное плановое обновление, например 2025-08 Cumulative Update for Microsoft server operating system version 24H2 for x64-based Systems (KB5063878) (26100.4946)
Внеплановое обновление
Обновление dotnet, например
2025-07 Cumulative Update for .NET Framework 3.5 and 4.8.1 for Microsoft server operating system version 24H2 for arm64 (KB5056579)
Какие-то еще внеплановые обновление, типа обновления Edge и чего то там еще. Всякая мелочь.
Так нужно ли обрабатывать все 10-20-50 обновлений в списке установленных? Конечно, нет.
Поэтому:
$DataRemote2 = $DataRemote1 | Sort-Object -Property InstalledOn -Descending | Select-Object -First 4
Вот теперь заводим новый класс!
Class CurrentState{
[string]$Name
[string]$IP
[string]$IsActive
[string]$CurrentUpdateKB
[string]$CurrentUpdateKBDate
[string]$Other
[string]$LastInstalledKBFotTroubleshooting}Remove-Variable TotalList -ErrorAction SilentlyContinue
$TotalList = @()foreach ($ThisUpdate in $DataRemote2) {
$ThisServer = [CurrentState]::new()
$LastInstalledKBFotTroubleshooting = ""
foreach ($ThisUpdate2 in $DataRemote2){
$LastInstalledKBFotTroubleshooting = $LastInstalledKBFotTroubleshooting + $ThisUpdate2.HotFixID +'.'}$ThisServer.LastInstalledKBFotTroubleshooting = $LastInstalledKBFotTroubleshooting
foreach ($BaseOfUpdate in $NewKeinArray) {
$ThisServer.IP = $MeineServerliste[0]
Write-Host "This |" $ThisUpdate.HotFixID " Base " $BaseOfUpdate.KB
if ($ThisUpdate.HotFixID -eq $BaseOfUpdate.KB) {
$ThisServer.CurrentUpdateKB = $ThisUpdate.HotFixID
$ThisServer.CurrentUpdateKBDate = $BaseOfUpdate.KBDate
$TotalList += $ThisServer
#break
}}Remove-Variable LastInstalledKBFotTroubleshooting
Remove-Variable ThisServer }
Часть 3.5 Осталось только выгрузить в Excel
В Powershell «из коробки» нет выгрузки в Excel. Есть txt, csv, xml. Внешний модуль для Excel, конечно, есть, но использовать его просто не хочется.
Однако, если вы сделаете
$CSVfile1 = $PfadZurSpeicherdatenbank + "\" + "MyExport1.csv"
Export-Csv -InputObject $TotalList -Path $CSVfile1
notepad $CSVfile1
То получите совсем не то, чего хотели, а что-то про #TYPE System.Object[]
Это совсем, совсем не то, что вам надо. Для решения этой проблемы есть несколько вариантов, самый читаемый, на мой взгляд,
$CSVfile2 = $PfadZurSpeicherdatenbank + "\" + "MyExport2.csv"
$CSVfile3 = $PfadZurSpeicherdatenbank + "\" + "MyExport3.csv"
$TotalList | Select * | Export-Csv -Path $CSVfile2 -Delimiter ";"
$TotalList | Select Ip, LastInstalledKBFotTroubleshooting | Export-Csv -Path $CSVfile3 -Delimiter ";"
notepad $CSVfile2
notepad $CSVfile3
Часть 3.6 Отладка и наладка
Что же делать, как же быть, если у вас поле CurrentUpdateKB – пустое? Не оказалось у вас установленного последнего обновления?
На мой взгляд, решение очевидно, нужно идти и ставить свежее обновление.
Но, если у вас другое мнение, то
Поле LastInstalledKBFotTroubleshooting у вас есть, Microsoft Update catalog доступен, дальше надо пояснять?
Часть 3.7 Альтернативы?
Пока обсуждал черновик с коллегами, выяснил что можно было решить эту задачу, инвентаризации, и попроще. Или иначе.
Можно было сделать, через, например, групповую политику, разовую задачу по выгрузке того, что мне нужно, в общую папку, в сотню – две файлов с разными именами. Потом уже гораздо проще решить задачу выборки из файлов.
Можно было выдернуть через Get-SilData, тоже вариант.
Можно было запросить SQL базу на WSUS, если она хоть как-то жива. Там очень простой запрос.