PowerShell 2.0 + WinRM: собираем отчёт по дискам со 200 серверов и чистим AD от уволенных
Как PowerShell Remoting с WinRM переводит рутинные Windows-задачи в скрипты: дисковый отчёт с сотен серверов за полминуты и автоматическая деактивация в AD.
PowerShell 2.0 с поддержкой Remoting через WinRM вошёл в стандартную поставку Windows Server 2008 R2 и стал основным инструментом массовой автоматизации Windows-инфраструктуры
Несколько месяцев назад мы начали системно переводить рутинные Windows-задачи с «зайти на сервер руками» на PowerShell с Remoting. Теперь есть что рассказать - не в духе «смотрите как круто», а конкретно: что работает, где споткнулись и что пока вызывает сомнения.
Почему именно сейчас
PowerShell 2.0 вышел ещё с Windows 7 и Server 2008 R2, WinRM (Windows Remote Management) шёл в комплекте. Но реально использовать Remoting в парке из сотен серверов мы начали только в этом году - после того как убедились, что WinRM включается через GPO без плясок и что Invoke-Command с параметром -ComputerName и списком хостов работает надёжно, а не через раз.
До этого у нас был зоопарк: VBScript через WMI, PSExec для разовых задач, кое-где самописные bat-файлы с net user и dsquery. Всё это работало, но плохо масштабировалось и не давало нормального вывода для дальнейшей обработки.
Задача 1: дисковое пространство с 200 серверов
Типичная ситуация у нескольких наших клиентов - мониторинг есть (Zabbix), но бывают серверы, которые в него не попали или попали без агента. Нужен быстрый adhoc-отчёт: где диски заполнены больше порога, отсортировать по свободному месту.
Раньше это делалось через веб-интерфейс vCenter или руками - кто-то шёл по списку и смотрел. Теперь:
$servers = Get-Content .\servers.txt
$result = Invoke-Command -ComputerName $servers -ThrottleLimit 50 -ScriptBlock {
Get-WmiObject Win32_LogicalDisk -Filter "DriveType=3" |
Select-Object DeviceID,
@{N='SizeGB'; E={[math]::Round($_.Size/1GB,1)}},
@{N='FreeGB'; E={[math]::Round($_.FreeSpace/1GB,1)}},
@{N='FreePct'; E={[math]::Round($_.FreeSpace/$_.Size*100,0)}},
PSComputerName
} -ErrorAction SilentlyContinue
$result | Sort-Object FreePct |
Where-Object FreePct -lt 20 |
Export-Csv .\disk-report.csv -NoTypeInformation -Encoding UTF8
-ThrottleLimit 50 - это ключевой параметр. По умолчанию PowerShell пытается открыть соединения со всеми хостами разом; при 200 серверах это даёт перегрузку и таймауты. С лимитом 50 параллельных сессий весь прогон занимает около 30 секунд, что принципиально лучше последовательного обхода.
-ErrorAction SilentlyContinue в данном случае осознанный выбор: если сервер недоступен, он просто не попадёт в отчёт, и мы увидим это по отсутствию строк. Для инвентаризации нам важны результаты с тех, кто откликнулся - а не остановка на первом же недоступном хосте.
WinRM на Windows Server 2008 R2 включается через GPO, что важно: не нужно обходить серверы руками. Настроили один раз политику Allow remote server management through WinRM, применили к OU серверов - и Remoting заработал на всём парке.
Задача 2: деактивация уволенных в AD
У нескольких клиентов процесс увольнения выглядел примерно одинаково: HR сообщает в IT, кто-то вспоминает отключить учётку, иногда через день, иногда через неделю. Иногда не вспоминает. После разговора про 152-ФЗ решили это автоматизировать.
Схема простая: в Active Directory есть атрибут extensionAttribute1, который HR-система заполняет датой увольнения при проведении приказа. Скрипт раз в ночь забирает всех пользователей, у кого эта дата прошла, и деактивирует учётку.
Import-Module ActiveDirectory
$today = Get-Date
$fired = Get-ADUser -Filter {
extensionAttribute1 -like "*" -and Enabled -eq $true
} -Properties extensionAttribute1, DisplayName, Department |
Where-Object {
$_.extensionAttribute1 -and
([datetime]::TryParse($_.extensionAttribute1, [ref]$null)) -and
([datetime]$_.extensionAttribute1 -le $today)
}
foreach ($user in $fired) {
Disable-ADAccount -Identity $user
Move-ADObject -Identity $user.DistinguishedName `
-TargetPath "OU=Disabled,DC=corp,DC=local"
Add-ADGroupMember -Identity "BlockedUsers" -Members $user
Write-Output "$(Get-Date -f 'yyyy-MM-dd HH:mm') | DISABLED | $($user.SamAccountName) | $($user.Department)"
}
Вывод пишется в лог-файл, который еженедельно смотрит служба безопасности. Учётка не удаляется - только переводится в отдельную OU и отключается. Удаление руками через месяц, после проверки, что почта и файлы переданы.
Парсинг даты через TryParse - потому что HR-система пишет атрибут в своём формате, и иногда туда попадает мусор. Лучше пропустить сомнительную запись, чем упасть с ошибкой в середине ночного прогона.
Что выяснилось в процессе
Несколько наблюдений, которые в документации не написаны.
Первое - WinRM и двойной хоп. Если со скрипт-сервера подключаешься к хосту A, а оттуда нужно подключиться к ресурсу B (файлопомойка, SQL), - не работает. Kerberos не умеет делегировать учётные данные через второй прыжок без явной настройки CredSSP или Kerberos constrained delegation. Мы на это наткнулись, когда попробовали через Remoting подключиться к серверу и оттуда смонтировать сетевую шару. Решение - либо CredSSP (с пониманием рисков), либо переписать скрипт так, чтобы работа с сетевым ресурсом шла с управляющего сервера, а не с удалённого хоста.
Второе - кодировки в выводе. Invoke-Command возвращает объекты PSObject, и кириллица в свойствах живёт нормально, пока не делаешь Export-Csv. Без -Encoding UTF8 получаешь кашу в Excel. Добавили в шаблон скриптов как стандарт.
Третье - $ErrorActionPreference на удалённой стороне. Переменная не передаётся в ScriptBlock автоматически. Если нужно гасить ошибки внутри блока - нужно явно выставлять там. Неочевидно, но один раз поймали на этом.
Где мы сейчас
Для клиентов на сопровождении инфраструктуры PowerShell + WinRM закрывает добрую половину задач, которые раньше требовали либо rdp-сессии, либо отдельного агента. Инвентаризация, дисковые отчёты, проверка сервисов, работа с AD - всё это теперь скрипты с расписанием.
Открытый вопрос - управление самими скриптами. Сейчас они лежат на одном сервере в папке с контролем версий через xcopy-бэкапы, что честно говоря не очень. Смотрим в сторону нормального git-репозитория и централизованного запуска, но пока не решили как именно.