
Привет, Хабр! Типичная ситуация: начальство требует полный отчёт по всему парку техники или только устроились в организацию с крупным парком машин, нужно сформировать понимание, с чем вы работаете. Вручную обходить 100+ рабочих мест - совсем не вариант.
В статье я поделюсь PowerShell-скриптом, который:
Сам обойдет все машины в сети
Соберёт подробную информацию о конфигурации
Сгенерирует удобные для восприятия HTML-отчёты
Складирует всё в вашу сетевую папку для удобства
Скрипт будет полезен системным администраторам, инженерам ТП, аудиторам и тем, кто устал вручную обходить все машины в организации.
Возможности скрипта
Скрипт собирает комплексную информацию о системе:
Информация об операционной системе (версия, архитектура, время работы)
Данные о BIOS и производителе оборудования Аппаратное обеспечение:
Процессор (модель, ядра/потоки, загрузка)
Оперативная память (объем, модули, использование)
Графические процессоры (модель, память, драйверы)
Накопители (модель, разделы, свободное пространство) Сетевые адаптеры:
Активные сетевые адаптеры
IP и MAC-адреса
Скорость соединения Доп информация:
Версия PowerShell
Логические диски и процент их заполнения
Назначение скрипта
Скрипт выполняет следующие задачи:
Собирает основную конфигурацию с удалённых машин
Сохраняет данные в структурированном HTML-формате
Централизованно хранит отчёты в вашей сетевой папке
Автоматически удаляет устаревшие отчёты
Логирует все этапы работы
Архитектура решения
Скрипт имеет следующую структуру:
Контроллер - основной скрипт, который запускается на машине админа
Агенты - функции, выполняемые на удалённых компьютерах
Детальный разбор компонентов
Я разберу лишь основные функции скрипта, тк нагружать вас во многом бесполезным кодом - нет смысла. Полный скрипт:
Скрытый текст
$centralLogsFolder = "\\сервер\общая_папка\Logs" $maxReportsPerComputer = 5 $logFile = Join-Path $centralLogsFolder "InventoryScript_$(Get-Date -Format 'yyyyMMdd').log" # Функция для логирования function Write-Log { param( [string]$message, [string]$level = "INFO" ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "[$timestamp][$level] $message" Add-Content -Path $logFile -Value $logEntry Write-Host $logEntry -ForegroundColor $(if ($level -eq "ERROR") { "Red" } elseif ($level -eq "WARNING") { "Yellow" } else { "White" }) } # Создаём основную папку для логов, если её нет if (!(Test-Path $centralLogsFolder)) { try { New-Item -ItemType Directory -Path $centralLogsFolder -Force | Out-Null Write-Log "Создана центральная папка для логов: $centralLogsFolder" } catch { Write-Log "Не удалось создать центральную папку для логов: $_" -level "ERROR" exit 1 } } # Получаем список компьютеров try { # Способ 1: Из Active Directory (раскомментируйте нужный) # $computers = Get-ADComputer -Filter * | Select-Object -ExpandProperty Name # Способ 2: Из файла $computers = Get-Content "C:\path\to\computers.txt" | Where-Object { $_ -notmatch '^\s*#' -and $_ -ne '' } # Способ 3: Вручную указать список # $computers = @("COMPUTER1", "COMPUTER2", "COMPUTER3") if (-not $computers) { Write-Log "Не найдено компьютеров для обработки" -level "WARNING" exit 1 } Write-Log "Получен список компьютеров для обработки: $($computers.Count) шт." } catch { Write-Log "Не удалось получить список компьютеров: $_" -level "ERROR" exit 1 } # Запрашиваем учётные данные $cred = Get-Credential -Message "Введите учётные данные с правами администратора на целевых компьютерах" if (-not $cred) { Write-Log "Не введены учётные данные" -level "ERROR" exit 1 } # Функция для удалённого сбора информации function Get-RemoteSystemInventory { param( [string]$computerName, [string]$outputDir, [int]$maxReports, [pscredential]$credential ) $session = $null try { # Проверяем доступность компьютера if (-not (Test-Connection -ComputerName $computerName -Count 2 -Quiet)) { Write-Log "Компьютер $computerName недоступен по сети" -level "WARNING" return } # Создаём сессию PSRemoting $sessionParams = @{ ComputerName = $computerName Credential = $credential ErrorAction = 'Stop' } # Пробуем подключиться (добавляем проверку для WinRM) try { $session = New-PSSession @sessionParams } catch { # Если WinRM отключен, пробуем через WMI Write-Log "Не удалось подключиться к $computerName через PSRemoting, пробуем WMI" -level "WARNING" $scriptBlock = { param($outputDir, $maxReports) # Встроенный код функции Get-SystemInventory (см. ниже) # ... (всё содержимое функции Get-SystemInventory) } Invoke-Command -ComputerName $computerName -Credential $credential -ScriptBlock $scriptBlock -ArgumentList $outputDir, $maxReports -ErrorAction Stop Write-Log "Успешно собраны данные с $computerName через WMI" -level "INFO" return } # Подготовка аргументов $remoteOutputDir = Join-Path $outputDir $computerName $arguments = @($remoteOutputDir, $maxReports) # Выполняем команду на удалённом компьютере $result = Invoke-Command -Session $session -ScriptBlock ${function:Get-SystemInventory} -ArgumentList $arguments -ErrorAction Stop Write-Log "Успешно собраны данные с компьютера $computerName" -level "INFO" } catch { Write-Log "Ошибка при работе с компьютером $computerName : $_" -level "ERROR" } finally { if ($session) { Remove-PSSession -Session $session -ErrorAction SilentlyContinue } } } # Основная функция сбора данных (будет выполняться на удалённых машинах) function Get-SystemInventory { param( [string]$outputDir, [int]$maxReports ) try { $computerName = $env:COMPUTERNAME $fileName = "Inventory_$(Get-Date -Format 'yyyyMMdd-HHmmss').html" $fullOutputDir = Join-Path $outputDir $computerName # Создаём папку и чистим старые отчёты if (!(Test-Path $fullOutputDir)) { New-Item -ItemType Directory -Path $fullOutputDir -Force | Out-Null } Get-ChildItem $fullOutputDir -Filter "Inventory_*.html" | Sort-Object CreationTime -Descending | Select-Object -Skip $maxReports | Remove-Item -Force -ErrorAction SilentlyContinue # Собираем основные данные $os = Get-CimInstance Win32_OperatingSystem $cpu = Get-CimInstance Win32_Processor $mem = Get-CimInstance Win32_PhysicalMemory $totalGB = [math]::Round(($mem | Measure-Object -Property Capacity -Sum).Sum /1GB, 2) # Получаем данные о загрузке CPU $cpuUsage = "Ошибка получения данных" try { $cpuUsage = (Get-Counter '\Processor(_Total)\% Processor Time' -ErrorAction Stop).CounterSamples.CookedValue.ToString("N2") } catch { try { $cpuUsage = (Get-WmiObject Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average.ToString("N2") } catch { $cpuUsage = "Ошибка получения данных" } } $gpus = Get-CimInstance Win32_VideoController $networkAdapters = Get-NetAdapter | Where-Object { $_.Status -eq "Up" } | ForEach-Object { $ipAddress = (Get-NetIPAddress -InterfaceIndex $_.ifIndex -AddressFamily IPv4).IPAddress [PSCustomObject]@{ Name = $_.Name Interface = $_.InterfaceDescription Status = $_.Status Speed = "$([math]::Round($_.Speed/1MB, 2)) Mbps" MAC = $_.MacAddress IP = $ipAddress } } $disks = Get-Disk | ForEach-Object { $partitions = Get-Partition -DiskNumber $_.DiskNumber | ForEach-Object { $vol = Get-Volume -Partition $_ $freeSpacePercent = if ($vol.Size -gt 0) { [math]::Round(($vol.SizeRemaining / $vol.Size) * 100, 2) } else { 0 } $freeSpaceClass = if ($freeSpacePercent -lt 10) { "err" } elseif ($freeSpacePercent -lt 20) { "warn" } else { "" } "<span class='$freeSpaceClass'>$($_.DriveLetter) $([math]::Round($_.Size/1GB))GB ($([math]::Round($vol.SizeRemaining/1GB))GB свободно, $freeSpacePercent%)</span>" } [PSCustomObject]@{ Model = $_.FriendlyName Size = [math]::Round($_.Size/1GB) Health = $_.HealthStatus Partitions = $partitions -join '<br>' } } # Генерируем HTML (как в вашем оригинальном скрипте) $html = @" <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Системный отчет - $computerName</title> <style> body{font-family:Segoe UI;margin:20px} h1,h2{color:#0066cc} table{width:100%;border-collapse:collapse} th,td{padding:8px;text-align:left;border-bottom:1px solid #ddd} .warn{color:orange} .err{color:red} .info{color:#0066cc} .disk-info{background-color:#f9f9f9} .gpu-info{background-color:#f0f8ff} .network-info{background-color:#fff8f0} </style> </head> <body> <h1>Отчет о системе - $computerName</h1> <p><b>Дата:</b> $(Get-Date)</p> <p><b>Время работы системы:</b> $([math]::Round($os.LastBootUpTime.Subtract((Get-Date)).TotalHours * -1, 2)) часов</p> <h2>ОС</h2> <table class="os-info"> <tr><td>Название</td><td>$($os.Caption)</td></tr> <tr><td>Версия</td><td>$($os.Version)</td></tr> <tr><td>Архитектура</td><td>$($os.OSArchitecture)</td></tr> <tr><td>Версия BIOS</td><td>$( (Get-CimInstance Win32_BIOS).Name )</td></tr> <tr><td>Производитель системы</td><td>$( (Get-CimInstance Win32_ComputerSystem).Manufacturer )</td></tr> <tr><td>Модель системы</td><td>$( (Get-CimInstance Win32_ComputerSystem).Model )</td></tr> </table> <h2>Процессор</h2> <table> <tr><td>Модель</td><td>$($cpu.Name)</td></tr> <tr><td>Ядер/Потоков</td><td>$($cpu.NumberOfCores)/$($cpu.NumberOfLogicalProcessors)</td></tr> <tr><td>Тактовая частота</td><td>$([math]::Round($cpu.MaxClockSpeed/1000, 2)) GHz</td></tr> <tr><td>Загрузка CPU</td><td>$cpuUsage%</td></tr> </table> <h2>Память</h2> <table> <tr><td>Всего</td><td>${totalGB}GB</td></tr> <tr><td>Модули</td><td>$($mem.Count)x$([math]::Round($mem[0].Capacity/1GB))GB $($mem[0].Manufacturer)</td></tr> <tr><td>Используется</td><td>$([math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory)/1MB, 2))GB / ${totalGB}GB ($([math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory)/$os.TotalVisibleMemorySize*100, 2))%)</td></tr> </table> <h2>Графические процессоры</h2> <table class="gpu-info"> <tr><th>Модель</th><th>Память</th><th>Драйвер</th><th>Разрешение</th></tr> "@ foreach ($gpu in $gpus) { $gpuMemory = if ($gpu.AdapterRAM -gt 0) { "$([math]::Round($gpu.AdapterRAM/1GB))GB" } else { "N/A" } $html += "<tr><td>$($gpu.Name)</td><td>$gpuMemory</td><td>$($gpu.DriverVersion)</td><td>$($gpu.CurrentHorizontalResolution)x$($gpu.CurrentVerticalResolution)</td></tr>" } $html += @" </table> <h2>Диски</h2> <table class="disk-info"> <tr><th>Модель</th><th>Размер</th><th>Состояние</th><th>Разделы</th></tr> "@ foreach ($d in $disks) { $healthClass = if ($d.Health -ne "Healthy") {"warn"} else {""} $html += "<tr><td>$($d.Model)</td><td>$($d.Size)GB</td><td class='$healthClass'>$($d.Health)</td><td>$($d.Partitions)</td></tr>" } $html += @" </table> <h2>Сетевые адаптеры</h2> <table class="network-info"> <tr><th>Имя</th><th>Интерфейс</th><th>Скорость</th><th>MAC</th><th>IP-адрес</th></tr> "@ foreach ($adapter in $networkAdapters) { $html += "<tr><td>$($adapter.Name)</td><td>$($adapter.Interface)</td><td>$($adapter.Speed)</td><td>$($adapter.MAC)</td><td>$($adapter.IP)</td></tr>" } $html += @" </table> <h2>Дополнительная информация</h2> <table> <tr><td>Версия PowerShell</td><td>$($PSVersionTable.PSVersion)</td></tr> <tr><td>Логические диски</td><td>$( (Get-PSDrive | Where-Object { $_.Provider -like "*FileSystem*" } | ForEach-Object { "$($_.Name) ($([math]::Round($_.Used/1GB, 2))GB/$([math]::Round($_.Free/1GB, 2))GB" })) -join ', ' )</td></tr> </table> </body> </html> "@ $html | Out-File "$fullOutputDir\$fileName" -Encoding UTF8 return "Отчет создан: \\$($env:COMPUTERNAME)\$($fullOutputDir.Replace(':','$'))\$fileName" } catch { return "Ошибка при создании отчета: $_" } } # Основной цикл обработки компьютеров foreach ($computer in $computers) { Write-Log "Начинаем обработку компьютера: $computer" try { $remoteOutputDir = Join-Path $centralLogsFolder $computer Get-RemoteSystemInventory -computerName $computer -outputDir $centralLogsFolder -maxReports $maxReportsPerComputer -credential $cred } catch { Write-Log "Критическая ошибка при обработке компьютера $computer : $_" -level "ERROR" } Write-Log "Завершена обработка компьютера: $computer" } Write-Log "Скрипт завершил работу" -level "INFO"
1. Настройка централизованного хранения логов
$centralLogsFolder = "\\сервер\общая_папка\Logs" $maxReportsPerComputer = 5 $logFile = Join-Path $centralLogsFolder "InventoryScript_$(Get-Date -Format 'yyyyMMdd').log"
Скрипт создаёт единое хранилище отчётов в сетевой папке, что позволяет:
Централизованно управлять отчётами
Вести детальный лог выполнения
2. Функция логирования
function Write-Log { param([string]$message, [string]$level = "INFO") $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "[$timestamp][$level] $message" Add-Content -Path $logFile -Value $logEntry Write-Host $logEntry -ForegroundColor $(if ($level -eq "ERROR") { "Red" } elseif ($level -eq "WARNING") { "Yellow" } else { "White" }) }
Плюсы реализации:
Несколько уровней логирования (INFO, WARNING, ERROR)
Вывод в консоль и запись в файл (для удобства)
Визуальное выделение ошибок (каждой свой цвет, тоже для удобства)
3. Получение списка компьютеров
В скрипте я указал 3 способа получения списка рабочих машин:
# Из Active Directory $computers = Get-ADComputer -Filter * | Select-Object -ExpandProperty Name # Из текстового файла $computers = Get-Content "C:\path\to\computers.txt" | Where-Object { $_ -notmatch '^\s*#' -and $_ -ne '' } # Заданный список # $computers = @("COMPUTER1", "COMPUTER2", "COMPUTER3")
В текущей версии активирован способ 2 - чтение из файла. Можно переписать под CSV-файл.
4. Механизм удалённого выполнения
Основная функция Get-RemoteSystemInventory реализует несколько важных особенностей:
Отказоустойчивость:
Проверка доступности компьютера перед подключением
Автоматическое переключение между PSRemoting и WMI
Обработка ошибок
if (-not (Test-Connection -ComputerName $computerName -Count 2 -Quiet)) { Write-Log "Компьютер $computerName недоступен по сети" -level "WARNING" return }
Гибкое подключение:
try { $session = New-PSSession @sessionParams } catch { Write-Log "Не удалось подключиться через PSRemoting, пробуем WMI" -level "WARNING" Invoke-Command -ComputerName $computerName -Credential $credential -ScriptBlock $scriptBlock -ArgumentList $outputDir, $maxReports }
5. Функция сбора данных (Get-SystemInventory)
Эта функция выполняется на удалённых компьютерах и собирает:
Основные системные данные:
Общую инфу о системе
Данные об ОС
Характеристики процессора
Информацию о памяти
Данные о видеокартах
Состояние дисков
Сетевые настройки
Дополнительную информацию
Пример обработки дисков:
$disks = Get-Disk | ForEach-Object { $partitions = Get-Partition -DiskNumber $_.DiskNumber | ForEach-Object { $vol = Get-Volume -Partition $_ $freeSpacePercent = if ($vol.Size -gt 0) { [math]::Round(($vol.SizeRemaining / $vol.Size) * 100, 2) } else { 0 } $freeSpaceClass = if ($freeSpacePercent -lt 10) { "err" } elseif ($freeSpacePercent -lt 20) { "warn" } else { "" } "<span class="$freeSpaceClass">$($_.DriveLetter) $([math]::Round($_.Size/1GB))GB ($([math]::Round($vol.SizeRemaining/1GB))GB свободно, $freeSpacePercent%)</span>" } ... }
6. Управление отчётами
Скрипт автоматически чистит старые отчёты, оставляя только указанное кол-во:
Get-ChildItem $fullOutputDir -Filter "Inventory_*.html" | Sort-Object CreationTime -Descending | Select-Object -Skip $maxReports | Remove-Item -Force -ErrorAction SilentlyContinue
Как выглядит отчёт:


Практическое применение
Я делал скрипт для формирования понимания о парке машин, коим сейчас владею, но это далеко не весь перечень потенциально применения:
Инвентаризации парка машин - удобный сбор данных о конфигурациях;
Выявления проблем - обнаружение нехватки места на дисках и тд.;;
Документирования инфраструктуры - создание базы знаний о системе. Скрипт можно улучшить и добавить генерацию сводных отчётов по всем компьютерам.
Заключение
Скрипт, описанный в статье - крайне полезный инструмент, который может помочь сэкономить много времени. Его можно допиливать под свои нужды: добавить сбор софта, интеграцию с БД или сводную аналитику. Если тема будет интересна - разберу в следующих статьях возможные улучшения кода.
P.S. В моей группе в Телеграмм разбираем практические кейсы: скрипты (Python/Bash/PowerShell), тонкости ОС и инструменты для эффективной работы.
