Private/Get-FslProfileStoreFinding.ps1
|
function Get-FslProfileStoreFinding { <# .SYNOPSIS Opt-in profile-store checks for Invoke-FslDiagnostic: scans the profile share(s) with Get-FslProfileReport and turns capacity risks (>=85% of maximum Warning, >=95% Critical) and structural anomalies (leftover folders, multi-disk folders) into findings. FSLogix's own event 33 only fires below 200 MB free - this check warns weeks earlier. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [AllowEmptyCollection()] [string[]]$Path = @(), # Auto-detected scans skip unreachable stores quietly (verbose only); # explicitly requested scans keep reporting a Warning finding. [switch]$SkipUnreachable, # Disk paths from event-33 findings: a container that FSLogix reports # as nearly full but the store sees far below 85% is the classic # 'VHDX resized without extending the partition inside' case. [AllowEmptyCollection()] [string[]]$CrossCheckPath = @() ) $storePaths = @($Path | Where-Object { $_ }) if ($storePaths.Count -eq 0) { try { $profilesKey = Get-ItemProperty -Path 'HKLM:\SOFTWARE\FSLogix\Profiles' -ErrorAction Stop $storePaths = @(ConvertTo-FslLocationList -Value $profilesKey.VHDLocations) } catch { $storePaths = @() } } if ($storePaths.Count -eq 0) { Write-Warning 'Profile store scan requested, but no path was given and no local VHDLocations were found - pass -ProfileStorePath explicitly.' return } foreach ($storePath in $storePaths) { $disks = @() try { $disks = @(Get-FslProfileReport -Path $storePath -ErrorAction Stop) } catch { if ($SkipUnreachable) { Write-Verbose ("Auto store scan skipped for '{0}': {1}" -f $storePath, $_.Exception.Message) } else { New-FslFinding -Category ProfileStore -Check 'Profile store scan' -Severity Warning -Target $storePath ` -Message ("Could not scan profile store '{0}': {1}" -f $storePath, $_.Exception.Message) ` -Recommendation 'Verify the account running the diagnostic can browse the share (on Azure Files: Storage File Data SMB Share Elevated Contributor).' } continue } $flagged = $false foreach ($disk in ($disks | Where-Object { $null -ne $_.PercentOfMax -and [double]$_.PercentOfMax -ge 85 })) { $flagged = $true $severity = 'Warning' if ([double]$disk.PercentOfMax -ge 95) { $severity = 'Critical' } $label = [string]$disk.UserName if (-not $label) { $label = Split-Path ([string]$disk.Folder) -Leaf } New-FslFinding -Category ProfileStore -Check 'Container capacity' -Severity $severity -Target $label ` -Message ("Container is at {0}% of its maximum size ({1} GB used). FSLogix's own warning (event 33) only fires below 200 MB free - act before sign-ins fail." -f $disk.PercentOfMax, $disk.SizeGB) ` -Evidence ("Disk: {0}. Last modified {1}." -f $disk.Disk, $disk.LastModified) ` -Recommendation 'Free space inside the profile first (Remove-FslOrphanedOst for stale Outlook caches, OneDrive cache, temp data); if that is not enough, raise the VHDX maximum with the user signed out (Resize-VHD, then extend the partition inside) - SizeInMBs only affects newly created containers.' ` -HelpUri 'https://learn.microsoft.com/en-us/fslogix/reference-configuration-settings' } foreach ($anomalyGroup in ($disks | Where-Object { $_.Anomaly } | Group-Object Folder)) { $flagged = $true $first = $anomalyGroup.Group | Select-Object -First 1 $groupSize = [math]::Round((($anomalyGroup.Group | Measure-Object SizeGB -Sum).Sum), 1) New-FslFinding -Category ProfileStore -Check 'Profile store anomaly' -Severity Warning -Target (Split-Path ([string]$anomalyGroup.Name) -Leaf) ` -Message ([string]$first.Anomaly) ` -Evidence ("Folder: {0} ({1} GB across {2} disk(s), last modified {3})." -f $anomalyGroup.Name, $groupSize, $anomalyGroup.Group.Count, $first.LastModified) ` -Recommendation 'Verify, then clean up: Get-FslOrphanedDisk classifies ownerless disks against the identity infrastructure; Remove-FslOrphanedDisk archives or deletes confirmed leftovers (WhatIf-first).' } foreach ($checkPath in $CrossCheckPath) { $crossMatch = $disks | Where-Object { [string]$_.Disk -eq $checkPath -and $null -ne $_.PercentOfMax -and [double]$_.PercentOfMax -lt 85 } | Select-Object -First 1 if ($crossMatch) { $flagged = $true $crossLabel = [string]$crossMatch.UserName if (-not $crossLabel) { $crossLabel = Split-Path ([string]$crossMatch.Folder) -Leaf } New-FslFinding -Category ProfileStore -Check 'Resize incomplete' -Severity Warning -Target $crossLabel ` -Message ("Event 33 reports this container below 200 MB free, but the store sees it at only {0}% of its maximum - the classic sign that the VHDX was resized without extending the partition INSIDE." -f $crossMatch.PercentOfMax) ` -Evidence ("Disk: {0} ({1} GB)." -f $crossMatch.Disk, $crossMatch.SizeGB) ` -Recommendation 'With the user signed out: Mount-VHD, Resize-Partition to the supported maximum, dismount. Event 33 stops at the next logon once the volume inside actually has room.' } } if (-not $flagged) { New-FslFinding -Category ProfileStore -Check 'Profile store' -Severity Pass -Target $storePath ` -Message ("Scanned {0} container(s) on '{1}': no capacity or structural findings." -f $disks.Count, $storePath) } # Metrics travel alongside the findings (separated by PSTypeName) so # the caller can persist them for the capacity forecast. $diskMetrics = @() foreach ($disk in $disks) { if (-not $disk.Disk -or $null -eq $disk.PercentOfMax) { continue } $diskMetrics += [pscustomobject]@{ Disk = [string]$disk.Disk UserName = [string]$disk.UserName SizeGB = [double]$disk.SizeGB PercentOfMax = [double]$disk.PercentOfMax } } [pscustomobject]@{ PSTypeName = 'FSLogixDoctor.StoreMetric' StorePath = [string]$storePath Disks = $diskMetrics } } } |