Перейти к содержимому

Как сделать автоматический сбор информации о всех заданиях резервного копирования Veeam с нескольких серверов в единый CSV-отчёт

Назначение скрипта

Данный PowerShell-скрипт предназначен для централизованного сбора детальной информации о заданиях резервного копирования (включая агентские Computer Backup Job) с одного или нескольких серверов Veeam Backup & Replication.
Результат работы — один общий CSV-файл, в который по каждому заданию попадают:

  • Сервер Veeam, с которого собраны данные;

  • Название задания и его тип;

  • Дата и время последнего запуска и его статус (Success/Warning/Failed);

  • Список объектов, которые бэкапятся (включая исключённые; для Backup Copy заданий — источник);

  • Статус каждой машины (или каждого объекта) в рамках последнего запуска;

  • Репозиторий, куда сохраняется бэкап, и его физический путь (в том числе для Scale‑Out и S3‑совместимых репозиториев).

Скрипт ведёт подробный лог выполнения в отдельную папку, автоматически удаляя логи старше заданного количества дней.


Когда это может пригодиться

  • Администраторам, обслуживающим несколько Veeam-серверов и желающим иметь единую сводку по всем заданиям;

  • Для аудита, инвентаризации или миграции;

  • Для быстрой проверки, что все задания успешно завершаются и куда именно пишутся данные;

  • Для автоматической генерации отчёта по расписанию (например, еженедельно) с последующей отправкой по почте.

<#
.SYNOPSIS
    Сбор информации о заданиях резервного копирования с одного или нескольких
    серверов Veeam Backup & Replication.

.DESCRIPTION
    Скрипт по очереди подключается к каждому серверу из списка $VBRServers,
    собирает по каждому заданию (включая агентские Computer Backup Job):
      - Сервер Veeam, с которого собраны данные
      - Название задания
      - Дата и время последнего запуска (и статус)
      - Что бэкапилось (объекты, включая исключённые; для Backup Copy - источник)
      - Машины и статус по каждой из них в последнем запуске
      - Куда бэкапилось (репозиторий и физический путь, в т.ч. Scale-Out/S3)
    Ведёт подробный лог выполнения в файл и хранит логи не дольше N дней.
    Результат выводится на экран и сохраняется в один общий CSV.
#>

# =========================================
#           ПЕРЕМЕННЫЕ (настройки)
# =========================================

# Список серверов Veeam Backup & Replication для опроса.
# Для каждого сервера можно (не обязательно) указать свои User/Password,
# если не указаны - используются общие $VBRUser/$VBRPassword ниже.
$VBRServers = @(
    @{ Server = "veeam03" }
    @{ Server = "veeam04" }
     @{ Server = veeam01" }
    # @{ Server = "veeam0X"; User = "DOMAIN\svc_veeam_ps"; Password = "пароль" }
)

$OutputPath        = ".\VeeamBackupInfo.csv"     # Путь к файлу отчёта CSV (общий по всем серверам)
$LogDir            = ".\Logs"                    # Папка для логов выполнения
$LogRetentionDays  = 3                           # Хранить логи не дольше N дней

# Общие учётные данные по умолчанию (если для сервера в $VBRServers не заданы свои).
# Нужны, например, если на сервере включена MFA - тогда используйте сервисную учётку
# (см. чекбокс "This is a service account" в Veeam Users and Roles).
$VBRUser           = ""                          # например "DOMAIN\svc_veeam_ps"
$VBRPassword       = ""                          # пароль сервисной учётки
# =========================================


# ---------------------------------------------------
#                 ФУНКЦИЯ ЛОГИРОВАНИЯ
# ---------------------------------------------------
function Write-Log {
    param(
        [string]$Message,
        [ValidateSet("INFO","WARN","ERROR")]
        [string]$Level = "INFO"
    )

    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $line = "[$timestamp] [$Level] $Message"

    switch ($Level) {
        "INFO"  { Write-Host $line -ForegroundColor Gray }
        "WARN"  { Write-Host $line -ForegroundColor Yellow }
        "ERROR" { Write-Host $line -ForegroundColor Red }
    }

    Add-Content -Path $script:LogFile -Value $line -Encoding UTF8
}

# ---------------------------------------------------
#          ПОДГОТОВКА ПАПКИ ЛОГОВ И РОТАЦИЯ
# ---------------------------------------------------
if (-not (Test-Path $LogDir)) {
    New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
}

$oldLogs = Get-ChildItem -Path $LogDir -Filter "VeeamBackupInfo_*.log" -ErrorAction SilentlyContinue |
           Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$LogRetentionDays) }

foreach ($old in $oldLogs) {
    Remove-Item $old.FullName -Force -ErrorAction SilentlyContinue
}

$LogFile = Join-Path $LogDir ("VeeamBackupInfo_{0}.log" -f (Get-Date -Format "yyyyMMdd_HHmmss"))
New-Item -Path $LogFile -ItemType File -Force | Out-Null

Write-Log "=== Запуск скрипта сбора информации Veeam (серверов в списке: $($VBRServers.Count)) ==="
Write-Log "Удалено старых лог-файлов: $($oldLogs.Count) (старше $LogRetentionDays дн.)"

# ---------------------------------------------------
#             ПОДКЛЮЧЕНИЕ МОДУЛЯ VEEAM
# ---------------------------------------------------
try {
    if (-not (Get-Module -Name Veeam.Backup.PowerShell)) {
        Write-Log "Импорт модуля Veeam.Backup.PowerShell..."
        Import-Module Veeam.Backup.PowerShell -ErrorAction Stop
        Write-Log "Модуль успешно загружен."
    }
    else {
        Write-Log "Модуль Veeam.Backup.PowerShell уже загружен."
    }
}
catch {
    Write-Log "Не удалось загрузить модуль Veeam.Backup.PowerShell: $($_.Exception.Message)" "ERROR"
    throw $_
}

# ---------------------------------------------------
#      ФУНКЦИЯ ПОДКЛЮЧЕНИЯ К СЕРВЕРУ VBR
# ---------------------------------------------------
function Connect-ToVBR {
    param(
        [string]$ServerName,
        [string]$User,
        [string]$Password
    )

    try {
        if ($User -and $Password) {
            Write-Log "Подключение к $ServerName под сервисной учёткой $User..."
            $secPass = ConvertTo-SecureString $Password -AsPlainText -Force
            $cred = New-Object System.Management.Automation.PSCredential($User, $secPass)
            Connect-VBRServer -Server $ServerName -Credential $cred -ErrorAction Stop
        }
        else {
            Write-Log "Подключение к $ServerName под текущим пользователем..."
            Connect-VBRServer -Server $ServerName -ErrorAction Stop
        }
        Write-Log "Подключение к $ServerName установлено успешно."
        return $true
    }
    catch {
        Write-Log "Не удалось подключиться к серверу Veeam ($ServerName): $($_.Exception.Message)" "ERROR"
        Write-Log "Если ошибка про MFA - используйте сервисную учётку (User/Password) с отключённой MFA." "WARN"
        return $false
    }
}

# ---------------------------------------------------
#      ФУНКЦИЯ ОПРЕДЕЛЕНИЯ ПУТИ РЕПОЗИТОРИЯ
# ---------------------------------------------------
function Get-RepoPathInfo {
    param($Repo)

    if (-not $Repo) { return $null }

    # 1) Обычный (Windows/Linux) репозиторий
    if ($Repo.PSObject.Properties.Name -contains "FriendlyPath" -and $Repo.FriendlyPath) {
        return $Repo.FriendlyPath
    }

    # 2) Scale-Out Backup Repository - собираем пути всех extent-ов (рекурсивно)
    try {
        $extents = Get-VBRRepositoryExtent -Repository $Repo -ErrorAction Stop
        if ($extents) {
            $paths = $extents | ForEach-Object {
                $extPath = Get-RepoPathInfo -Repo $_.Repository
                "$($_.Repository.Name): $extPath"
            }
            return ($paths -join "; ")
        }
    }
    catch { }

    # 3) Object Storage / S3-совместимый репозиторий
    try {
        if ($Repo.PSObject.Properties.Name -contains "AmazonOptions" -and $Repo.AmazonOptions) {
            $opt    = $Repo.AmazonOptions
            $bucket = $opt.BucketName
            $folder = $opt.FolderName
            $point  = $null
            if ($Repo.PSObject.Properties.Name -contains "ServicePoint") { $point = $Repo.ServicePoint }
            elseif ($Repo.PSObject.Properties.Name -contains "ConnectionPoint") { $point = $Repo.ConnectionPoint }
            $built = "amazonS3://$point/$bucket/$folder" -replace "/{2,}", "/"
            if ($bucket -or $folder) { return $built }
        }
    }
    catch { }

    try {
        $objRepo = Get-VBRObjectStorageRepository -Id $Repo.Id -ErrorAction Stop
        if ($objRepo) {
            $folder = $null; $conn = $null
            if ($objRepo.PSObject.Properties.Name -contains "Folder") { $folder = $objRepo.Folder }
            if ($objRepo.PSObject.Properties.Name -contains "ConnectionPoint") { $conn = $objRepo.ConnectionPoint }
            if ($folder -or $conn) { return ("$conn/$folder" -replace "/{2,}", "/") }
        }
    }
    catch { }

    # 4) Фолбэк - сырое свойство Path
    if ($Repo.PSObject.Properties.Name -contains "Path" -and $Repo.Path) {
        return $Repo.Path.ToString()
    }

    return "не удалось определить путь (тип объекта: $($Repo.GetType().Name))"
}

# ---------------------------------------------------
#   ФУНКЦИЯ ПОЛУЧЕНИЯ СТАТУСА КАЖДОЙ МАШИНЫ В ЗАДАНИИ
# ---------------------------------------------------
function Get-JobMachineStatuses {
    param($Session)

    if (-not $Session) { return "нет данных о сессии" }

    try {
        $tasks = Get-VBRTaskSession -Session $Session -ErrorAction Stop
        if (-not $tasks) { return "нет задач в сессии" }

        $list = $tasks | ForEach-Object { "$($_.Name): $($_.Status)" }
        return ($list -join "; ")
    }
    catch {
        return "не удалось получить статусы машин: $($_.Exception.Message)"
    }
}

# ---------------------------------------------------
#   ФУНКЦИЯ ПОЛУЧЕНИЯ ПОСЛЕДНЕЙ СЕССИИ + СТАТУСОВ МАШИН
#   с учётом особенностей разных типов заданий
# ---------------------------------------------------
function Get-JobLastSessionInfo {
    param($Job)

    $info = [PSCustomObject]@{
        LastRunTime     = "нет данных"
        LastRunStatus   = "нет данных"
        MachineStatuses = "нет данных о сессии"
    }

    # --- Агентские (EpAgentBackup) задания ---
    if ($Job.JobType -eq "EpAgentBackup") {
        try {
            $compJob = Get-VBRComputerBackupJob -Name $Job.Name -ErrorAction Stop
            $sessions = Get-VBRComputerBackupJobSession -ErrorAction Stop |
                        Where-Object { $_.JobId -eq $compJob.Id } |
                        Sort-Object CreationTime -Descending

            if ($sessions) {
                $latest = $sessions | Select-Object -First 1
                $info.LastRunTime   = $latest.CreationTime
                $info.LastRunStatus = $latest.Result

                $perMachine = $sessions | Group-Object Name | ForEach-Object {
                    $s = $_.Group | Sort-Object CreationTime -Descending | Select-Object -First 1
                    "$($s.Name): $($s.Result)"
                }
                $info.MachineStatuses = ($perMachine -join "; ")
            }
            else {
                $info.MachineStatuses = "нет сессий (Get-VBRComputerBackupJobSession пуст)"
            }
        }
        catch {
            $info.MachineStatuses = "не удалось получить сессии агентского задания: $($_.Exception.Message)"
        }
        return $info
    }

    # --- Обычные (VM/NAS) и Backup Copy задания ---
    $sess = Get-VBRBackupSession | Where-Object { $_.JobId -eq $Job.Id } |
            Sort-Object CreationTime -Descending | Select-Object -First 1

    if ($sess) {
        $info.LastRunTime     = $sess.CreationTime
        $info.LastRunStatus   = $sess.Result
        $info.MachineStatuses = Get-JobMachineStatuses -Session $sess
    }
    elseif ($Job.JobType -eq "SimpleBackupCopyPolicy" -or $Job.JobType -like "*BackupCopy*") {
        try {
            $info.LastRunStatus   = $Job.GetLastResult()
            $info.MachineStatuses = "Backup Copy в непрерывном режиме - постатейный статус машин через PowerShell недоступен (ограничение Veeam), см. консоль"
        }
        catch { }
    }

    return $info
}

# ---------------------------------------------------
#      ФУНКЦИЯ ПОЛУЧЕНИЯ ОБЪЕКТОВ ЗАДАНИЯ
# ---------------------------------------------------
function Get-JobObjectsList {
    param($Job)

    if ($Job.JobType -eq "SimpleBackupCopyPolicy" -or $Job.JobType -like "*BackupCopy*") {
        try {
            $linkedIds = $Job.LinkedJobIds | ForEach-Object { $_.Guid }
            if (-not $linkedIds) {
                return "Backup Copy задание без привязанного job-источника (источник задан напрямую как backup/репозиторий)"
            }

            $linkedJobs = Get-VBRJob -WarningAction SilentlyContinue |
                          Where-Object { $linkedIds -contains $_.Id.Guid }

            if (-not $linkedJobs) {
                return "не удалось найти задание-источник по LinkedJobIds"
            }

            $parts = foreach ($lj in $linkedJobs) {
                $srcObjects = (Get-VBRJobObject -Job $lj -ErrorAction SilentlyContinue |
                               ForEach-Object { $_.Name }) -join ", "
                if (-not $srcObjects) { $srcObjects = "(объекты источника не определены)" }
                "[источник: $($lj.Name)] $srcObjects"
            }
            return ($parts -join "; ")
        }
        catch {
            return "не удалось определить источник Backup Copy задания: $($_.Exception.Message)"
        }
    }

    try {
        $objInfo = Get-VBRJobObject -Job $Job -ErrorAction Stop
        $list = $objInfo | ForEach-Object {
            $excl = $false
            if ($_.PSObject.Properties.Name -contains "IsExcluded") { $excl = $_.IsExcluded }
            if ($excl) { "[ИСКЛЮЧЕНО] $($_.Name)" } else { "$($_.Name)" }
        }
        return ($list -join "; ")
    }
    catch {
        return $null
    }
}

# ---------------------------------------------------
#   ФУНКЦИЯ СБОРА ВСЕХ ЗАДАНИЙ С ТЕКУЩЕГО ПОДКЛЮЧЁННОГО СЕРВЕРА
# ---------------------------------------------------
function Get-VeeamJobsFromCurrentServer {
    param([string]$ServerLabel)

    $localResult = @()

    # --- Обычные задания (VM, NAS и т.п. через Get-VBRJob) ---
    $jobs = Get-VBRJob
    Write-Log "[$ServerLabel] Найдено заданий (Get-VBRJob): $($jobs.Count)"

    foreach ($job in $jobs) {

        Write-Log "[$ServerLabel] Обработка задания: '$($job.Name)' (тип: $($job.JobType))"

        $sessInfo = Get-JobLastSessionInfo -Job $job

        if ($sessInfo.LastRunTime -ne "нет данных") {
            Write-Log "  Последняя сессия: $($sessInfo.LastRunTime) - статус $($sessInfo.LastRunStatus)"
        }
        else {
            Write-Log "  Сессии запуска не найдены." "WARN"
        }

        $objectsList = Get-JobObjectsList -Job $job

        if ($null -eq $objectsList) {
            Write-Log "  Get-VBRJobObject не поддерживается для этого задания, пробуем Get-VBRComputerBackupJob..." "WARN"
            try {
                $compJob = Get-VBRComputerBackupJob -Name $job.Name -ErrorAction Stop
                $objectsList = (Get-VBRComputerBackupJobObject -Job $compJob -ErrorAction Stop |
                                 ForEach-Object { $_.Name }) -join "; "
            }
            catch {
                Write-Log "  Не удалось получить объекты через Get-VBRComputerBackupJob: $($_.Exception.Message)" "ERROR"
                $objectsList = "не удалось получить список объектов"
            }
        }

        $repo = $null
        try {
            $repo = $job.GetBackupTargetRepository()
        }
        catch {
            Write-Log "  Не удалось получить репозиторий через GetBackupTargetRepository(): $($_.Exception.Message)" "WARN"
        }

        $repoName = if ($repo) { $repo.Name } else { "не удалось определить" }
        $repoPath = Get-RepoPathInfo -Repo $repo

        Write-Log "  Статусы машин: $($sessInfo.MachineStatuses)"

        $localResult += [PSCustomObject]@{
            "Сервер Veeam"                     = $ServerLabel
            "Название задания"                = $job.Name
            "Тип задания"                     = $job.JobType
            "Последний запуск"                = $sessInfo.LastRunTime
            "Статус последнего запуска"       = $sessInfo.LastRunStatus
            "Что бэкапится (объекты задания)" = $objectsList
            "Машины и статус (посл. запуск)"  = $sessInfo.MachineStatuses
            "Куда бэкапится (репозиторий)"    = $repoName
            "Путь репозитория"                = $repoPath
        }
    }

    # --- Агентские задания (Computer Backup Job), которых нет в Get-VBRJob ---
    try {
        $compJobs = Get-VBRComputerBackupJob -ErrorAction Stop
        $alreadyListed = $localResult."Название задания"

        foreach ($cjob in $compJobs) {
            if ($alreadyListed -contains $cjob.Name) { continue }

            Write-Log "[$ServerLabel] Обработка агентского задания: '$($cjob.Name)'"

            $sessions = Get-VBRComputerBackupJobSession -ErrorAction SilentlyContinue |
                        Where-Object { $_.JobId -eq $cjob.Id } |
                        Sort-Object CreationTime -Descending

            $lastRunTime = "нет данных"
            $lastRunStatus = "нет данных"
            $machineStatuses = "нет данных о сессии"

            if ($sessions) {
                $latest = $sessions | Select-Object -First 1
                $lastRunTime   = $latest.CreationTime
                $lastRunStatus = $latest.Result

                $perMachine = $sessions | Group-Object Name | ForEach-Object {
                    $s = $_.Group | Sort-Object CreationTime -Descending | Select-Object -First 1
                    "$($s.Name): $($s.Result)"
                }
                $machineStatuses = ($perMachine -join "; ")
            }

            $objectsList = (Get-VBRComputerBackupJobObject -Job $cjob -ErrorAction SilentlyContinue |
                             ForEach-Object { $_.Name }) -join "; "

            $repo = $null
            try { $repo = $cjob.BackupRepository } catch { }
            $repoName = if ($repo) { $repo.Name } else { "не удалось определить" }
            $repoPath = Get-RepoPathInfo -Repo $repo

            $localResult += [PSCustomObject]@{
                "Сервер Veeam"                     = $ServerLabel
                "Название задания"                = $cjob.Name
                "Тип задания"                     = "ComputerBackup"
                "Последний запуск"                = $lastRunTime
                "Статус последнего запуска"       = $lastRunStatus
                "Что бэкапится (объекты задания)" = $objectsList
                "Машины и статус (посл. запуск)"  = $machineStatuses
                "Куда бэкапится (репозиторий)"    = $repoName
                "Путь репозитория"                = $repoPath
            }
        }
    }
    catch {
        Write-Log "[$ServerLabel] Get-VBRComputerBackupJob недоступен или вернул ошибку: $($_.Exception.Message)" "WARN"
    }

    return $localResult
}

# =========================================
#        ГЛАВНЫЙ ЦИКЛ ПО ВСЕМ СЕРВЕРАМ
# =========================================
$allResults = @()

foreach ($srv in $VBRServers) {

    $serverName = $srv.Server
    $userCur = if ($srv.ContainsKey('User') -and $srv.User) { $srv.User } else { $VBRUser }
    $passCur = if ($srv.ContainsKey('Password') -and $srv.Password) { $srv.Password } else { $VBRPassword }

    Write-Log "======================================================="
    Write-Log "Обработка сервера: $serverName"
    Write-Log "======================================================="

    $connected = Connect-ToVBR -ServerName $serverName -User $userCur -Password $passCur

    if (-not $connected) {
        Write-Log "Пропускаем сервер $serverName из-за ошибки подключения." "ERROR"
        continue
    }

    try {
        $serverResults = Get-VeeamJobsFromCurrentServer -ServerLabel $serverName
        $allResults += $serverResults
        Write-Log "[$serverName] Собрано заданий: $($serverResults.Count)"
    }
    catch {
        Write-Log "[$serverName] Ошибка при сборе данных: $($_.Exception.Message)" "ERROR"
    }
    finally {
        try {
            Disconnect-VBRServer -ErrorAction SilentlyContinue
            Write-Log "Отключились от сервера $serverName"
        }
        catch { }
    }
}

Write-Log "Всего заданий собрано со всех серверов: $($allResults.Count)"

# --- Вывод на экран ---
$allResults | Format-Table -AutoSize -Wrap

# --- Экспорт в общий CSV ---
$allResults | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 -Delimiter ";"
Write-Log "Общий отчёт сохранён в: $OutputPath"

Write-Host "`nГотово. Отчёт: $OutputPath" -ForegroundColor Green
Write-Host "Лог выполнения: $LogFile" -ForegroundColor Green

Write-Log "=== Завершение скрипта ==="

 

Similar Posts:

Метки:

Добавить комментарий

Яндекс.Метрика