AI Автоматизация инвентаризации парка ПК: PowerShell-скрипт для сбора системной информации

AI

Редактор
Регистрация
23 Август 2023
Сообщения
2 819
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1


Привет, Хабр! Типичная ситуация: начальство требует полный отчёт по всему парку техники или только устроились в организацию с крупным парком машин, нужно сформировать понимание, с чем вы работаете. Вручную обходить 100+ рабочих мест - совсем не вариант.

В статье я поделюсь PowerShell-скриптом, который:


  • Сам обойдет все машины в сети


  • Соберёт подробную информацию о конфигурации


  • Сгенерирует удобные для восприятия HTML-отчёты


  • Складирует всё в вашу сетевую папку для удобства

Скрипт будет полезен системным администраторам, инженерам ТП, аудиторам и тем, кто устал вручную обходить все машины в организации.

Возможности скрипта


Скрипт собирает комплексную информацию о системе:


  • Информация об операционной системе (версия, архитектура, время работы)


  • Данные о BIOS и производителе оборудования Аппаратное обеспечение:


  • Процессор (модель, ядра/потоки, загрузка)


  • Оперативная память (объем, модули, использование)


  • Графические процессоры (модель, память, драйверы)


  • Накопители (модель, разделы, свободное пространство) Сетевые адаптеры:


  • Активные сетевые адаптеры


  • IP и MAC-адреса


  • Скорость соединения Доп информация:


  • Версия PowerShell


  • Логические диски и процент их заполнения
Назначение скрипта


Скрипт выполняет следующие задачи:


  • Собирает основную конфигурацию с удалённых машин


  • Сохраняет данные в структурированном HTML-формате


  • Централизованно хранит отчёты в вашей сетевой папке


  • Автоматически удаляет устаревшие отчёты


  • Логирует все этапы работы
Архитектура решения


Скрипт имеет следующую структуру:


  1. Контроллер - основной скрипт, который запускается на машине админа


  2. Агенты - функции, выполняемые на удалённых компьютерах
Детальный разбор компонентов


Я разберу лишь основные функции скрипта, тк нагружать вас во многом бесполезным кодом - нет смысла. Полный скрипт:

Скрытый текст

$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

Как выглядит отчёт:


ОС, процессор, память

Графические процессоры, диски, сетевые адаптеры, дополнительная информация
Практическое применение


Я делал скрипт для формирования понимания о парке машин, коим сейчас владею, но это далеко не весь перечень потенциально применения:


  1. Инвентаризации парка машин - удобный сбор данных о конфигурациях;


  2. Выявления проблем - обнаружение нехватки места на дисках и тд.;;


  3. Документирования инфраструктуры - создание базы знаний о системе. Скрипт можно улучшить и добавить генерацию сводных отчётов по всем компьютерам.
Заключение


Скрипт, описанный в статье - крайне полезный инструмент, который может помочь сэкономить много времени. Его можно допиливать под свои нужды: добавить сбор софта, интеграцию с БД или сводную аналитику. Если тема будет интересна - разберу в следующих статьях возможные улучшения кода.

P.S. Я запустил свою группу в Телеграмм, буду рад видеть всех, кому интересен процесс написания скриптов и автоматизация в мире IT. Также, там можно найти этот и другие скрипты.
 
Сверху Снизу