Pull to refresh

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

Level of difficultyEasy
Reading time11 min
Views3.3K

Привет, Хабр! Типичная ситуация: начальство требует полный отчёт по всему парку техники или только устроились в организацию с крупным парком машин, нужно сформировать понимание, с чем вы работаете. Вручную обходить 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. Также, там можно найти этот и другие скрипты.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+9
Comments16

Articles