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

Как сделать скрипт для восстановления баз данных из шары.

Задача раз 2 недели восстанавливать базы данных на dev сервер. Бэкап сделаны с помощью ola.hallengren

-- В ночь с субботы на воскресение
--if(datepart(weekday, getdate()) = 1)
-- Для миграции на АГ сметили создание резервных копий для того что бы использовать верфицированые фулл бэкапы в процессе работ
-- В ночь с Пятницу на субботу  (В Субботу в 01.00 создать фулл бекап)

if(datepart(weekday, getdate()) = 7)
begin
    EXECUTE [dbo].[DatabaseBackup]
    @Databases = 'USER_DATABASES,-%_restore',
    @Directory = N'\\ARC03.adminbd.ru\sql_backup$',
    @BackupType = 'FULL',
    @Verify = 'Y',
    @Compress = 'Y',
    @CleanupTime = 312,
    @CleanupMode = 'AFTER_BACKUP',
    @CheckSum = 'Y',
    @LogToTable = 'Y',
    @DatabasesInParallel= 'Y';
end

Скрипт восстановления

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

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

# === Параметры (настраиваемые) ===
$SourceBackupPath = "\\arc03.adminbd.ru\backup$"
$TargetServer     = "dev4"
$DataPath         = "F:\data"
$LogPath          = "E:\log"

$DatabaseList = @(
    "bd1",
    "bd2"
)

# Email-настройки
$From       = "dev4@tech.adminbd.ru"
$To         = "moskvichev@adminbd.ru"
$SmtpServer = "tech.adminbd.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>Подробный лог доступен на сервере: <code>$LogFilePath</code></p>
"@
Send-Email -Subject $StartSubject -Body $StartBody -IsHtml

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

# === Параллельное восстановление ===
$ParallelResults = $DatabaseList | ForEach-Object -Parallel {
    $DatabaseName     = $_
    $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 "→ Начало обработки базы: $DatabaseName"

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

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

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

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

    # Генерация FileMapping через скрипт
    try {
        $ScriptText = Restore-DbaDatabase -SqlInstance $TargetServer -Path $LatestBackup -DatabaseName $DatabaseName -OutputScriptOnly -EnableException
    }
    catch {
        & $Log "❌ Ошибка генерации скрипта восстановления: $($_.Exception.Message)"
        return [PSCustomObject]@{
            DatabaseName = $DatabaseName
            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 = $DatabaseName
            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 = "${DatabaseName}_${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 для $DatabaseName (файлов: $($NewFileMap.Count))"

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

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

Запуск через task scheduler

Cmd

"C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo -ExecutionPolicy Bypass -File "%~dp0restoredb.ps1"

Similar Posts:

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

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