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

Как сделать автоматическую проверку и восстановление VHDX‑дисков FSLogix (NTFS) для RDS‑сессий

Назначение

Скрипт PowerShell предназначен для диагностики и автоматического исправления ошибок файловой системы на VHDX‑дисках FSLogix. Он выявляет пользователей, у которых активна RDS‑сессия, но профиль не смонтирован (том с меткой Profile-ИмяПользователя отсутствует), затем находит соответствующий VHDX‑файл в заданных сетевых хранилищах, монтирует его, проверяет с помощью chkdsk /f и при необходимости восстанавливает целостность NTFS.

Основные возможности

  • 🔎 Автоматический поиск проблемных сессий через RD Connection Broker.

  • 📁 Поддержка нескольких хранилищ VHDX (UNC‑пути).

  • 🧪 Безопасное монтирование образов без присвоения буквы (буква назначается временно только для проверки).

  • 🛠 Исправление ошибок NTFS с помощью chkdsk /f (режим можно отключить).

  • 📄 Подробное логирование и CSV‑отчёт о состоянии каждого проверенного VHDX.

  • ⚡ Ограничение на количество проверяемых дисков – защита от перегрузки при большом числе проблем.

Как это работает

  1. Скрипт получает список активных сессий из указанной коллекции RDS.

  2. Для каждого RDS‑хоста проверяет, какие пользователи имеют смонтированный том FSLogix.

  3. Формирует список «проблемных» пользователей (сессия есть – тома нет).

  4. Для каждого такого пользователя перебирает заданные UNC‑пути в поисках его VHDX‑файла.

  5. Монтирует VHDX (без автоматической буквы), определяет том, временно назначает свободную букву.

  6. Запускает chkdsk /f для исправления ошибок файловой системы.

  7. Отмонтирует диск, удаляет временную букву, сохраняет результат.

  8. Формирует итоговый лог и CSV‑отчёт.

Результаты работы

  • Лог‑файл: %TEMP%\FSLogixVHDXCheck\FSLogix_VHDX_Check_<дата_время>.log
    Содержит все действия скрипта, ошибки и результаты chkdsk.

  • CSV‑отчёт: %TEMP%\FSLogixVHDXCheck\VHDX_Report_<дата_время>.csv
    Включает: пользователя, путь к VHDX, статус (OK / REPAIRED / CORRUPT / NTFS_ERRORS), результат восстановления, детали ошибки.

Важные замечания

  • Скрипт не изменяет и не удаляет профили пользователей – только файловая система VHDX.

  • Работает только с NTFS‑томами. Если том определён как RAW – восстановление невозможно (требуется ручное вмешательство).

  • Для корректной работы необходимы права администратора на RDS‑хостах и доступ к брокеру соединений.

  • Режим восстановления ($EnableRepair) можно отключить – тогда скрипт будет только диагностировать проблемы.

Пример использования

  1. Скачайте скрипт, при необходимости отредактируйте параметры в начале файла:

    • $collectionName – имя коллекции RDS.

    • $connectionBroker – FQDN брокера.

    • $fslogixRootUncList – массив путей к хранилищам VHDX.

    • $EnableRepair – установите $true для автоматического исправления.

  2. Запустите от имени администратора:

    powershell
    .\FSLogix_VHDX_Check.ps1
  3. После завершения проверьте лог и CSV‑отчёт в папке %TEMP%\FSLogixVHDXCheck.

Требования к окружению

  • Windows Server 2016 / 2019 / 2022 (RDS‑роль).

  • Модуль Hyper-V (для Mount-DiskImage / Dismount-DiskImage).

  • Командлеты Get-RDUserSession (доступны на сервере с ролью RDS‑брокера).

  • Сетевой доступ к UNC‑хранилищам VHDX.

# === НАСТРОЙКИ ===
$collectionName = "Farm "
$connectionBroker = "RDCB01.adminbd.ru"
$fslogixRootUncList = @(
    "\\UPD02\DisksfsFSLogix$"
    "\\UPD03\DisksfsFSLogix$"
)
$MaxDisksToCheck = 1500
$EnableRepair = $true

# === ЛОГИРОВАНИЕ ===
$LogDir = "$env:TEMP\FSLogixVHDXCheck"
if (-not (Test-Path $LogDir)) { New-Item -Path $LogDir -ItemType Directory -Force | Out-Null }
$LogPath = "$LogDir\FSLogix_VHDX_Check_$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').log"

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "[$timestamp] [$Level] $Message"
    Write-Host $logEntry
    Add-Content -Path $LogPath -Value $logEntry
}

Write-Log "=== НАЧАЛО ПРОВЕРКИ И ВОССТАНОВЛЕНИЯ VHDX (NTFS) ===" "INFO"
Write-Log "Коллекция: $collectionName | Брокер: $connectionBroker" "INFO"
Write-Log "Пути к дискам: $($fslogixRootUncList -join ', ')" "INFO"
Write-Log "Лимит проверки: первые $MaxDisksToCheck дисков" "INFO"
Write-Log "Режим восстановления: $(if ($EnableRepair) { 'ВКЛЮЧЕН (chkdsk /f)' } else { 'ВЫКЛЮЧЕН (только диагностика)' })" "INFO"

# === 1. ПОЛУЧАЕМ ВСЕ СЕССИИ ===
Write-Log "Получение сессий из коллекции '$collectionName' через брокер '$connectionBroker'..." "INFO"
try {
    $sessions = Get-RDUserSession -CollectionName $collectionName -ConnectionBroker $connectionBroker -ErrorAction Stop
} catch {
    Write-Log "❌ Ошибка при получении сессий: $($_.Exception.Message)" "ERROR"
    exit 1
}

if (-not $sessions) {
    Write-Log "ℹ️ Нет сессий в коллекции." "INFO"
    exit 0
}

$validSessions = $sessions | Where-Object { $_.HostServer -and $_.HostServer.Trim() -ne '' }
Write-Log "Всего сессий: $($sessions.Count). Валидных (с HostServer): $($validSessions.Count)" "INFO"

if (-not $validSessions) {
    Write-Log "❌ Нет валидных сессий для проверки." "WARN"
    exit 0
}

# === 2. НАХОДИМ ПОЛЬЗОВАТЕЛЕЙ БЕЗ СМОНТИРОВАННОГО ПРОФИЛЯ ===
$sessionsByHost = $validSessions | Group-Object HostServer
$problemUsers = @()

Write-Log "Поиск пользователей с активной сессией, но без смонтированного профиля..." "INFO"

foreach ($group in $sessionsByHost) {
    $hostName = $group.Name.Trim()
    $hostSessions = $group.Group

    try {
        $userNamesToCheck = $hostSessions.UserName | ForEach-Object { ($_ -split '\\')[-1] }

        $mountedUsers = Invoke-Command -ComputerName $hostName -ScriptBlock {
            param($expectedNames)
            $mounted = @()
            $volumes = Get-Volume -ErrorAction SilentlyContinue |
                       Where-Object { $_.FileSystemLabel -and $_.FileSystemLabel.StartsWith("Profile-") }
            foreach ($vol in $volumes) {
                $userNameFromLabel = $vol.FileSystemLabel.Substring("Profile-".Length)
                $userNameClean = ($userNameFromLabel -split '\\')[-1].Trim()
                foreach ($exp in $expectedNames) {
                    if ($userNameClean.ToLower() -eq $exp.ToLower()) {
                        $mounted += $exp
                        break
                    }
                }
            }
            return $mounted
        } -ArgumentList (,$userNamesToCheck) -ErrorAction Stop

        foreach ($session in $hostSessions) {
            $userName = ($session.UserName -split '\\')[-1]
            if ($userName -notin $mountedUsers) {
                $problemUsers += [PSCustomObject]@{
                    FullUserName = $session.UserName
                    UserName     = $userName
                    SessionHost  = $session.HostServer
                    SessionId    = $session.UnifiedSessionId
                    SessionState = $session.SessionState
                }
                Write-Log "⚠️ Обнаружена проблема: пользователь '$userName' на хосте '$hostName' — профиль не смонтирован." "WARN"
            }
        }
    } catch {
        $errMsg = "Не удалось проверить хост '$hostName': $($_.Exception.Message)"
        Write-Log $errMsg "ERROR"
        foreach ($session in $hostSessions) {
            $userName = ($session.UserName -split '\\')[-1]
            $problemUsers += [PSCustomObject]@{
                FullUserName = $session.UserName
                UserName     = $userName
                SessionHost  = $session.HostServer
                SessionId    = $session.UnifiedSessionId
                SessionState = $session.SessionState
            }
            Write-Log "⚠️ Пользователь '$userName' помечен как проблемный из-за ошибки проверки хоста." "WARN"
        }
    }
}

if (-not $problemUsers) {
    Write-Log "✅ Все пользователи с сессией имеют смонтированный профиль. Проверка не требуется." "INFO"
    exit 0
}

# ОГРАНИЧИВАЕМ ДО ПЕРВЫХ N
if ($problemUsers.Count -gt $MaxDisksToCheck) {
    Write-Log "Найдено $($problemUsers.Count) проблемных пользователей. Проверим только первые $MaxDisksToCheck." "INFO"
    $problemUsers = $problemUsers | Select-Object -First $MaxDisksToCheck
} else {
    Write-Log "Будет проверено $($problemUsers.Count) VHDX-файлов." "INFO"
}

# === ФУНКЦИЯ ДЛЯ ПОЛУЧЕНИЯ СВОБОДНОЙ БУКВЫ ===
function Get-FreeDriveLetter {
    $used = Get-Volume | Where-Object { $_.DriveLetter } | ForEach-Object { $_.DriveLetter + ":" }
    for ($i = 68; $i -le 90; $i++) {
        $letter = [char]$i + ":"
        if ($used -notcontains $letter) {
            return $letter
        }
    }
    throw "Нет свободных букв диска (D-Z)"
}

# === 3. ПРОВЕРКА И ВОССТАНОВЛЕНИЕ VHDX ===
$vhdxCheckResults = @()

foreach ($user in $problemUsers) {
    Write-Log "`n=== Обработка пользователя: $($user.UserName) ===" "INFO"
   
    foreach ($rootUnc in $fslogixRootUncList) {
        $foundVhdx = $false
        $vhdxPath = ""

        Write-Log "Поиск VHDX для '$($user.UserName)' в хранилище: $rootUnc" "INFO"

        try {
            $userFolders = Get-ChildItem -Path $rootUnc -Directory -ErrorAction Stop |
                           Where-Object { $_.Name -like "$($user.UserName)*" }

            foreach ($folder in $userFolders) {
                $candidate1 = Join-Path $folder.FullName "Profile_$($user.UserName).VHDX"
                $candidate2 = Join-Path $folder.FullName "Profile_$($user.UserName).vhdx"
                
                if (Test-Path $candidate1) { $vhdxPath = $candidate1; $foundVhdx = $true; break }
                if (Test-Path $candidate2) { $vhdxPath = $candidate2; $foundVhdx = $true; break }
            }
        } catch {
            Write-Log "Ошибка при поиске в $rootUnc : $($_.Exception.Message)" "WARN"
            continue
        }

        if (-not $foundVhdx) { 
            Write-Log "VHDX для пользователя не найден в $rootUnc" "DEBUG"
            continue 
        }

        # === РАБОТА С VHDX ===
        $status = "OK"
        $fsType = "N/A"
        $errorDetail = ""
        $repairAttempted = $false
        $repairSuccess = $null
        $driveLetter = $null
        $letterChar = $null
        $diskNumber = $null

        try {
            Write-Log "Монтирование VHDX: $vhdxPath" "INFO"

            $mountedImage = Mount-DiskImage -ImagePath $vhdxPath -NoDriveLetter -PassThru -ErrorAction Stop
            Start-Sleep -Seconds 5

            # === НАДЁЖНОЕ ОПРЕДЕЛЕНИЕ НОМЕРА ДИСКА ===
            $disk = $null

            # Способ 1: Через Get-DiskImage
            try {
                $diskImage = Get-DiskImage -ImagePath $vhdxPath -ErrorAction Stop
                $disk = $diskImage | Get-Disk -ErrorAction SilentlyContinue
            } catch { }

            # Способ 2: Поиск по всем дискам (fallback)
            if (-not $disk -or -not $disk.Number) {
                $disk = Get-Disk | Where-Object { 
                    $_.Location -like "*$([System.IO.Path]::GetFileName($vhdxPath))*" -or 
                    $_.FriendlyName -like "*$($user.UserName)*" 
                } | Select-Object -First 1
            }

            if (-not $disk -or -not $disk.Number) {
                throw "Не удалось определить номер диска для VHDX. Возможно, диск не инициализировался."
            }

            $diskNumber = $disk.Number
            Write-Log "Диск определён как Disk $diskNumber" "INFO"

            $expectedLabel = "Profile-$($user.UserName)"

            $volume = Get-Volume -ErrorAction SilentlyContinue | 
                      Where-Object { $_.FileSystemLabel -and $_.FileSystemLabel.Trim() -eq $expectedLabel }

            if (-not $volume) {
                throw "Том с меткой '$expectedLabel' не найден"
            }

            $fsType = $volume.FileSystem
            Write-Log "Том найден: метка='$expectedLabel', FS=$fsType, Health=$($volume.HealthStatus)" "INFO"

            if ($fsType -eq "RAW") {
                $status = "CORRUPT"
                $errorDetail = "Том в состоянии RAW — восстановление невозможно."
                Write-Log $errorDetail "ERROR"
                continue
            }

            # === ПРИСВАИВАЕМ БУКВУ ===
            $driveLetter = Get-FreeDriveLetter
            $letterChar = $driveLetter.TrimEnd(':')

            Write-Log "Назначение временной буквы: $driveLetter" "INFO"

            Start-Sleep -Seconds 3

            $partition = Get-Partition -DiskNumber $diskNumber | 
                         Where-Object { -not $_.DriveLetter } | 
                         Select-Object -First 1

            if (-not $partition) {
                $partition = Get-Partition -DiskNumber $diskNumber | Select-Object -First 1
            }

            if ($partition) {
                $partition | Set-Partition -NewDriveLetter $letterChar -ErrorAction Stop
                Write-Log "Буква $driveLetter успешно назначена" "INFO"
            } else {
                throw "Не удалось найти раздел на диске $diskNumber"
            }

            Start-Sleep -Seconds 3

            # === ЗАПУСК CHKDSK ===
            if ($EnableRepair) {
                Write-Log "Запуск chkdsk $driveLetter /f ..." "INFO"
                $repairAttempted = $true

                $chkdskOutput = & chkdsk "$driveLetter" /f 2>&1 | Out-String

                Write-Log "chkdsk завершён. Вывод (первые 700 символов):`n$($chkdskOutput.Substring(0, [Math]::Min(700, $chkdskOutput.Length)))" "DEBUG"

                if ($chkdskOutput -match "(Windows has made corrections|исправлений в файловой системе|corrected)") {
                    $repairSuccess = $true
                    $status = "REPAIRED"
                    $errorDetail = "Ошибки NTFS успешно исправлены."
                    Write-Log "✅ chkdsk исправил ошибки." "INFO"
                }
                elseif ($chkdskOutput -match "(did not find any problems|не обнаружил проблем|No further action is required)") {
                    $repairSuccess = $true
                    $errorDetail = "Ошибок NTFS не обнаружено."
                    Write-Log "✅ chkdsk: ошибок не найдено." "INFO"
                }
                else {
                    $repairSuccess = $false
                    $status = "NTFS_ERRORS"
                    $errorDetail = "chkdsk обнаружил ошибки, но не смог их полностью исправить."
                    Write-Log "❌ chkdsk не исправил все ошибки." "ERROR"
                }
            }
            else {
                $errorDetail = "Восстановление отключено."
                Write-Log $errorDetail "WARN"
            }

        }
        catch {
            $status = "CORRUPT"
            $errorDetail = "Ошибка обработки VHDX: $($_.Exception.Message)"
            Write-Log "❌ $errorDetail" "ERROR"
        }
        finally {
            # === ОЧИСТКА ===
            if ($driveLetter) {
                try {
                    Write-Log "Удаление буквы $driveLetter ..." "DEBUG"
                    Get-Volume -DriveLetter $letterChar -ErrorAction SilentlyContinue | 
                        Set-Volume -DriveLetter $null -ErrorAction SilentlyContinue

                    Get-Partition -DriveLetter $letterChar -ErrorAction SilentlyContinue | 
                        Set-Partition -NewDriveLetter $null -ErrorAction SilentlyContinue
                }
                catch { Write-Log "Ошибка удаления буквы: $($_.Exception.Message)" "WARN" }
            }

            try {
                Write-Log "Отмонтирование VHDX ..." "DEBUG"
                Dismount-DiskImage -ImagePath $vhdxPath -ErrorAction SilentlyContinue
                Write-Log "VHDX успешно отмонтирован" "DEBUG"
            }
            catch { Write-Log "Ошибка отмонтирования: $($_.Exception.Message)" "WARN" }
        }

        $vhdxCheckResults += [PSCustomObject]@{
            UserName         = $user.UserName
            StoragePath      = $rootUnc
            VhdxPath         = $vhdxPath
            Status           = $status
            FileSystem       = $fsType
            RepairAttempted  = $repairAttempted
            RepairSuccess    = $repairSuccess
            Error            = $errorDetail
            DiskNumber       = $diskNumber
        }
    }
}

# === 4. ИТОГОВЫЙ ОТЧЁТ ===
Write-Log "`n=== ИТОГОВЫЙ ОТЧЁТ ===" "INFO"

$repaired = $vhdxCheckResults | Where-Object { $_.Status -eq "REPAIRED" }
$failed   = $vhdxCheckResults | Where-Object { $_.Status -notin @("OK", "REPAIRED") }

if ($repaired) { 
    Write-Log "✅ Исправлено: $($repaired.Count)" "INFO"
    $repaired | ForEach-Object { Write-Log " - $($_.UserName) ($($_.StoragePath)): исправлены ошибки NTFS" "INFO" } 
}

if ($failed) { 
    Write-Log "❌ Проблемные: $($failed.Count)" "ERROR"
    $failed | ForEach-Object { Write-Log " - $($_.UserName) ($($_.StoragePath)): $($_.Error)" "ERROR" } 
}

if (($repaired.Count + $failed.Count) -eq 0) { 
    Write-Log "✅ Все в порядке." "INFO" 
}

$csvPath = "$LogDir\VHDX_Report_$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').csv"
$vhdxCheckResults | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
Write-Log "📄 Отчёт: $csvPath" "INFO"

Write-Host "`n✅ Готово. Лог: $LogPath" -ForegroundColor Cyan

Similar Posts:

Метки:

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

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