Задача автоматизировать проверку бэкапов mssql .
Основные возможности:
-
Сканирование структуры бэкапов — автоматическое обнаружение резервных копий баз данных в заданной папке
-
Гибкая настройка именования — возможность переименования баз при восстановлении
-
Управление дисковым пространством — автоматическое освобождение места при необходимости
-
Параллельное восстановление — одновременное восстановление нескольких баз для ускорения процесса
-
Проверка целостности — автоматическая проверка восстановленных баз (DBCC CHECKDB)
-
Логирование и отчетность — детальное логирование и отправка отчетов по email
-
Удаление старых баз — различные стратегии удаления существующих баз перед восстановлением
Режимы управления пространством:
-
Calculate/Smart — удаляет только минимально необходимые базы
-
All — удаляет все базы (кроме системных)
-
None — не удаляет ничего
Основные этапы выполнения:
-
Инициализация — загрузка модулей, проверка параметров
-
Сканирование — поиск резервных копий в указанной папке
-
Анализ места — проверка свободного дискового пространства
-
Удаление баз (опционально) — освобождение места при необходимости
-
Восстановление — параллельное восстановление баз данных
-
Проверка — проверка целостности восстановленных баз
-
Отчетность — логирование и отправка отчетов
-
Очистка — удаление старых логов
Создадим базу для мониторинга выполнения скрипта
USE [master]
GO
/****** Object: Database [dba] Script Date: 24.12.2025 12:52:53 ******/
CREATE DATABASE [dba]
CONTAINMENT = NONE
ON PRIMARY
( NAME = N'dba', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQL\DATA\dba.mdf' , SIZE = 73728KB , MAXSIZE = UNLIMITED, FILEGROWTH = 65536KB )
LOG ON
( NAME = N'dba_log', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQL\DATA\dba_log.ldf' , SIZE = 73728KB , MAXSIZE = 2048GB , FILEGROWTH = 65536KB )
GO
IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [dba].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO
ALTER DATABASE [dba] SET ANSI_NULL_DEFAULT OFF
GO
ALTER DATABASE [dba] SET ANSI_NULLS OFF
GO
ALTER DATABASE [dba] SET ANSI_PADDING OFF
GO
ALTER DATABASE [dba] SET ANSI_WARNINGS OFF
GO
ALTER DATABASE [dba] SET ARITHABORT OFF
GO
ALTER DATABASE [dba] SET AUTO_CLOSE OFF
GO
ALTER DATABASE [dba] SET AUTO_SHRINK OFF
GO
ALTER DATABASE [dba] SET AUTO_UPDATE_STATISTICS ON
GO
ALTER DATABASE [dba] SET CURSOR_CLOSE_ON_COMMIT OFF
GO
ALTER DATABASE [dba] SET CURSOR_DEFAULT GLOBAL
GO
ALTER DATABASE [dba] SET CONCAT_NULL_YIELDS_NULL OFF
GO
ALTER DATABASE [dba] SET NUMERIC_ROUNDABORT OFF
GO
ALTER DATABASE [dba] SET QUOTED_IDENTIFIER OFF
GO
ALTER DATABASE [dba] SET RECURSIVE_TRIGGERS OFF
GO
ALTER DATABASE [dba] SET ENABLE_BROKER
GO
ALTER DATABASE [dba] SET AUTO_UPDATE_STATISTICS_ASYNC OFF
GO
ALTER DATABASE [dba] SET DATE_CORRELATION_OPTIMIZATION OFF
GO
ALTER DATABASE [dba] SET TRUSTWORTHY OFF
GO
ALTER DATABASE [dba] SET ALLOW_SNAPSHOT_ISOLATION OFF
GO
ALTER DATABASE [dba] SET PARAMETERIZATION SIMPLE
GO
ALTER DATABASE [dba] SET READ_COMMITTED_SNAPSHOT OFF
GO
ALTER DATABASE [dba] SET HONOR_BROKER_PRIORITY OFF
GO
ALTER DATABASE [dba] SET RECOVERY FULL
GO
ALTER DATABASE [dba] SET MULTI_USER
GO
ALTER DATABASE [dba] SET PAGE_VERIFY CHECKSUM
GO
ALTER DATABASE [dba] SET DB_CHAINING OFF
GO
ALTER DATABASE [dba] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF )
GO
ALTER DATABASE [dba] SET TARGET_RECOVERY_TIME = 60 SECONDS
GO
ALTER DATABASE [dba] SET DELAYED_DURABILITY = DISABLED
GO
ALTER DATABASE [dba] SET QUERY_STORE = OFF
GO
ALTER DATABASE [dba] SET READ_WRITE
GO
Таблицы
USE [dba]
GO
/****** Object: Table [dbo].[DBCCResults] Script Date: 24.12.2025 12:55:13 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[DBCCResults](
[ID] [int] IDENTITY(1,1) NOT NULL,
[DatabaseName] [nvarchar](128) NOT NULL,
[CheckDate] [datetime] NOT NULL,
[Command] [nvarchar](max) NULL,
[Outcome] [nvarchar](50) NULL,
[DurationSeconds] [int] NULL,
[ErrorCount] [int] NULL,
[ConsistencyErrorCount] [int] NULL,
[LogInfo] [nvarchar](max) NULL,
[Log] [nvarchar](max) NULL,
CONSTRAINT [PK_DBCCResults] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[DBCCResults] ADD DEFAULT (getdate()) FOR [CheckDate]
GO
USE [dba]
GO
/****** Object: Table [dbo].[RestoreLog] Script Date: 24.12.2025 12:55:52 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[RestoreLog](
[Id] [int] IDENTITY(1,1) NOT NULL,
[DatabaseName] [nvarchar](128) NOT NULL,
[OriginalDatabaseName] [nvarchar](128) NULL,
[RestoreDate] [datetime] NOT NULL,
[DurationSeconds] [int] NULL,
[Status] [nvarchar](50) NOT NULL,
[Details] [nvarchar](max) NULL,
[LogFilePath] [nvarchar](500) NULL,
[BackupType] [nvarchar](50) NULL,
[WasDropped] [bit] NULL,
[FinalStatus] [nvarchar](50) NULL,
[EstimatedSizeMB] [decimal](18, 2) NULL,
[DeletionMode] [nvarchar](50) NULL,
[SegmentName] [nvarchar](128) NULL,
CONSTRAINT [PK_RestoreLog] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[RestoreLog] ADD DEFAULT (getdate()) FOR [RestoreDate]
GO
Сам скрипт восстановление на Powershell 7 и dbatools и использования для проверки целостности Ola Hallengren
# Подключаем модуль dbatools
Import-Module dbatools -Force
# --- ПАРАМЕТРЫ ---
# Основные пути и серверы
$SourceBackupRoot = "\\arc03.admibd.ru\MSK-DL02`$msk-g02"
$TargetServer = "db01"
$DataPath = "F:\data"
$LogPath = "e:\log"
# ФИЛЬТР по имени базы
#$DatabaseFilter = @("AI111")
$DatabaseFilter = @()
# Базы для исключения из восстановления
$ExcludedDatabases = @('master', 'model', 'msdb', 'tempdb')
# Папки серверов для исключения
$ExcludedServerFolders = @('DBAtools', 'TMP')
# Базы, которые НЕ удалять на целевом сервере
$DatabasesToExcludeFromDeletion = @('master', 'model', 'msdb', 'tempdb', 'dba')
# НАСТРОЙКА ФОРМИРОВАНИЯ ИМЕНИ БАЗЫ
$PathSegmentFromEnd = 2 # Берем вторую папку с конца (перед именем базы)
# РЕЖИМ УДАЛЕНИЯ
$DeletionMode = "Smart" # Calculate | All | None | Smart
# КОЭФФИЦИЕНТ РАСШИРЕНИЯ БЭКАПА (для учета сжатия)
$BackupExpansionFactor = 7 # Например, 172 GB бэкап → 1165 GB база (172 * 6.8 ≈ 1170)
# Параметры параллельности
$RestoreThrottleLimit = 5
$DeletionThrottleLimit = 10
# Проверка целостности
$RunIntegrityCheck = $true
$CheckThrottleLimit = 1
# Параметры повторной проверки
$MaxRetryAttempts = 3
$RetryDelaySeconds = 5
# Очистка логов Ola Hallengren
$OlaLogsCleanupDays = 14
# Email
$SmtpServer = "tech.admibd.ru"
$SmtpPort = 25
$EmailFrom = "db01@tech.admibd.ru"
$EmailTo = "konstantin.moskvichev@admibd.ru"
$EmailEncoding = "UTF8"
$SendEmailReport = $true
# Транскрипт
$TranscriptPath = "C:\temp\RestoreTranscript_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
# Время ожидания между операциями
$WaitAfterDeletion = 30 # секунд
# Очистка логов
$CleanupOldLogs = $true
$LogCleanupDays = 7
# --- КОНЕЦ ПАРАМЕТРОВ ---
# Создаём папку для логов, если не существует
if (-not (Test-Path "C:\temp")) { New-Item -ItemType Directory -Path "C:\temp" -Force | Out-Null }
# Запуск транскрипта
Start-Transcript -Path $TranscriptPath -Force
# Функция логирования
function Write-Log {
param([string]$Message)
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$LogEntry = "[$Timestamp] $Message"
Write-Host $LogEntry
}
# Функция отправки email
function Send-EmailReport {
param(
[string]$Subject,
[string]$Body,
[bool]$IsHtml = $false,
[array]$Attachments = @()
)
try {
if ($SendEmailReport) {
$mailParams = @{
SmtpServer = $SmtpServer
Port = $SmtpPort
From = $EmailFrom
To = $EmailTo
Subject = $Subject
Body = $Body
Encoding = $EmailEncoding
ErrorAction = 'Stop'
}
if ($IsHtml) { $mailParams['BodyAsHtml'] = $true }
if ($Attachments.Count -gt 0) { $mailParams['Attachments'] = $Attachments }
# Останавливаем транскрипт перед отправкой
try {
Stop-Transcript
Write-Host "Транскрипт остановлен для отправки email" -ForegroundColor Yellow
} catch {
Write-Host "Ошибка остановки транскрипта: $($_.Exception.Message)" -ForegroundColor Yellow
}
Send-MailMessage @mailParams
Write-Host "Email отчет отправлен успешно" -ForegroundColor Green
# Запускаем транскрипт снова
try {
Start-Transcript -Path $TranscriptPath -Append
Write-Host "Транскрипт возобновлен" -ForegroundColor Yellow
} catch {
Write-Host "Ошибка возобновления транскрипта: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
} catch {
$errorMsg = "Ошибка отправки email: $($_.Exception.Message)"
Write-Host $errorMsg -ForegroundColor Red
# Пытаемся возобновить транскрипт даже при ошибке
try {
Start-Transcript -Path $TranscriptPath -Append
Write-Host "Транскрипт возобновлен после ошибки email" -ForegroundColor Yellow
} catch {
Write-Host "Ошибка возобновления транскрипта: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
}
# Функция для проверки соответствия базы фильтру
function Test-DatabaseFilter {
param([string]$DatabaseName, [array]$Filter)
if ($Filter.Count -eq 0) { return $true }
foreach ($filterItem in $Filter) {
if ($filterItem -like "*`**") {
if ($DatabaseName -like $filterItem) { return $true }
} else {
if ($DatabaseName -eq $filterItem) { return $true }
}
}
return $false
}
# УНИВЕРСАЛЬНАЯ функция сканирования бэкапов
function Get-BackupDatabases {
param(
[string]$BackupRootPath,
[array]$DatabaseFilter,
[array]$ExcludedDbs,
[array]$ExcludedServers,
[int]$PathSegmentFromEnd = 2
)
Write-Log "Сканирование структуры бэкапов в: $BackupRootPath"
Write-Log "Используемый сегмент пути от конца: $PathSegmentFromEnd"
Write-Log "Коэффициент расширения бэкапа: $BackupExpansionFactor"
$databaseList = @()
try {
# Проверяем доступность пути
if (-not (Test-Path $BackupRootPath)) {
throw "Путь $BackupRootPath не существует или недоступен!"
}
Write-Log "Поиск папок с подпапкой FULL (рекурсивно)..."
# Рекурсивно ищем все папки
$allFolders = Get-ChildItem -Path $BackupRootPath -Directory -Recurse -ErrorAction SilentlyContinue
Write-Log "Всего найдено папок: $($allFolders.Count)"
# Собираем уникальные папки баз данных (те, у которых есть папка FULL внутри)
$databaseFolders = @()
foreach ($folder in $allFolders) {
# Проверяем, что это не служебная папка
$folderName = $folder.Name
if ($folderName -in @('FULL', 'DIFF', 'LOG', 'FULL_1', 'DIFF_1', 'LOG_1')) {
continue
}
# Проверяем, есть ли внутри папка FULL
$fullPath = Join-Path $folder.FullName "FULL"
if (Test-Path $fullPath) {
# Это папка базы данных
$databaseFolders += $folder
}
}
Write-Log "Найдено папок баз данных (с папкой FULL): $($databaseFolders.Count)"
foreach ($dbFolder in $databaseFolders) {
$dbName = $dbFolder.Name
# Пропускаем исключенные базы
if ($dbName -in $ExcludedDbs) {
Write-Log " База $dbName пропущена - в списке исключений"
continue
}
# Проверяем фильтр базы
if (-not (Test-DatabaseFilter -DatabaseName $dbName -Filter $DatabaseFilter)) {
Write-Log " База $dbName пропущена - не соответствует фильтру"
continue
}
# Ищем FULL бэкап
$fullBackupPath = Join-Path $dbFolder.FullName "FULL"
$fullBackupFile = $null
$backupSizeMB = 0
if (Test-Path $fullBackupPath) {
$fullBackups = Get-ChildItem -Path $fullBackupPath -Filter "*.bak" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending
if ($fullBackups) {
$fullBackupFile = $fullBackups[0]
# ПОЛУЧАЕМ РЕАЛЬНЫЙ РАЗМЕР ФАЙЛА БЭКАПА
try {
$backupSizeBytes = $fullBackupFile.Length
$backupSizeMB = [math]::Round($backupSizeBytes / 1MB, 2)
$backupSizeGB = [math]::Round($backupSizeBytes / 1GB, 2)
Write-Log " Размер бэкапа: $backupSizeMB MB ($backupSizeGB GB)"
} catch {
Write-Log " Не удалось определить размер бэкапа, используется оценка 1024 MB"
$backupSizeMB = 1024 # Значение по умолчанию
}
}
}
if ($fullBackupFile) {
# Ищем DIFF бэкап и его размер
$diffBackupFile = $null
$diffBackupSizeMB = 0
$diffBackupPath = Join-Path $dbFolder.FullName "DIFF"
if (Test-Path $diffBackupPath) {
$diffBackups = Get-ChildItem -Path $diffBackupPath -Filter "*.bak" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending
if ($diffBackups) {
$diffBackupFile = $diffBackups[0]
# Получаем размер DIFF бэкапа
try {
$diffSizeBytes = $diffBackupFile.Length
$diffBackupSizeMB = [math]::Round($diffSizeBytes / 1MB, 2)
} catch {
Write-Log " Не удалось определить размер DIFF бэкапа"
}
}
}
# Формируем полный путь к папке базы для определения сегментов
$dbFolderPath = $dbFolder.FullName.Trim('\')
$dbPathParts = $dbFolderPath.Split('\')
Write-Log " Найдена база: $dbName"
Write-Log " Полный путь: $dbFolderPath"
Write-Log " Части пути: $($dbPathParts -join ' | ')"
# Определяем имя сегмента (отсчет от конца пути)
$segmentName = "Unknown"
if ($dbPathParts.Count -ge $PathSegmentFromEnd) {
$segmentName = $dbPathParts[-$PathSegmentFromEnd]
# Очищаем имя сегмента (убираем $ и все после него)
if ($segmentName -contains '$') {
$segmentName = $segmentName.Split('$')[0]
}
# Формируем имя базы: сегмент_имя_базы
$databaseNameOnTarget = "${segmentName}_${dbName}"
Write-Log " Сегмент #$PathSegmentFromEnd с конца: $segmentName"
Write-Log " Имя на целевом сервере: $databaseNameOnTarget"
} else {
$warningMsg = "Путь к базе $dbName содержит только $($dbPathParts.Count) частей, а запрошен сегмент $PathSegmentFromEnd. Используем имя базы как есть."
Write-Log " ВНИМАНИЕ: $warningMsg"
# Используем имя базы как есть
$databaseNameOnTarget = $dbName
}
# Проверяем, что имя не слишком длинное (ограничение SQL Server - 128 символов)
if ($databaseNameOnTarget.Length -gt 128) {
Write-Log " Внимание: имя базы слишком длинное ($($databaseNameOnTarget.Length) символов), будет обрезано"
$databaseNameOnTarget = $databaseNameOnTarget.Substring(0, 128)
}
$backupInfo = [PSCustomObject]@{
OriginalDatabaseName = $dbName
ServerName = $segmentName
DatabaseName = $databaseNameOnTarget
FullBackupFile = $fullBackupFile.FullName
DiffBackupFile = if ($diffBackupFile) { $diffBackupFile.FullName } else { $null }
HasDiffBackup = [bool]$diffBackupFile
FullBackupDate = $fullBackupFile.LastWriteTime
DiffBackupDate = if ($diffBackupFile) { $diffBackupFile.LastWriteTime } else { $null }
BackupSizeMB = $backupSizeMB # РЕАЛЬНЫЙ размер FULL бэкапа
DiffBackupSizeMB = $diffBackupSizeMB # РЕАЛЬНЫЙ размер DIFF бэкапа
TotalBackupSizeMB = if ($diffBackupFile) {
[math]::Round($backupSizeMB + $diffBackupSizeMB)
} else {
$backupSizeMB
}
# РАСШИРЕННЫЙ размер (с учетом коэффициента сжатия)
ExpandedSizeMB = if ($diffBackupFile) {
[math]::Round(($backupSizeMB + $diffBackupSizeMB) * $BackupExpansionFactor)
} else {
[math]::Round($backupSizeMB * $BackupExpansionFactor)
}
}
$databaseList += $backupInfo
Write-Log " ✓ Бэкап найден: $databaseNameOnTarget (оригинал: $dbName, тип: $($backupInfo.HasDiffBackup ? 'FULL+DIFF' : 'FULL'))"
Write-Log " Файл бэкапа: $($fullBackupFile.Name)"
Write-Log " Размер бэкапа: $backupSizeMB MB ($([math]::Round($backupSizeMB/1024, 2)) GB)"
Write-Log " Расширенный размер базы: $($backupInfo.ExpandedSizeMB) MB ($([math]::Round($backupInfo.ExpandedSizeMB/1024, 2)) GB)"
Write-Log " Дата бэкапа: $($fullBackupFile.LastWriteTime)"
} else {
Write-Log " База $dbName пропущена - нет файлов .bak в папке FULL"
}
}
} catch {
$ErrorMessage = "Ошибка при сканировании структуры бэкапов: $($_.Exception.Message)"
Write-Log $ErrorMessage
throw
}
Write-Log "Всего найдено баз с бэкапами: $($databaseList.Count)"
# Считаем общий реальный размер бэкапов и расширенный размер
$totalBackupSizeMB = ($databaseList | Measure-Object -Property TotalBackupSizeMB -Sum).Sum
$totalBackupSizeGB = [math]::Round($totalBackupSizeMB / 1024, 2)
$totalExpandedSizeMB = ($databaseList | Measure-Object -Property ExpandedSizeMB -Sum).Sum
$totalExpandedSizeGB = [math]::Round($totalExpandedSizeMB / 1024, 2)
Write-Log "Общий размер всех бэкапов: $totalBackupSizeMB MB ($totalBackupSizeGB GB)"
Write-Log "Общий расширенный размер баз после восстановления: $totalExpandedSizeMB MB ($totalExpandedSizeGB GB)"
# Логируем детали найденных баз
if ($databaseList.Count -gt 0) {
Write-Log "Найденные базы для восстановления:"
foreach ($db in $databaseList) {
$backupSizeGB = [math]::Round($db.BackupSizeMB / 1024, 2)
$expandedSizeGB = [math]::Round($db.ExpandedSizeMB / 1024, 2)
Write-Log " - $($db.DatabaseName) (оригинал: $($db.OriginalDatabaseName), сервер: $($db.ServerName))"
Write-Log " Бэкап: $backupSizeGB GB → База: ~$expandedSizeGB GB (x$BackupExpansionFactor)"
}
}
return $databaseList
}
# Удаление всех баз кроме исключённых (параллельно)
function Remove-AllDatabasesParallel {
param([string]$TargetServer, [array]$DatabasesToExcludeFromDeletion, [int]$ThrottleLimit)
Write-Log "=== Начало удаления ВСЕХ баз кроме исключенных ==="
try {
$TargetConnection = Connect-DbaInstance -SqlInstance $TargetServer -TrustServerCertificate
$AllDatabases = $TargetConnection | Get-DbaDatabase
$DatabasesToDelete = $AllDatabases | Where-Object {
$_.Name -notin $DatabasesToExcludeFromDeletion -and $_.Status -eq 'Normal'
} | ForEach-Object {
[PSCustomObject]@{
DatabaseName = $_.Name
SizeMB = [math]::Round($_.Size, 2)
DataSpaceMB = [math]::Round($_.DataSpaceUsage.Megabyte, 2)
LogSpaceMB = [math]::Round($_.LogSpaceUsage.Megabyte, 2)
}
}
$TargetConnection.ConnectionContext.Disconnect()
Write-Log "Найдено баз для удаления: $($DatabasesToDelete.Count)"
if ($DatabasesToDelete.Count -eq 0) {
Write-Log "Нет баз для удаления."
return @()
}
Write-Log "Будет удалено $($DatabasesToDelete.Count) баз:"
$deleteResults = $DatabasesToDelete | ForEach-Object -Parallel {
$dbInfo = $_
$targetServer = $using:TargetServer
Import-Module dbatools -Force
$startTime = Get-Date
try {
Remove-DbaDatabase -SqlInstance $targetServer -Database $dbInfo.DatabaseName -Confirm:$false | Out-Null
$endTime = Get-Date
$duration = $endTime - $startTime
return [PSCustomObject]@{
DatabaseName = $dbInfo.DatabaseName
Status = "Успешно"
Details = "Удалена за $("{0:hh\:mm\:ss}" -f $duration)"
SizeMB = $dbInfo.SizeMB
DataSpaceMB = $dbInfo.DataSpaceMB
LogSpaceMB = $dbInfo.LogSpaceMB
}
} catch {
$endTime = Get-Date
return [PSCustomObject]@{
DatabaseName = $dbInfo.DatabaseName
Status = "Ошибка"
Details = "Ошибка удаления: $($_.Exception.Message)"
SizeMB = $dbInfo.SizeMB
DataSpaceMB = $dbInfo.DataSpaceMB
LogSpaceMB = $dbInfo.LogSpaceMB
}
}
} -ThrottleLimit $ThrottleLimit
Write-Log "Параллельное удаление завершено"
$SuccessCount = ($deleteResults | Where-Object { $_.Status -eq 'Успешно' }).Count
$ErrorCount = ($deleteResults | Where-Object { $_.Status -eq 'Ошибка' }).Count
Write-Log "Результаты удаления: успешно - $SuccessCount, с ошибкой - $ErrorCount"
if ($SuccessCount -gt 0) {
Write-Log "Ожидание ${WaitAfterDeletion} секунд для освобождения места..."
Start-Sleep -Seconds $WaitAfterDeletion
$DataDriveLetter = ($DataPath -split ':')[0]
$FreeSpaceAfter = [math]::Round((Get-PSDrive -Name $DataDriveLetter).Free / 1MB, 2)
Write-Log "Свободное место после удаления: $FreeSpaceAfter MB"
}
Write-Log "=== Удаление всех баз завершено ==="
return $deleteResults
} catch {
$ErrorMessage = "Ошибка при удалении всех баз: $($_.Exception.Message)"
Write-Log $ErrorMessage
throw
}
}
# Умное удаление только необходимых баз (от самых маленьких к большим)
function Remove-DatabasesSmart {
param(
[string]$TargetServer,
[array]$DatabasesToExcludeFromDeletion,
[array]$DatabasesToRestore, # Массив баз для восстановления
[int]$ThrottleLimit,
[double]$RequiredSpaceMB # Необходимый объем пространства
)
Write-Log "=== Начало умного удаления баз ==="
Write-Log "Требуется освободить: $RequiredSpaceMB MB ($([math]::Round($RequiredSpaceMB/1024, 2)) GB)"
try {
# Получаем все базы на сервере, сортируем по размеру (от маленьких к большим)
$TargetConnection = Connect-DbaInstance -SqlInstance $TargetServer -TrustServerCertificate
$AllDatabases = $TargetConnection | Get-DbaDatabase | Where-Object {
$_.Name -notin $DatabasesToExcludeFromDeletion -and $_.Status -eq 'Normal'
} | ForEach-Object {
[PSCustomObject]@{
DatabaseName = $_.Name
SizeMB = [math]::Round($_.Size, 2)
DataSpaceMB = [math]::Round($_.DataSpaceUsage.Megabyte, 2)
LogSpaceMB = [math]::Round($_.LogSpaceUsage.Megabyte, 2)
}
} | Sort-Object SizeMB # Сортируем от самых маленьких к большим
$TargetConnection.ConnectionContext.Disconnect()
Write-Log "Найдено кандидатов на удаление: $($AllDatabases.Count)"
if ($AllDatabases.Count -eq 0) {
Write-Log "Нет баз для удаления."
return @()
}
# Отображаем размеры баз
Write-Log "Размеры баз для возможного удаления (отсортированы по возрастанию):"
foreach ($db in $AllDatabases) {
$dbSizeGB = [math]::Round($db.SizeMB / 1024, 2)
Write-Log " $($db.DatabaseName): $($db.SizeMB) MB ($dbSizeGB GB) (Данные: $($db.DataSpaceMB) MB, Логи: $($db.LogSpaceMB) MB)"
}
# Планируем удаление: удаляем по одной базе, пока не наберем нужный объем
$databasesToDelete = @()
$freedSpaceMB = 0
foreach ($db in $AllDatabases) {
if ($freedSpaceMB -ge $RequiredSpaceMB) {
Write-Log " Уже освобождено достаточно места: $freedSpaceMB MB / требуется: $RequiredSpaceMB MB"
break # Уже освободили достаточно места
}
# Проверяем, не восстанавливаем ли мы эту же базу (чтобы не удалить то, что собираемся восстановить)
$isBeingRestored = $DatabasesToRestore | Where-Object { $_.DatabaseName -eq $db.DatabaseName }
if ($isBeingRestored) {
Write-Log " Пропускаем $($db.DatabaseName) - эта база будет восстановлена"
continue
}
$databasesToDelete += $db
$freedSpaceMB += $db.SizeMB
$freedSpaceGB = [math]::Round($freedSpaceMB / 1024, 2)
$requiredSpaceGB = [math]::Round($RequiredSpaceMB / 1024, 2)
Write-Log " Запланировано удаление: $($db.DatabaseName) ($([math]::Round($db.SizeMB/1024, 2)) GB). Всего освобождено: $freedSpaceGB GB / требуется: $requiredSpaceGB GB"
}
if ($databasesToDelete.Count -eq 0) {
Write-Log "Нет подходящих баз для удаления."
return @()
}
Write-Log "Будет удалено $($databasesToDelete.Count) баз для освобождения $([math]::Round($freedSpaceMB/1024, 2)) GB места:"
foreach ($db in $databasesToDelete) {
$dbSizeGB = [math]::Round($db.SizeMB / 1024, 2)
Write-Log " - $($db.DatabaseName) ($dbSizeGB GB)"
}
# Удаляем запланированные базы
$deleteResults = $databasesToDelete | ForEach-Object -Parallel {
$dbInfo = $_
$targetServer = $using:TargetServer
Import-Module dbatools -Force
$startTime = Get-Date
try {
Remove-DbaDatabase -SqlInstance $targetServer -Database $dbInfo.DatabaseName -Confirm:$false | Out-Null
$endTime = Get-Date
$duration = $endTime - $startTime
return [PSCustomObject]@{
DatabaseName = $dbInfo.DatabaseName
Status = "Успешно"
Details = "Удалена за $("{0:hh\:mm\:ss}" -f $duration)"
SizeMB = $dbInfo.SizeMB
DataSpaceMB = $dbInfo.DataSpaceMB
LogSpaceMB = $dbInfo.LogSpaceMB
}
} catch {
$endTime = Get-Date
return [PSCustomObject]@{
DatabaseName = $dbInfo.DatabaseName
Status = "Ошибка"
Details = "Ошибка удаления: $($_.Exception.Message)"
SizeMB = $dbInfo.SizeMB
DataSpaceMB = $dbInfo.DataSpaceMB
LogSpaceMB = $dbInfo.LogSpaceMB
}
}
} -ThrottleLimit $ThrottleLimit
Write-Log "Умное удаление завершено"
$SuccessCount = ($deleteResults | Where-Object { $_.Status -eq 'Успешно' }).Count
$ErrorCount = ($deleteResults | Where-Object { $_.Status -eq 'Ошибка' }).Count
Write-Log "Результаты удаления: успешно - $SuccessCount, с ошибкой - $ErrorCount"
if ($SuccessCount -gt 0) {
Write-Log "Ожидание ${WaitAfterDeletion} секунд для освобождения места..."
Start-Sleep -Seconds $WaitAfterDeletion
# Проверяем, сколько места освободили
$DataDriveLetter = ($DataPath -split ':')[0]
$FreeSpaceAfter = [math]::Round((Get-PSDrive -Name $DataDriveLetter).Free / 1MB, 2)
$FreeSpaceAfterGB = [math]::Round($FreeSpaceAfter / 1024, 2)
# Вычисляем сколько места было до удаления (приблизительно)
$totalFreedMB = ($deleteResults | Where-Object { $_.Status -eq 'Успешно' } | Measure-Object -Property SizeMB -Sum).Sum
$totalFreedGB = [math]::Round($totalFreedMB / 1024, 2)
Write-Log "Освобождено места: $totalFreedMB MB ($totalFreedGB GB)"
Write-Log "Свободное место после удаления: $FreeSpaceAfter MB ($FreeSpaceAfterGB GB)"
}
Write-Log "=== Умное удаление баз завершено ==="
return $deleteResults
} catch {
$ErrorMessage = "Ошибка при умном удалении баз: $($_.Exception.Message)"
Write-Log $ErrorMessage
throw
}
}
# Режим Calculate - удаление только если не хватает места
function Calculate-SpaceAndDelete {
param(
[string]$TargetServer,
[string]$DataPath,
[array]$DatabasesInfo,
[array]$DatabasesToExcludeFromDeletion,
[int]$ThrottleLimit
)
Write-Log "=== Проверка места и расчет необходимости удаления (режим Calculate) ==="
try {
# 1. Проверяем текущее свободное место
$DataDriveLetter = ($DataPath -split ':')[0]
$CurrentFreeSpaceMB = [math]::Round((Get-PSDrive -Name $DataDriveLetter).Free / 1MB, 2)
$CurrentFreeSpaceGB = [math]::Round($CurrentFreeSpaceMB / 1024, 2)
$CurrentFreeSpaceTB = [math]::Round($CurrentFreeSpaceMB / 1048576, 2)
Write-Log "Свободное место на $DataDriveLetter : $CurrentFreeSpaceMB MB ($CurrentFreeSpaceGB GB / $CurrentFreeSpaceTB TB)"
# 2. Используем РАСШИРЕННЫЕ размеры баз (с учетом коэффициента сжатия)
$TotalExpandedSizeMB = 0
foreach ($db in $DatabasesInfo) {
$dbSize = $db.ExpandedSizeMB
$TotalExpandedSizeMB += $dbSize
$dbSizeGB = [math]::Round($dbSize / 1024, 2)
$backupSizeGB = [math]::Round($db.BackupSizeMB / 1024, 2)
Write-Log " $($db.DatabaseName): бэкап $backupSizeGB GB → база ~$dbSizeGB GB (x$BackupExpansionFactor)"
}
$TotalExpandedSizeGB = [math]::Round($TotalExpandedSizeMB / 1024, 2)
Write-Log "Общий РАСШИРЕННЫЙ размер для восстановления $($DatabasesInfo.Count) баз: $TotalExpandedSizeMB MB ($TotalExpandedSizeGB GB)"
# 3. Проверяем, достаточно ли места с учетом реального размера баз после восстановления
# Добавляем запас 10% для индексов и роста
$RequiredSpaceMB = $TotalExpandedSizeMB * 1.1
$RequiredSpaceGB = [math]::Round($RequiredSpaceMB / 1024, 2)
Write-Log "Требуется с запасом 10%: $RequiredSpaceMB MB ($RequiredSpaceGB GB)"
Write-Log "Доступно: $CurrentFreeSpaceMB MB ($CurrentFreeSpaceGB GB)"
if ($CurrentFreeSpaceMB -gt $RequiredSpaceMB) {
Write-Log "Места достаточно для восстановления. Удаление не требуется."
Write-Log "Доступно: $CurrentFreeSpaceMB MB > Требуется: $RequiredSpaceMB MB"
return @()
}
# 4. Рассчитываем, сколько места нужно освободить
$SpaceDeficitMB = $RequiredSpaceMB - $CurrentFreeSpaceMB
$SpaceDeficitGB = [math]::Round($SpaceDeficitMB / 1024, 2)
Write-Log "Места недостаточно. Требуется освободить: $SpaceDeficitMB MB ($SpaceDeficitGB GB)"
# 5. Используем умное удаление
Write-Log "Запуск умного удаления баз..."
return Remove-DatabasesSmart -TargetServer $TargetServer `
-DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
-DatabasesToRestore $DatabasesInfo `
-ThrottleLimit $ThrottleLimit `
-RequiredSpaceMB $SpaceDeficitMB
} catch {
$ErrorMessage = "Ошибка при проверке места: $($_.Exception.Message)"
Write-Log $ErrorMessage
throw
}
}
# Функция проверки и создания таблицы RestoreLog
function Ensure-RestoreLogTable {
param([string]$TargetServer)
Write-Log "Проверка и создание таблицы RestoreLog..."
$checkAndCreateTable = @"
-- Проверяем существование таблицы
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dba].[dbo].[RestoreLog]') AND type in (N'U'))
BEGIN
CREATE TABLE [dba].[dbo].[RestoreLog](
[Id] [int] IDENTITY(1,1) NOT NULL,
[DatabaseName] [nvarchar](128) NOT NULL,
[OriginalDatabaseName] [nvarchar](128) NOT NULL,
[RestoreDate] [datetime] NOT NULL,
[DurationSeconds] [float] NULL,
[Status] [nvarchar](50) NOT NULL,
[Details] [nvarchar](max) NULL,
[LogFilePath] [nvarchar](500) NULL,
[BackupType] [nvarchar](50) NULL,
[WasDropped] [bit] NULL,
[FinalStatus] [nvarchar](50) NULL,
[EstimatedSizeMB] [float] NULL,
[DeletionMode] [nvarchar](50) NULL,
[SegmentName] [nvarchar](128) NULL,
CONSTRAINT [PK_RestoreLog] PRIMARY KEY CLUSTERED ([Id] ASC)
)
PRINT 'Таблица RestoreLog создана.'
END
ELSE
BEGIN
-- Проверяем наличие столбца SegmentName
IF COL_LENGTH('dba.dbo.RestoreLog', 'SegmentName') IS NULL
BEGIN
ALTER TABLE [dba].[dbo].[RestoreLog] ADD [SegmentName] [nvarchar](128) NULL;
PRINT 'Столбец SegmentName добавлен в таблицу RestoreLog.'
END
PRINT 'Таблица RestoreLog уже существует.'
END
"@
try {
Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $checkAndCreateTable -EnableException
Write-Log "Таблица RestoreLog проверена/создана успешно"
return $true
} catch {
Write-Log "Ошибка при проверке/создании таблицы RestoreLog: $($_.Exception.Message)"
return $false
}
}
# Функция записи результата восстановления в таблицу
function Write-RestoreLogToDb {
param(
[string]$TargetServer,
[string]$DatabaseName,
[string]$OriginalDatabaseName,
[string]$Status,
[string]$Details,
[string]$BackupType,
[bool]$WasDropped,
[string]$LogFilePath,
[double]$DurationSeconds,
[string]$FinalStatus,
[double]$EstimatedSizeMB,
[string]$DeletionMode,
[string]$SegmentName
)
# Проверяем, что имя базы не пустое
if ([string]::IsNullOrEmpty($DatabaseName)) {
Write-Log "Пропуск записи в RestoreLog: пустое имя базы"
return $false
}
# Проверяем, что база dba существует
try {
$dbCheck = Get-DbaDatabase -SqlInstance $TargetServer -Database "dba" -ErrorAction Stop
if (-not $dbCheck) {
Write-Log "База dba не существует на сервере $TargetServer"
return $false
}
} catch {
Write-Log "Ошибка проверки базы dba: $($_.Exception.Message)"
return $false
}
# Укорачиваем Details если слишком длинные
$safeDetails = if ($Details.Length -gt 4000) { $Details.Substring(0, 4000) } else { $Details }
$safeDetails = $safeDetails -replace "'", "''"
$safeLogFilePath = $LogFilePath -replace "'", "''"
$safeSegmentName = if ($SegmentName) { $SegmentName -replace "'", "''" } else { '' }
# Преобразуем WasDropped в bit (1 или 0)
$wasDroppedBit = if ($WasDropped) { 1 } else { 0 }
# Проверяем наличие столбца SegmentName
$checkColumnQuery = @"
IF COL_LENGTH('dba.dbo.RestoreLog', 'SegmentName') IS NOT NULL
SELECT 'SegmentName' as ColumnName
ELSE
SELECT '' as ColumnName
"@
try {
$columnCheck = Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $checkColumnQuery -EnableException
$hasSegmentNameColumn = ($columnCheck.ColumnName -eq 'SegmentName')
if ($hasSegmentNameColumn) {
$query = @"
INSERT INTO [dba].[dbo].[RestoreLog]
([DatabaseName], [OriginalDatabaseName], [RestoreDate], [DurationSeconds], [Status], [Details],
[LogFilePath], [BackupType], [WasDropped], [FinalStatus], [EstimatedSizeMB], [DeletionMode], [SegmentName])
VALUES
('$DatabaseName', '$OriginalDatabaseName', GETDATE(), $DurationSeconds, '$Status', '$safeDetails',
'$safeLogFilePath', '$BackupType', $wasDroppedBit, '$FinalStatus', $EstimatedSizeMB, '$DeletionMode', '$safeSegmentName')
"@
} else {
$query = @"
INSERT INTO [dba].[dbo].[RestoreLog]
([DatabaseName], [OriginalDatabaseName], [RestoreDate], [DurationSeconds], [Status], [Details],
[LogFilePath], [BackupType], [WasDropped], [FinalStatus], [EstimatedSizeMB], [DeletionMode])
VALUES
('$DatabaseName', '$OriginalDatabaseName', GETDATE(), $DurationSeconds, '$Status', '$safeDetails',
'$safeLogFilePath', '$BackupType', $wasDroppedBit, '$FinalStatus', $EstimatedSizeMB, '$DeletionMode')
"@
}
$result = Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $query -ErrorAction Stop
Write-Log "Запись в RestoreLog для $DatabaseName выполнена успешно"
return $true
} catch {
$errorMsg = "Ошибка записи в RestoreLog для $DatabaseName : $($_.Exception.Message)"
Write-Log $errorMsg
# Пытаемся создать таблицу и повторить вставку
try {
Write-Log "Попытка создать таблицу RestoreLog..."
$tableCreated = Ensure-RestoreLogTable -TargetServer $TargetServer
if ($tableCreated) {
# Повторяем попытку вставки с проверкой наличия столбца
$columnCheck = Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $checkColumnQuery -EnableException
$hasSegmentNameColumn = ($columnCheck.ColumnName -eq 'SegmentName')
if ($hasSegmentNameColumn) {
$query = @"
INSERT INTO [dba].[dbo].[RestoreLog]
([DatabaseName], [OriginalDatabaseName], [RestoreDate], [DurationSeconds], [Status], [Details],
[LogFilePath], [BackupType], [WasDropped], [FinalStatus], [EstimatedSizeMB], [DeletionMode], [SegmentName])
VALUES
('$DatabaseName', '$OriginalDatabaseName', GETDATE(), $DurationSeconds, '$Status', '$safeDetails',
'$safeLogFilePath', '$BackupType', $wasDroppedBit, '$FinalStatus', $EstimatedSizeMB, '$DeletionMode', '$safeSegmentName')
"@
} else {
$query = @"
INSERT INTO [dba].[dbo].[RestoreLog]
([DatabaseName], [OriginalDatabaseName], [RestoreDate], [DurationSeconds], [Status], [Details],
[LogFilePath], [BackupType], [WasDropped], [FinalStatus], [EstimatedSizeMB], [DeletionMode])
VALUES
('$DatabaseName', '$OriginalDatabaseName', GETDATE(), $DurationSeconds, '$Status', '$safeDetails',
'$safeLogFilePath', '$BackupType', $wasDroppedBit, '$FinalStatus', $EstimatedSizeMB, '$DeletionMode')
"@
}
$result = Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $query -ErrorAction Stop
Write-Log "Запись в RestoreLog для $DatabaseName выполнена после создания таблицы"
return $true
} else {
return $false
}
} catch {
Write-Log "Не удалось создать таблицу или записать данные: $($_.Exception.Message)"
return $false
}
}
}
# Проверка целостности с повторными попытками
function CheckDatabaseWithRetry {
param(
[string]$ServerInstance,
[string]$DatabaseName,
[int]$MaxAttempts,
[int]$RetryDelay
)
$Attempt = 1
$LastError = ""
while ($Attempt -le $MaxAttempts) {
try {
$DbStatus = Get-DbaDatabase -SqlInstance $ServerInstance -Database $DatabaseName -ErrorAction Stop
if ($DbStatus.Status -ne 'Normal') {
$Msg = "База '$DatabaseName' не ONLINE (Status: $($DbStatus.Status))"
return [PSCustomObject]@{
DatabaseName = $DatabaseName
Status = "Пропущена"
Details = $Msg
Attempts = $Attempt
}
}
$SqlCmd = "EXECUTE [master].dbo.DatabaseIntegrityCheck @Databases = '$DatabaseName', @CheckCommands = 'CHECKDB', @LogToTable = 'Y', @MaxDOP='40', @PhysicalOnly = 'Y';"
Invoke-DbaQuery -SqlInstance $ServerInstance -Query $SqlCmd -EnableException
return [PSCustomObject]@{
DatabaseName = $DatabaseName
Status = "Завершена"
Details = "Проверка выполнена успешно (попытка $Attempt)"
Attempts = $Attempt
}
} catch {
$LastError = $_.Exception.Message
if ($Attempt -lt $MaxAttempts) {
Start-Sleep -Seconds $RetryDelay
}
$Attempt++
}
}
return [PSCustomObject]@{
DatabaseName = $DatabaseName
Status = "Ошибка"
Details = "Все $MaxAttempts попыток завершены ошибкой: $LastError"
Attempts = $MaxAttempts
}
}
# Функция проверки и создания таблицы DBCCResults
function Ensure-DBCCResultsTable {
param([string]$TargetServer)
Write-Log "Проверка и создание таблицы DBCCResults..."
$checkAndCreateTable = @"
-- Проверяем существование таблица
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dba].[dbo].[DBCCResults]') AND type in (N'U'))
BEGIN
CREATE TABLE [dba].[dbo].[DBCCResults](
[ID] [int] IDENTITY(1,1) NOT NULL,
[DatabaseName] [nvarchar](128) NOT NULL,
[CheckDate] [datetime] NOT NULL,
[Command] [nvarchar](max) NOT NULL,
[Outcome] [nvarchar](50) NULL,
[DurationSeconds] [int] NULL,
[ErrorCount] [int] NULL,
[ConsistencyErrorCount] [int] NULL,
[LogInfo] [nvarchar](max) NULL,
[Log] [nvarchar](max) NULL,
CONSTRAINT [PK_DBCCResults] PRIMARY KEY CLUSTERED ([ID] ASC)
)
PRINT 'Таблица DBCCResults создана.'
END
ELSE
BEGIN
-- Проверяем наличие столбца Log
IF COL_LENGTH('dba.dbo.DBCCResults', 'Log') IS NULL
BEGIN
ALTER TABLE dba.dbo.DBCCResults ADD [Log] NVARCHAR(MAX) NULL;
PRINT 'Столбец Log добавлен в таблицу DBCCResults.'
END
-- Проверяем наличие столбца LogInfo
IF COL_LENGTH('dba.dbo.DBCCResults', 'LogInfo') IS NULL
BEGIN
ALTER TABLE dba.dbo.DBCCResults ADD [LogInfo] NVARCHAR(MAX) NULL;
PRINT 'Столбец LogInfo добавлен в таблицу DBCCResults.'
END
PRINT 'Таблица DBCCResults уже существует.'
END
"@
try {
Invoke-DbaQuery -SqlInstance $TargetServer -Database "dba" -Query $checkAndCreateTable -EnableException
Write-Log "Таблица DBCCResults проверена/создана успешно"
return $true
} catch {
Write-Log "Ошибка при проверке/создании таблицы DBCCResults: $($_.Exception.Message)"
return $false
}
}
# Функция для записи результатов проверки в DBCCResults
function Write-DBCCResultsToDb {
param(
[string]$TargetServer,
[array]$CheckResults
)
Write-Log "=== Запись результатов проверки в DBCCResults ==="
# Проверяем и создаем таблицу
$tableReady = Ensure-DBCCResultsTable -TargetServer $TargetServer
if (-not $tableReady) {
Write-Log "Не удалось подготовить таблицу DBCCResults"
return
}
try {
foreach ($res in $CheckResults) {
$DbName = $res.DatabaseName.Replace("'", "''")
$CheckDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$Command = "DBCC CHECKDB ([$DbName]) WITH ALL_ERRORMSGS"
$Outcome = switch ($res.Status) {
"Завершена" { "Succeeded" }
"Ошибка" { "Failed" }
"Пропущена" { "Skipped" }
default { "Unknown" }
}
$DurationSec = 0
$ErrorCount = if ($res.Status -eq "Ошибка") { 1 } else { 0 }
$ConsistencyErrorCount = $ErrorCount
$LogText = "$($res.Details) (попыток: $($res.Attempts))"
$LogInfoText = "$($res.Details) (попыток: $($res.Attempts))"
$SqlInsert = @"
INSERT INTO [dba].[dbo].[DBCCResults]
(DatabaseName, CheckDate, Command, Outcome, DurationSeconds, ErrorCount, ConsistencyErrorCount, LogInfo, [Log])
VALUES
(N'$DbName', '$CheckDate', N'$Command', N'$Outcome', $DurationSec, $ErrorCount, $ConsistencyErrorCount, N'$(($LogInfoText -replace "'", "''"))', N'$(($LogText -replace "'", "''"))');
"@
Invoke-DbaQuery -SqlInstance $TargetServer -Query $SqlInsert -EnableException
}
Write-Log "Запись в DBCCResults завершена."
} catch {
Write-Log "Ошибка записи в DBCCResults: $($_.Exception.Message)"
}
}
# Функция для очистки старых записей из логов
function Cleanup-OldLogs {
param(
[string]$TargetServer,
[int]$OlaLogsCleanupDays
)
Write-Log "=== Очистка старых записей из логов ==="
$Cutoff = (Get-Date).AddDays(-$OlaLogsCleanupDays).ToString("yyyy-MM-dd HH:mm:ss")
try {
# Проверяем наличие таблицы CommandLog
$checkCommandLog = @"
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[CommandLog]') AND type in (N'U'))
BEGIN
DELETE FROM [dbo].[CommandLog] WHERE StartTime < '$Cutoff' AND CommandType = 'DBCC_CHECKDB';
END
"@
Invoke-DbaQuery -SqlInstance $TargetServer -Query $checkCommandLog -EnableException
# Проверяем наличие таблицы RestoreLog
$checkRestoreLog = @"
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dba].[dbo].[RestoreLog]') AND type in (N'U'))
BEGIN
DELETE FROM [dba].[dbo].[RestoreLog] WHERE RestoreDate < '$Cutoff';
END
"@
Invoke-DbaQuery -SqlInstance $TargetServer -Query $checkRestoreLog -EnableException
Write-Log "Очистка логов завершена."
} catch {
Write-Log "Ошибка при очистке логов: $($_.Exception.Message)"
}
}
# Создание HTML-отчёта
function New-HtmlReport {
param(
[array]$RestoreResults,
[array]$DeleteResults,
[array]$CheckResults,
[string]$TotalDuration,
[string]$CheckDuration,
[string]$DeletionMode,
[string]$TargetServer,
[int]$TotalDatabases,
[array]$DatabaseFilter,
[int]$PathSegmentFromEnd,
[string]$SourceBackupRoot,
[double]$BackupExpansionFactor,
[double]$TotalExpandedSizeGB
)
$CombinedResults = @()
# Фильтруем пустые результаты восстановления
$FilteredRestoreResults = $RestoreResults | Where-Object { -not [string]::IsNullOrEmpty($_.DatabaseName) }
foreach ($r in $FilteredRestoreResults) {
$CombinedResults += [PSCustomObject]@{
Type = "Восстановление"
DatabaseName = $r.DatabaseName
Status = $r.Status
Duration = $r.Duration
Details = $r.Details
SortPriority = if ($r.Status -eq "Ошибка") { 1 } elseif ($r.Status -eq "Успешно") { 2 } else { 3 }
}
}
foreach ($cr in $CheckResults) {
$DetailsText = "$($cr.Details) (попыток: $($cr.Attempts))"
$CombinedResults += [PSCustomObject]@{
Type = "Проверка"
DatabaseName = $cr.DatabaseName
Status = $cr.Status
Duration = "N/A"
Details = $DetailsText
SortPriority = if ($cr.Status -eq "Ошибка") { 1 } elseif ($cr.Status -eq "Завершена") { 2 } else { 3 }
}
}
$SortedResults = $CombinedResults | Sort-Object @{Expression = {$_.SortPriority}; Ascending = $true}, @{Expression = {$_.Type}; Ascending = $false}, @{Expression = {$_.DatabaseName}; Ascending = $true}
$HtmlRows = @()
foreach ($result in $SortedResults) {
# Пропускаем пустые строки
if ([string]::IsNullOrEmpty($result.DatabaseName)) {
continue
}
$Bg = if ($result.Status -eq "Ошибка") { "#FFB6C1" }
elseif ($result.Status -in @("Успешно", "Завершена")) { "#90EE90" }
else { "#FFA07A" }
$HtmlRows += "<tr style='background-color:$Bg'><td>$($result.Type)</td><td>$($result.DatabaseName)</td><td>$($result.Status)</td><td>$($result.Duration)</td><td>$($result.Details)</td></tr>"
}
$SuccessRestores = ($FilteredRestoreResults | Where-Object { $_.Status -eq "Успешно" }).Count
$ErrorRestores = ($FilteredRestoreResults | Where-Object { $_.Status -eq "Ошибка" }).Count
$SuccessChecks = ($CheckResults | Where-Object { $_.Status -eq "Завершена" }).Count
$ErrorChecks = ($CheckResults | Where-Object { $_.Status -eq "Ошибка" }).Count
$SkippedChecks = ($CheckResults | Where-Object { $_.Status -eq "Пропущена" }).Count
$TotalDeleted = if ($DeleteResults) { ($DeleteResults | Where-Object { $_.Status -eq 'Успешно' }).Count } else { 0 }
$TotalFreedGB = if ($DeleteResults) { [math]::Round(($DeleteResults | Where-Object { $_.Status -eq 'Успешно' } | Measure-Object -Property SizeMB -Sum).Sum / 1024, 2) } else { 0 }
$SegmentDescription = switch ($PathSegmentFromEnd) {
1 { "Имя базы" }
2 { "Папка перед базой (имя сервера)" }
3 { "Папка за 2 уровня до базы" }
4 { "Папка за 3 уровня до базы" }
default { "Сегмент $PathSegmentFromEnd с конца" }
}
$HtmlReport = @"
<!DOCTYPE html>
<html><head><meta charset='utf-8'><title>Отчёт db01 восстановление баз</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; }
h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background: #3498db; color: white; font-weight: bold; }
.error-row { background-color: #FFB6C1; }
.success-row { background-color: #90EE90; }
.warning-row { background-color: #FFA07A; }
.summary { background: #f8f9fa; border-left: 4px solid #3498db; padding: 15px; margin: 20px 0; }
ul { padding-left: 20px; }
.success-text { color: #28a745; font-weight: bold; }
.error-text { color: #dc3545; font-weight: bold; }
.warning-text { color: #ffc107; font-weight: bold; }
.info-text { color: #17a2b8; font-weight: bold; }
</style></head>
<body>
<h2>Отчёт о восстановлении и проверке на $TargetServer</h2>
<div class="summary">
<p><strong>Дата выполнения:</strong> $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")</p>
<p><strong>Общее время выполнения:</strong> $TotalDuration</p>
<p><strong>Время проверки целостности:</strong> $CheckDuration</p>
<p><strong>Источник бэкапов:</strong> $SourceBackupRoot</p>
<p><strong>Коэффициент расширения бэкапа:</strong> $BackupExpansionFactor</p>
<p><strong>Расчетный размер после восстановления:</strong> $TotalExpandedSizeGB GB</p>
<p><strong>Фильтр баз:</strong> $(if ($DatabaseFilter.Count -eq 0) {'ВСЕ базы'} else {$($DatabaseFilter -join ', ')})</p>
<p><strong>Режим удаления:</strong> $DeletionMode</p>
<p><strong>Формирование имен:</strong> $SegmentDescription (сегмент $PathSegmentFromEnd с конца)</p>
</div>
<div class="summary">
<p><strong>Статистика:</strong></p>
<ul>
<li>Всего баз для восстановления: $TotalDatabases</li>
<li>Удалено баз для освобождения места: <span class="info-text">$TotalDeleted</span></li>
<li>Освобождено места: <span class="info-text">$TotalFreedGB GB</span></li>
<li>Успешных восстановлений: <span class="success-text">$SuccessRestores</span></li>
<li>Ошибок восстановления: <span class="error-text">$ErrorRestores</span></li>
<li>Успешных проверок: <span class="success-text">$SuccessChecks</span></li>
<li>Ошибок проверки: <span class="error-text">$ErrorChecks</span></li>
<li>Пропущенных проверок: <span class="warning-text">$SkippedChecks</span></li>
</ul>
</div>
<p><em>Примечание: строки отсортированы по приоритету (ошибки → успешно → пропущено)</em></p>
<table>
<thead>
<tr>
<th>Операция</th>
<th>База</th>
<th>Статус</th>
<th>Время</th>
<th>Детали</th>
</tr>
</thead>
<tbody>
$($HtmlRows -join '')
</tbody>
</table>
<p><strong>Логи:</strong> $TranscriptPath</p>
<p><strong>Таблицы с логами:</strong> [dba].[dbo].[RestoreLog], [dba].[dbo].[DBCCResults]</p>
</body></html>
"@
return $HtmlReport
}
# === ОСНОВНОЙ СКРИПТ ===
Write-Log "=== СТАРТ СКРИПТА ==="
Write-Log "Целевой сервер: $TargetServer"
Write-Log "Путь к бэкапам: $SourceBackupRoot"
Write-Log "Режим удаления: $DeletionMode"
Write-Log "Коэффициент расширения бэкапа: $BackupExpansionFactor"
Write-Log "Формирование имен (сегмент пути с конца): $PathSegmentFromEnd"
Write-Log "Фильтр баз: $($DatabaseFilter -join ', ')"
$TotalStartTime = Get-Date
$RestoreResults = @()
$DeleteResults = @() # Инициализируем переменную заранее
try {
# 0. Проверяем наличие базы dba
Write-Log "Проверка наличия базы dba..."
try {
$dbaDb = Get-DbaDatabase -SqlInstance $TargetServer -Database "dba" -ErrorAction Stop
if (-not $dbaDb) {
Write-Log "База dba не существует на сервере $TargetServer. Создаем..."
$createDbQuery = @"
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'dba')
BEGIN
CREATE DATABASE [dba];
PRINT 'База dba создана.'
END
ELSE
BEGIN
PRINT 'База dba уже существует.'
END
"@
Invoke-DbaQuery -SqlInstance $TargetServer -Query $createDbQuery -EnableException
Write-Log "База dba проверена/создана"
} else {
Write-Log "База dba существует"
}
} catch {
Write-Log "Ошибка при проверке/создании базы dba: $($_.Exception.Message)"
throw
}
# 1. Сканирование бэкапов
Write-Log "Этап 1: Сканирование бэкапов..."
$BackupDatabases = Get-BackupDatabases -BackupRootPath $SourceBackupRoot `
-DatabaseFilter $DatabaseFilter `
-ExcludedDbs $ExcludedDatabases `
-ExcludedServers $ExcludedServerFolders `
-PathSegmentFromEnd $PathSegmentFromEnd
if ($BackupDatabases.Count -eq 0) {
$message = "Не найдено баз с бэкапами для восстановления по пути: $SourceBackupRoot"
Write-Log $message
Send-EmailReport -Subject "Ошибка: Нет баз для восстановления на $TargetServer" -Body $message
Stop-Transcript
exit 1
}
Write-Log "Найдено баз для восстановления: $($BackupDatabases.Count)"
$totalBackupSizeMB = ($BackupDatabases | Measure-Object -Property TotalBackupSizeMB -Sum).Sum
$totalBackupSizeGB = [math]::Round($totalBackupSizeMB / 1024, 2)
$totalExpandedSizeMB = ($BackupDatabases | Measure-Object -Property ExpandedSizeMB -Sum).Sum
$totalExpandedSizeGB = [math]::Round($totalExpandedSizeMB / 1024, 2)
Write-Log "Общий размер всех бэкапов: $totalBackupSizeMB MB ($totalBackupSizeGB GB)"
Write-Log "Общий расчетный размер баз после восстановления: $totalExpandedSizeMB MB ($totalExpandedSizeGB GB)"
# 2. Удаление баз (в зависимости от режима)
$DeleteResults = @()
switch ($DeletionMode) {
"All" {
Write-Log "Этап 2: Удаление всех баз (режим All)..."
$DeleteResults = Remove-AllDatabasesParallel -TargetServer $TargetServer `
-DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
-ThrottleLimit $DeletionThrottleLimit
}
"Calculate" {
Write-Log "Этап 2: Проверка места и расчет необходимости удаления (режим Calculate)..."
$DeleteResults = Calculate-SpaceAndDelete -TargetServer $TargetServer `
-DataPath $DataPath `
-DatabasesInfo $BackupDatabases `
-DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
-ThrottleLimit $DeletionThrottleLimit
}
"Smart" {
Write-Log "Этап 2: Умное удаление баз (режим Smart)..."
# Аналогично Calculate, но всегда удаляет минимально необходимые базы
$DeleteResults = Calculate-SpaceAndDelete -TargetServer $TargetServer `
-DataPath $DataPath `
-DatabasesInfo $BackupDatabases `
-DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
-ThrottleLimit $DeletionThrottleLimit
}
"None" {
Write-Log "Этап 2: Пропуск удаления баз (режим None)..."
}
default {
Write-Log "Неизвестный режим удаления: $DeletionMode. Используется режим Calculate."
$DeleteResults = Calculate-SpaceAndDelete -TargetServer $TargetServer `
-DataPath $DataPath `
-DatabasesInfo $BackupDatabases `
-DatabasesToExcludeFromDeletion $DatabasesToExcludeFromDeletion `
-ThrottleLimit $DeletionThrottleLimit
}
}
# 3. Восстановление баз (параллельное)
Write-Log "Этап 3: Параллельное восстановление $($BackupDatabases.Count) баз..."
$RestoreResults = $BackupDatabases | ForEach-Object -Parallel {
$dbInfo = $_
$targetServer = $using:TargetServer
$dataPath = $using:DataPath
$logPath = $using:LogPath
$transcriptPath = $using:TranscriptPath
$deletionMode = $using:DeletionMode
$backupExpansionFactor = $using:BackupExpansionFactor
Import-Module dbatools -Force
$startTime = Get-Date
$dbName = $dbInfo.DatabaseName
$originalName = $dbInfo.OriginalDatabaseName
$serverName = $dbInfo.ServerName
$fullBackupPath = $dbInfo.FullBackupFile
$diffBackupPath = $dbInfo.DiffBackupFile
$hasDiff = $dbInfo.HasDiffBackup
$backupSizeMB = $dbInfo.BackupSizeMB
$expandedSizeMB = $dbInfo.ExpandedSizeMB
$wasDropped = $false
try {
# Проверяем, что имя базы не пустое
if ([string]::IsNullOrEmpty($dbName)) {
return $null
}
# Удаляем, если существует
$existingDb = Get-DbaDatabase -SqlInstance $targetServer -Database $dbName -ErrorAction SilentlyContinue
if ($existingDb) {
Remove-DbaDatabase -SqlInstance $targetServer -Database $dbName -Confirm:$false
$wasDropped = $true
}
# Получаем FileMapping из FULL бэкапа
$filemap = @{}
try {
$backupHeader = Read-DbaBackupHeader -SqlInstance $targetServer -Path $fullBackupPath
foreach ($file in $backupHeader.FileList) {
if ($file.Type -eq 'D') {
$newPath = Join-Path $dataPath "${dbName}_$($file.LogicalName).mdf"
} else {
$newPath = Join-Path $logPath "${dbName}_$($file.LogicalName).ldf"
}
$filemap[$file.LogicalName] = $newPath
}
} catch {
# Продолжаем без маппинга
}
# Восстанавливаем FULL бэкап
$fullRestoreParams = @{
SqlInstance = $targetServer
DatabaseName = $dbName
Path = $fullBackupPath
WithReplace = $true
NoRecovery = $true
}
if ($filemap.Count -gt 0) {
$fullRestoreParams['FileMapping'] = $filemap
}
Restore-DbaDatabase @fullRestoreParams | Out-Null
# Восстанавливаем DIFF если есть
$backupType = "FULL"
if ($hasDiff -and $diffBackupPath -and (Test-Path $diffBackupPath)) {
$diffRestoreParams = @{
SqlInstance = $targetServer
DatabaseName = $dbName
Path = $diffBackupPath
Continue = $true
NoRecovery = $false
}
if ($filemap.Count -gt 0) {
$diffRestoreParams['FileMapping'] = $filemap
}
Restore-DbaDatabase @diffRestoreParams | Out-Null
$backupType = "FULL+DIFF"
} else {
# Восстанавливаем только FULL
Invoke-DbaQuery -SqlInstance $targetServer -Query "RESTORE DATABASE [$dbName] WITH RECOVERY"
}
# Проверяем статус базы
$finalDb = Get-DbaDatabase -SqlInstance $targetServer -Database $dbName -ErrorAction SilentlyContinue
$endTime = Get-Date
$duration = $endTime - $startTime
if ($finalDb -and $finalDb.Status -eq 'Normal') {
$status = "Успешно"
$details = "Восстановлено $backupType (бэкап: $backupSizeMB MB → база: ~$expandedSizeMB MB)"
if ($wasDropped) { $details += " (предварительно удалена)" }
} else {
$status = "Ошибка"
$details = "База не ONLINE после восстановления"
if ($wasDropped) { $details += " (предварительно удалена)" }
}
return [PSCustomObject]@{
DatabaseName = $dbName
OriginalName = $originalName
ServerName = $serverName
Status = $status
Duration = "{0:hh\:mm\:ss}" -f $duration
Details = $details
BackupType = $backupType
WasDropped = $wasDropped # Убедимся, что это Boolean
DurationSeconds = $duration.TotalSeconds
FinalStatus = if ($finalDb) { $finalDb.Status } else { "Unknown" }
EstimatedSizeMB = $expandedSizeMB # Используем расширенный размер
}
} catch {
$endTime = Get-Date
$duration = $endTime - $startTime
$errorMsg = $_.Exception.Message
# Возвращаем результат с ошибкой только если имя базы не пустое
if (-not [string]::IsNullOrEmpty($dbName)) {
return [PSCustomObject]@{
DatabaseName = $dbName
OriginalName = $originalName
ServerName = $serverName
Status = "Ошибка"
Duration = "{0:hh\:mm\:ss}" -f $duration
Details = "Ошибка: $errorMsg"
BackupType = "Ошибка"
WasDropped = $wasDropped # Убедимся, что это Boolean
DurationSeconds = $duration.TotalSeconds
FinalStatus = "Error"
EstimatedSizeMB = $expandedSizeMB # Используем расширенный размер
}
} else {
return $null
}
}
} -ThrottleLimit $RestoreThrottleLimit
# Фильтруем пустые результаты
$RestoreResults = $RestoreResults | Where-Object { $_ -ne $null }
Write-Log "Параллельное восстановление завершено. Обработано результатов: $($RestoreResults.Count)"
# 4. Запись в RestoreLog
Write-Log "Этап 4: Запись результатов восстановления в таблицу RestoreLog..."
$logWriteSuccess = 0
$logWriteFail = 0
# Сначала убедимся, что таблица существует
$tableReady = Ensure-RestoreLogTable -TargetServer $TargetServer
if (-not $tableReady) {
Write-Log "Предупреждение: не удалось создать таблицу RestoreLog"
}
foreach ($res in $RestoreResults) {
# Убедимся, что WasDropped - это Boolean
$wasDropped = if ($null -eq $res.WasDropped) { $false } else { [bool]$res.WasDropped }
$result = Write-RestoreLogToDb -TargetServer $TargetServer `
-DatabaseName $res.DatabaseName `
-OriginalDatabaseName $res.OriginalName `
-Status $res.Status `
-Details $res.Details `
-BackupType $res.BackupType `
-WasDropped $wasDropped `
-LogFilePath $TranscriptPath `
-DurationSeconds $res.DurationSeconds `
-FinalStatus $res.FinalStatus `
-EstimatedSizeMB $res.EstimatedSizeMB `
-DeletionMode $DeletionMode `
-SegmentName $res.ServerName
if ($result) {
$logWriteSuccess++
} else {
$logWriteFail++
}
}
Write-Log "Запись в RestoreLog завершена: успешно - $logWriteSuccess, с ошибкой - $logWriteFail"
# 5. Проверка целостности с повторными попытками
$CheckResults = @()
if ($RunIntegrityCheck) {
Write-Log "Ожидание 2 минуты для завершения всех операций восстановления..."
Start-Sleep -Seconds 120
$SuccessfulDatabases = $RestoreResults | Where-Object { $_.Status -eq "Успешно" } | ForEach-Object { $_.DatabaseName }
if ($SuccessfulDatabases.Count -gt 0) {
Write-Log "Этап 5: Проверка целостности восстановленных баз..."
$CheckStartTime = Get-Date
Write-Log "Запуск проверок для $($SuccessfulDatabases.Count) успешно восстановленных баз..."
# Используем ThreadJob для проверки
$CheckJobs = $SuccessfulDatabases | ForEach-Object {
Start-ThreadJob -ScriptBlock $function:CheckDatabaseWithRetry -ArgumentList $TargetServer, $_, $MaxRetryAttempts, $RetryDelaySeconds -ThrottleLimit $CheckThrottleLimit
}
Write-Log "Ожидание завершения всех проверок..."
$CheckResults = $CheckJobs | Wait-Job | Receive-Job
$CheckJobs | Remove-Job -Force
$CheckEndTime = Get-Date
$CheckDuration = $CheckEndTime - $CheckStartTime
Write-Log "Проверка целостности заняла: $($CheckDuration.ToString('hh\:mm\:ss'))"
Write-Log "Всего обработано результатов проверки: $($CheckResults.Count)"
Write-Log "Результаты проверки целостности:"
foreach ($result in $CheckResults) {
$msg = "База: $($result.DatabaseName), Статус: $($result.Status), Детали: $($result.Details), Попыток: $($result.Attempts)"
Write-Log $msg
}
# Запись результатов проверки в DBCCResults
Write-DBCCResultsToDb -TargetServer $TargetServer -CheckResults $CheckResults
# Очистка старых логов Ola Hallengren
Cleanup-OldLogs -TargetServer $TargetServer -OlaLogsCleanupDays $OlaLogsCleanupDays
} else {
Write-Log "Нет успешно восстановленных баз для проверки целостности"
}
}
# 6. Очистка логов
if ($CleanupOldLogs) {
Write-Log "Этап 6: Очистка старых логов..."
try {
$OldTranscripts = Get-ChildItem -Path "C:\temp\" -Filter "RestoreTranscript_*.log" -Recurse |
Where-Object { $_.CreationTime -lt (Get-Date).AddDays(-$LogCleanupDays) }
if ($OldTranscripts) {
$OldTranscripts | Remove-Item -Force
Write-Log "Удалены старые транскрипты (старше $LogCleanupDays дней): $($OldTranscripts.Count) файлов"
}
} catch {
Write-Log "Ошибка при очистке логов: $($_.Exception.Message)"
}
}
# 7. Подготовка итогов
$TotalEndTime = Get-Date
$TotalDuration = "{0:hh\:mm\:ss}" -f ($TotalEndTime - $TotalStartTime)
$CheckDurationStr = if ($RunIntegrityCheck -and $CheckResults.Count -gt 0) { $CheckDuration.ToString('hh\:mm\:ss') } else { "N/A" }
$SuccessRestores = ($RestoreResults | Where-Object { $_.Status -eq "Успешно" }).Count
$ErrorRestores = ($RestoreResults | Where-Object { $_.Status -eq "Ошибка" }).Count
$SuccessChecks = ($CheckResults | Where-Object { $_.Status -eq "Завершена" }).Count
$ErrorChecks = ($CheckResults | Where-Object { $_.Status -eq "Ошибка" }).Count
$SkippedChecks = ($CheckResults | Where-Object { $_.Status -eq "Пропущена" }).Count
# 8. Отправка отчета
if ($SendEmailReport) {
Write-Log "Этап 7: Отправка HTML-отчёта..."
$htmlReport = New-HtmlReport -RestoreResults $RestoreResults `
-DeleteResults $DeleteResults `
-CheckResults $CheckResults `
-TotalDuration $TotalDuration `
-CheckDuration $CheckDurationStr `
-DeletionMode $DeletionMode `
-TargetServer $TargetServer `
-TotalDatabases $BackupDatabases.Count `
-DatabaseFilter $DatabaseFilter `
-PathSegmentFromEnd $PathSegmentFromEnd `
-SourceBackupRoot $SourceBackupRoot `
-BackupExpansionFactor $BackupExpansionFactor `
-TotalExpandedSizeGB $totalExpandedSizeGB
$emailSubject = "Отчёт db01: Восстановление баз - $SuccessRestores/$($BackupDatabases.Count) успешно - $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
Send-EmailReport -Subject $emailSubject -Body $htmlReport -IsHtml $true -Attachments @($TranscriptPath)
}
# 9. Вывод итогов в консоль
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host "ИТОГИ ВОССТАНОВЛЕНИЯ" -ForegroundColor Yellow
Write-Host ("=" * 70) -ForegroundColor Cyan
Write-Host "Сервер: $TargetServer" -ForegroundColor Cyan
Write-Host "Источник: $SourceBackupRoot" -ForegroundColor Cyan
Write-Host "Коэффициент расширения: $BackupExpansionFactor" -ForegroundColor Cyan
Write-Host "Формирование имен: сегмент $PathSegmentFromEnd с конца" -ForegroundColor Cyan
Write-Host "Всего баз: $($BackupDatabases.Count)" -ForegroundColor White
Write-Host "Размер бэкапов: $totalBackupSizeGB GB → Расчетный размер баз: $totalExpandedSizeGB GB" -ForegroundColor White
Write-Host "Режим удаления: $DeletionMode" -ForegroundColor Cyan
if ($DeleteResults.Count -gt 0) {
$deletedCount = ($DeleteResults | Where-Object { $_.Status -eq 'Успешно' }).Count
$freedGB = [math]::Round(($DeleteResults | Where-Object { $_.Status -eq 'Успешно' } | Measure-Object -Property SizeMB -Sum).Sum / 1024, 2)
Write-Host "Удалено баз для освобождения места: $deletedCount" -ForegroundColor Magenta
Write-Host "Освобождено места: $freedGB GB" -ForegroundColor Magenta
}
Write-Host "Успешно восстановлено: $SuccessRestores" -ForegroundColor $(if ($SuccessRestores -gt 0) { "Green" } else { "Red" })
Write-Host "С ошибками восстановления: $ErrorRestores" -ForegroundColor $(if ($ErrorRestores -gt 0) { "Red" } else { "Green" })
if ($RunIntegrityCheck -and $CheckResults.Count -gt 0) {
Write-Host "Проверок целостности: $($CheckResults.Count)" -ForegroundColor White
Write-Host "Успешных проверок: $SuccessChecks" -ForegroundColor Green
Write-Host "Ошибок проверки: $ErrorChecks" -ForegroundColor $(if ($ErrorChecks -gt 0) { "Red" } else { "Green" })
Write-Host "Пропущенных проверок: $SkippedChecks" -ForegroundColor Yellow
}
Write-Host "Общее время: $TotalDuration" -ForegroundColor Cyan
Write-Host "Путь к логам: $TranscriptPath" -ForegroundColor Gray
Write-Host "Записано в RestoreLog: $logWriteSuccess из $($RestoreResults.Count)" -ForegroundColor $(if ($logWriteSuccess -eq $RestoreResults.Count) { "Green" } else { "Yellow" })
if ($SendEmailReport) {
Write-Host "Отчет отправлен на email" -ForegroundColor Green
}
# Детали восстановления
if ($RestoreResults.Count -gt 0) {
Write-Host "`nДетали восстановления:" -ForegroundColor Yellow
foreach ($result in $RestoreResults) {
$color = if ($result.Status -eq "Успешно") { "Green" } else { "Red" }
Write-Host " $($result.DatabaseName)" -ForegroundColor $color -NoNewline
Write-Host " [$($result.BackupType)]" -ForegroundColor Magenta -NoNewline
Write-Host " - $($result.Status)" -ForegroundColor $color -NoNewline
Write-Host " ($($result.Duration))" -ForegroundColor Gray
if ($result.Status -eq "Ошибка") {
Write-Host " Ошибка: $($result.Details)" -ForegroundColor DarkRed
}
}
} else {
Write-Host "`nНет результатов восстановления для отображения" -ForegroundColor Yellow
}
# Детали удаления (если были)
if ($DeleteResults.Count -gt 0) {
Write-Host "`nДетали удаления баз:" -ForegroundColor Yellow
foreach ($delete in $DeleteResults | Where-Object { $_.Status -eq 'Успешно' }) {
$sizeGB = [math]::Round($delete.SizeMB / 1024, 2)
Write-Host " Удалено: $($delete.DatabaseName) ($sizeGB GB)" -ForegroundColor Magenta
}
}
# Детали проверки целостности
if ($RunIntegrityCheck -and $CheckResults.Count -gt 0) {
Write-Host "`nДетали проверки целостности:" -ForegroundColor Yellow
foreach ($check in $CheckResults) {
$color = if ($check.Status -eq "Завершена") { "Green" }
elseif ($check.Status -eq "Ошибка") { "Red" }
else { "Yellow" }
Write-Host " $($check.DatabaseName)" -ForegroundColor $color -NoNewline
Write-Host " - $($check.Status)" -ForegroundColor $color -NoNewline
Write-Host " (попыток: $($check.Attempts))" -ForegroundColor Gray
if ($check.Status -ne "Завершена") {
Write-Host " Детали: $($check.Details)" -ForegroundColor DarkYellow
}
}
}
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
Write-Host "СКРИПТ ЗАВЕРШЕН УСПЕШНО" -ForegroundColor Green
Write-Host ("=" * 70) -ForegroundColor Cyan
} catch {
$errorMessage = "КРИТИЧЕСКАЯ ОШИБКА: $($_.Exception.Message)"
Write-Log $errorMessage
Write-Host $errorMessage -ForegroundColor Red
# Отправка email об ошибке
if ($SendEmailReport) {
$errorBody = @"
<h2>Критическая ошибка при восстановлении баз</h2>
<p><strong>Сервер:</strong> $TargetServer</p>
<p><strong>Время ошибки:</strong> $(Get-Date -Format 'dd.MM.yyyy HH:mm:ss')</p>
<p><strong>Источник:</strong> $SourceBackupRoot</p>
<p><strong>Коэффициент расширения:</strong> $BackupExpansionFactor</p>
<p><strong>Ошибка:</strong> $($_.Exception.Message)</p>
<p><strong>Формирование имен:</strong> сегмент $PathSegmentFromEnd с конца</p>
<p><strong>Файл логов:</strong> $TranscriptPath</p>
"@
try {
Send-EmailReport -Subject "КРИТИЧЕСКАЯ ОШИБКА: Сбой восстановления на $TargetServer" -Body $errorBody -IsHtml $true -Attachments @($TranscriptPath)
} catch {
Write-Host "Ошибка отправки email об ошибке: $($_.Exception.Message)" -ForegroundColor Red
}
}
throw
} finally {
Write-Log "=== ЗАВЕРШЕНИЕ РАБОТЫ СКРИПТА ==="
try {
Stop-Transcript
} catch {
Write-Host "Ошибка остановки транскрипта: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
Бэкапы делаются
-- В ночь с субботы на воскресение
--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
--проверяем наличие баз для полного бэкапа (ни разу не делался)
declare @Databases as varchar(5000);
set @Databases = (
select stuff(
(
select ',' + isnull(d.[name], bs.[database_name]) as [text()]
from sys.databases d with(nolock)
left join msdb.dbo.backupset bs with(nolock)
on bs.[database_name] = d.[name]
and bs.backup_finish_date > dateadd(day, -30, GETDATE())
--определяем наличие бэкапов именно на текущем сервере
and bs.server_name = @@SERVERNAME
where 1=1
--user databases
and d.database_id > 4
and d.[name] not like '%restore'
--online
and d.[state] = 0
and bs.[database_name] is null
order by isnull(d.[name], bs.[database_name])
for xml path('')
), 1, 1, ''
)
);
if(@Databases is not null)
begin
EXECUTE [dbo].[DatabaseBackup]
@Databases = @Databases,
@Directory = N'\\ARC03.adminbd.ru\sql_backup$',
@BackupType = 'FULL',
@Verify = 'Y',
@Compress = 'Y',
@CheckSum = 'Y',
@LogToTable = 'Y',
@DatabasesInParallel= 'Y';
end;
--делаем разностный бэкап
EXECUTE [dbo].[DatabaseBackup]
@Databases = 'USER_DATABASES,-%_restore',
@Directory = N'\\ARC03.adminbd.ru\sql_backup$',
@BackupType = 'DIFF',
@Verify = 'Y',
@Compress = 'Y',
@CleanupTime = 168,
@CleanupMode = 'AFTER_BACKUP',
@CheckSum = 'Y',
@LogToTable = 'Y',
@DatabasesInParallel= 'Y';


