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

Как восстановить базы данных SQL Server автоматически с уведомлениями по email с помощью PowerShell

Описание:
В статье представлен готовый PowerShell-скрипт для автоматического восстановления баз данных SQL Server из резервных копий. Скрипт использует модуль dbatools, выполняет параллельное восстановление нескольких баз, автоматически определяет пути для файлов данных и логов, а также отправляет подробные email-отчёты о ходе и результатах операции. Подходит для регулярных задач обновления тестовых сред или восстановления после сбоев. Так же добавляет к имени базы DEV

# ======================================================================
# Автоматическое восстановление баз данных с уведомлениями по email
# ======================================================================
# Требуется: модуль dbatools, доступ к бэкапам и целевому SQL Server
# ======================================================================

# Подключаем модуль dbatools
Import-Module dbatools -ErrorAction Stop

# === Параметры (настраиваемые) ===
$SourceBackupPath = "\\rc03.admibd.ru\sql_backup$\DBCL01`$dbag01"
$TargetServer     = "dev-04"
$DataPath         = "d:\data"
$LogPath          = "E:\log"
$Suffix           = "dev"   # суффикс для имени целевой базы

$DatabaseList = @(
    "HR"
)

# Email-настройки
$From       = "dev-db04@tech.admibd.ru"
$To         = "it@admibd.ru","moskvichev@admibd.ru"
$SmtpServer = "tech.admibd.ru"
$SmtpPort   = 25

# Лог-файл
$LogFilePath = "C:\temp\RestoreLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"

# Создаём папку для логов, если не существует
if (-not (Test-Path "C:\temp")) { New-Item -ItemType Directory -Path "C:\temp" -Force | Out-Null }

# === Вспомогательные функции ===

function Write-Log {
    param([string]$Message)
    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $LogEntry = "[$Timestamp] $Message"
    Add-Content -Path $LogFilePath -Value $LogEntry -Encoding UTF8
    Write-Host $LogEntry
}

function Send-Email {
    param(
        [string]$Subject,
        [string]$Body,
        [switch]$IsHtml
    )
    try {
        $Params = @{
            SmtpServer                 = $SmtpServer
            Port                       = $SmtpPort
            From                       = $From
            To                         = $To
            Subject                    = $Subject
            Body                       = $Body
            Encoding                   = [System.Text.Encoding]::UTF8
            DeliveryNotificationOption = 'Never'
        }
        if ($IsHtml) { $Params.BodyAsHtml = $true }
        Send-MailMessage @Params
        Write-Log "Письмо отправлено: $Subject"
    }
    catch {
        Write-Log "Ошибка отправки email: $($_.Exception.Message)"
    }
}

# === СТАРТ ===
Write-Log "=== ЗАПУСК восстановления баз данных на $TargetServer ==="

# Отправляем уведомление о начале
$StartSubject = "🚀 Началось восстановление баз данных на $TargetServer"
$StartBody = @"
<p>Восстановление баз данных на сервере <strong>$TargetServer</strong> запущено.</p>
<p><strong>Время начала:</strong> $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")</p>
<p><strong>Список баз:</strong> $($DatabaseList -join ', ')</p>
<p>Целевые имена будут с суффиксом <strong>$Suffix</strong>.</p>
<p>Подробный лог доступен на сервере: <code>$LogFilePath</code></p>
"@
Send-Email -Subject $StartSubject -Body $StartBody -IsHtml

# Список для результатов
$Results = [System.Collections.Generic.List[object]]::new()

# === Параллельное восстановление ===
$ParallelResults = $DatabaseList | ForEach-Object -Parallel {
    $OriginalName      = $_
    $Suffix            = $using:Suffix
    $TargetDatabaseName = $OriginalName + $Suffix
    $SourceBackupPath  = $using:SourceBackupPath
    $TargetServer      = $using:TargetServer
    $DataPath          = $using:DataPath
    $LogPath           = $using:LogPath
    $LogFilePath       = $using:LogFilePath

    # Подключаем модуль в потоке
    Import-Module dbatools -ErrorAction Stop

    # Локальная лог-функция для потока
    $Log = {
        param($msg)
        $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        Add-Content -Path $LogFilePath -Value "[$ts] $msg" -Encoding UTF8
        Write-Host "[$ts] $msg"
    }.GetNewClosure()

    & $Log "→ Начало обработки базы: $OriginalName -> $TargetDatabaseName"

    # Удаление существующей целевой базы
    $DbExists = Get-DbaDatabase -SqlInstance $TargetServer -Database $TargetDatabaseName -ErrorAction SilentlyContinue
    if ($DbExists) {
        try {
            & $Log "Удаление существующей базы $TargetDatabaseName..."
            $DropResult = Remove-DbaDatabase -SqlInstance $TargetServer -Database $TargetDatabaseName -Confirm:$false -EnableException
            if ($DropResult.Status -ne "Dropped") {
                throw "Неожиданный статус при удалении: $($DropResult.Status)"
            }
            & $Log "База $TargetDatabaseName успешно удалена."
        }
        catch {
            & $Log "❌ Ошибка удаления базы $TargetDatabaseName : $($_.Exception.Message)"
            return [PSCustomObject]@{
                DatabaseName       = $OriginalName
                TargetDatabaseName = $TargetDatabaseName
                Status             = "Ошибка"
                Duration           = "N/A"
                Details            = "Ошибка удаления: $($_.Exception.Message)"
            }
        }
    } else {
        & $Log "База $TargetDatabaseName не существует — удаление пропущено."
    }

    # Поиск последнего FULL-бэкапа по оригинальному имени
    $FullBackupDir = Join-Path $SourceBackupPath (Join-Path $OriginalName "FULL")
    $BackupFiles = Get-ChildItem -Path $FullBackupDir -Filter "*.bak" -ErrorAction SilentlyContinue | Sort-Object CreationTime -Descending

    if (-not $BackupFiles -or $BackupFiles.Count -eq 0) {
        & $Log "❌ Бэкап для $OriginalName не найден в: $FullBackupDir"
        return [PSCustomObject]@{
            DatabaseName       = $OriginalName
            TargetDatabaseName = $TargetDatabaseName
            Status             = "Ошибка"
            Duration           = "N/A"
            Details            = "Бэкап не найден"
        }
    }

    $LatestBackup = $BackupFiles[0].FullName
    & $Log "Используется бэкап: $LatestBackup"

    # Генерация FileMapping через скрипт
    try {
        $ScriptText = Restore-DbaDatabase -SqlInstance $TargetServer -Path $LatestBackup -DatabaseName $TargetDatabaseName -OutputScriptOnly -EnableException
    }
    catch {
        & $Log "❌ Ошибка генерации скрипта восстановления: $($_.Exception.Message)"
        return [PSCustomObject]@{
            DatabaseName       = $OriginalName
            TargetDatabaseName = $TargetDatabaseName
            Status             = "Ошибка"
            Duration           = "N/A"
            Details            = "Ошибка генерации скрипта: $($_.Exception.Message)"
        }
    }

    # Парсинг MOVE-инструкций
    $MoveRegex = [regex]::new("MOVE N'([^']+)' TO N'([^']+)'")
    $Matches = $MoveRegex.Matches($ScriptText)

    if ($Matches.Count -eq 0) {
        & $Log "❌ Не найдено ни одной инструкции MOVE в скрипте восстановления."
        return [PSCustomObject]@{
            DatabaseName       = $OriginalName
            TargetDatabaseName = $TargetDatabaseName
            Status             = "Ошибка"
            Duration           = "N/A"
            Details            = "Не удалось определить файлы из бэкапа"
        }
    }

    $NewFileMap = @{}
    $UsedPaths = @{}
    foreach ($Match in $Matches) {
        $LogicalName = $Match.Groups[1].Value
        $OriginalPath = $Match.Groups[2].Value
        $Extension = [IO.Path]::GetExtension($OriginalPath)
        $BaseName = "${TargetDatabaseName}_${LogicalName}"

        # Формируем путь
        $TargetDir = if ($Extension -eq '.ldf') { $LogPath } else { $DataPath }
        $NewPath = Join-Path $TargetDir "$BaseName$Extension"

        # Разрешаем конфликты имён
        $Counter = 1
        $FinalPath = $NewPath
        while ($UsedPaths.ContainsKey($FinalPath)) {
            $FinalPath = Join-Path $TargetDir "${BaseName}_${Counter}${Extension}"
            $Counter++
        }
        $UsedPaths[$FinalPath] = $true
        $NewFileMap[$LogicalName] = $FinalPath
    }

    & $Log "Создана FileMapping для $TargetDatabaseName (файлов: $($NewFileMap.Count))"

    # Восстановление
    $StartTime = Get-Date
    try {
        & $Log "▶ Запуск восстановления $TargetDatabaseName..."
        $null = Restore-DbaDatabase -SqlInstance $TargetServer -Path $LatestBackup -DatabaseName $TargetDatabaseName -FileMapping $NewFileMap -WithReplace -EnableException
        $EndTime = Get-Date
        $Duration = $EndTime - $StartTime
        $DurationStr = "{0:hh\:mm\:ss}" -f $Duration

        # Проверка появления базы
        $DbCheck = Get-DbaDatabase -SqlInstance $TargetServer -Database $TargetDatabaseName -ErrorAction SilentlyContinue
        if ($DbCheck) {
            & $Log "✅ База $TargetDatabaseName успешно восстановлена за $DurationStr"
            return [PSCustomObject]@{
                DatabaseName       = $OriginalName
                TargetDatabaseName = $TargetDatabaseName
                Status             = "Успешно"
                Duration           = $DurationStr
                Details            = "Восстановлено за $DurationStr как $TargetDatabaseName"
            }
        } else {
            throw "База не обнаружена после восстановления"
        }
    }
    catch {
        $EndTime = Get-Date
        $Duration = $EndTime - $StartTime
        $DurationStr = "{0:hh\:mm\:ss}" -f $Duration
        & $Log "❌ Ошибка восстановления $TargetDatabaseName : $($_.Exception.Message)"
        return [PSCustomObject]@{
            DatabaseName       = $OriginalName
            TargetDatabaseName = $TargetDatabaseName
            Status             = "Ошибка"
            Duration           = $DurationStr
            Details            = "Ошибка: $($_.Exception.Message)"
        }
    }
} -ThrottleLimit 3

# Добавляем результаты
foreach ($res in $ParallelResults) {
    $Results.Add($res)
}

Write-Log "=== ВСЕ БАЗЫ ОБРАБОТАНЫ ==="

# === Формирование и отправка итогового отчёта ===

$HtmlRows = foreach ($r in $Results) {
    $BgColor = if ($r.Status -eq "Успешно") { "#d4edda" } else { "#f8d7da" }
    $Color   = if ($r.Status -eq "Успешно") { "#155724" } else { "#721c24" }
    "<tr style='background-color: $BgColor; color: $Color;'>
        <td><strong>$($r.DatabaseName)</strong></td>
        <td>$($r.TargetDatabaseName)</td>
        <td>$($r.Status)</td>
        <td>$($r.Duration)</td>
        <td>$($r.Details)</td>
    </tr>"
}

$TotalCount   = $Results.Count
$SuccessCount = ($Results | Where-Object Status -eq "Успешно").Count
$FailCount    = $TotalCount - $SuccessCount

$Summary = if ($FailCount -eq 0) {
    "✅ Все базы восстановлены успешно."
} else {
    "⚠️ Успешно: $SuccessCount из $TotalCount. Ошибок: $FailCount."
}

$CompletionTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$HtmlReport = @"
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Отчёт: восстановление БД на $TargetServer</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        table { border-collapse: collapse; width: 100%; margin-top: 10px; }
        th, td { border: 1px solid #ccc; padding: 10px; text-align: left; }
        th { background-color: #f1f1f1; }
        .summary { font-size: 1.1em; font-weight: bold; margin-bottom: 15px; color: #333; }
    </style>
</head>
<body>
    <h2>✅ Восстановление баз данных на $TargetServer завершено</h2>
    <p><strong>Время завершения:</strong> $CompletionTime</p>
    <div class="summary">$Summary</div>
    <table>
        <thead>
            <tr>
                <th>Исходная БД</th>
                <th>Целевая БД</th>
                <th>Статус</th>
                <th>Длительность</th>
                <th>Детали</th>
            </tr>
        </thead>
        <tbody>
            $($HtmlRows -join "")
        </tbody>
    </table>
    <p><em>С уважением,<br>Скрипт автоматического восстановления</em></p>
</body>
</html>
"@

$EndSubject = if ($FailCount -eq 0) {
    "✅ Успешно: восстановление БД на $TargetServer"
} else {
    "⚠️ Частичный успех: восстановление БД на $TargetServer ($SuccessCount/$TotalCount)"
}

Send-Email -Subject $EndSubject -Body $HtmlReport -IsHtml

# === Вывод в консоль ===
Write-Host "`n" + ("=" * 60) -ForegroundColor Cyan
Write-Host "РЕЗУЛЬТАТЫ ВОССТАНОВЛЕНИЯ" -ForegroundColor Cyan
Write-Host ("=" * 60) -ForegroundColor Cyan
$Results | Format-Table -AutoSize
Write-Host "`n📄 Лог: $LogFilePath" -ForegroundColor Cyan
Write-Host "📧 Отчёты отправлены на: $To" -ForegroundColor Green напиши заголовок статьи и описание . начинаться заголовок статьи как восстановить

 

Similar Posts:

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

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