Private/Backup/Invoke-RestoreWorkflow.ps1
|
#region Invoke-RestoreWorkflow.ps1 - Restore Workflow Orchestration Module <# .SYNOPSIS Orchestration and workflow management functions for restore operations. .DESCRIPTION This module provides comprehensive workflow orchestration for ACL restore operations, coordinating backup validation, target verification, ACL application, and result reporting. It serves as the central coordination point for all restore operation components. BUSINESS VALUE: - Ensures consistent restore operation workflows - Provides centralized error handling and recovery - Coordinates complex multi-step restore processes - Enables standardized restore operation reporting .NOTES Author: Jeffrey Stuhr Module: Find-UnknownSID.RestoreOperations Dependencies: Test-BackupValidation, Restore-ACLOperation modules Version: 2.1.0 SECURITY CONSIDERATIONS: - Validates all inputs before processing - Implements safe operation ordering and rollback - Provides comprehensive audit trail of operations - Ensures proper resource cleanup and disposal #> function Invoke-RestoreWorkflow { <# .SYNOPSIS Orchestrates complete ACL restoration workflow from backup to completion. .DESCRIPTION Central orchestration function that coordinates all aspects of ACL restoration: - Backup discovery and validation (supports bulk operations) - Target object verification - ACL application with safety checks - Result verification and reporting - Error handling and recovery This function implements the complete restore workflow with proper error handling, rollback capabilities, and comprehensive logging. It supports both single object restoration and bulk restoration for search base scopes. BUSINESS VALUE: - Standardizes restore operation procedures - Reduces manual errors through automation - Provides consistent restore operation outcomes - Enables scalable disaster recovery operations .PARAMETER TargetObjectDN Distinguished name of the object to restore ACL for, OR a search base DN for bulk restoration of all objects within that scope. Must be a valid Active Directory object DN. .PARAMETER BackupPath Directory containing backup files for automatic discovery. For single objects: finds the most recent backup for the specified object. For bulk operations: discovers all backup files within the search base scope. .PARAMETER BackupFile Specific backup file path for targeted restoration. Takes precedence over BackupPath parameter. .PARAMETER ValidationLevel Level of backup validation: Basic, Standard, or Comprehensive. Higher levels provide more thorough validation but take longer. .PARAMETER VerifyRestoration Whether to verify successful restoration after ACL application. Recommended for critical operations requiring confirmation. .PARAMETER WhatIf Preview restore operation without making actual changes. Shows what would be restored for validation purposes. .PARAMETER CorrelationId Unique identifier for tracking this workflow operation. .EXAMPLE PS> Invoke-RestoreWorkflow -TargetObjectDN "CN=TestUser,CN=Users,DC=contoso,DC=com" -BackupPath "C:\Backups" DESCRIPTION: Complete restore workflow using automatic backup discovery OUTPUT: RestoreWorkflowResult with detailed operation status USE CASE: Standard restoration for disaster recovery .EXAMPLE PS> Invoke-RestoreWorkflow -TargetObjectDN $dn -BackupFile $specificBackup -ValidationLevel Comprehensive -VerifyRestoration DESCRIPTION: High-assurance restore with comprehensive validation and verification OUTPUT: Detailed workflow result with verification status USE CASE: Critical system restoration requiring maximum validation .EXAMPLE PS> Invoke-RestoreWorkflow -TargetObjectDN $dn -BackupPath $path -WhatIf DESCRIPTION: Preview restore operation without making changes OUTPUT: Shows what would be restored for approval workflow USE CASE: Change management and approval processes .INPUTS [string] TargetObjectDN from pipeline or parameter [string] Backup file or directory paths .OUTPUTS [PSCustomObject] RestoreWorkflowResult with properties: - Success: Overall workflow success status - TargetObjectDN: DN of the processed object - WorkflowSteps: Array of completed workflow steps - BackupValidation: Backup validation results - TargetValidation: Target object validation results - ACLOperation: ACL application results - VerificationResult: Optional restoration verification - Duration: Total workflow execution time - CorrelationId: Tracking identifier .NOTES Author: Jeffrey Stuhr Blog: https://www.techbyjeff.net LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/ WORKFLOW STAGES: 1. Input validation and parameter processing 2. Backup discovery and validation 3. Target object verification 4. ACL application with safety checks 5. Optional restoration verification 6. Result compilation and reporting PERFORMANCE CHARACTERISTICS: - Standard workflow: 200-500ms per object - Comprehensive validation adds 100-200ms - Verification adds 50-100ms overhead - Memory usage: ~2MB per concurrent workflow TROUBLESHOOTING: - For workflow failures: .\Troubleshooting\Common\Backup-Restore-Issues.md - For performance issues: .\Troubleshooting\Performance\Optimization-Guide.md - For validation errors: .\Troubleshooting\Common\Backup-Restore-Issues.md .LINK .\Troubleshooting\Common\Backup-Restore-Issues.md .\Documentation\RestoreOperations-Refactoring-Plan.md #> [CmdletBinding(SupportsShouldProcess)] [OutputType('RestoreWorkflowResult')] param( [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$TargetObjectDN, [Parameter()] [string]$BackupPath, [Parameter()] [string]$BackupFile, [Parameter()] [ValidateSet('Basic', 'Standard', 'Comprehensive')] [string]$ValidationLevel = 'Standard', [Parameter()] [switch]$VerifyRestoration, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) begin { Write-Verbose "Starting restore workflow orchestration - CorrelationId: $CorrelationId" # Import required modules if not already loaded if (-not (Get-Command Test-BackupIntegrity -ErrorAction SilentlyContinue)) { . $PSScriptRoot\Test-BackupValidation.ps1 } if (-not (Get-Command Set-ObjectACL -ErrorAction SilentlyContinue)) { . $PSScriptRoot\Restore-ACLOperation.ps1 } } process { Write-Verbose "Starting restore workflow for target: $TargetObjectDN" Write-Verbose "Starting restore workflow" # Trim and sanitize input first $TargetObjectDN = $TargetObjectDN.Trim() # Validate target DN format after trimming if (-not ($TargetObjectDN -match '^(CN|OU|DC)=')) { throw "Invalid target object DN format" } # Validate that either BackupFile or BackupPath is specified if (-not $BackupFile -and -not $BackupPath) { throw "Either BackupFile or BackupPath must be specified" } # Validate file/path existence and security if ($BackupFile) { # Sanitize the file path $BackupFile = [System.IO.Path]::GetFullPath($BackupFile) # Validate file extension $allowedExtensions = @('.xml', '.clixml') $fileExtension = [System.IO.Path]::GetExtension($BackupFile).ToLower() if ($fileExtension -notin $allowedExtensions) { throw "Invalid file extension. Only .xml and .clixml files are allowed" } # Check if file exists if (-not (Test-Path $BackupFile -PathType Leaf)) { throw "Backup file not found: $BackupFile" } } if ($BackupPath) { # Sanitize the directory path $BackupPath = [System.IO.Path]::GetFullPath($BackupPath) # Check if directory exists if (-not (Test-Path $BackupPath -PathType Container)) { throw "Backup directory not found: $BackupPath" } } $workflowStartTime = Get-Date $workflowSteps = @() $overallSuccess = $false $errorMessage = $null $allResults = @() try { Write-Verbose "Starting restore workflow for: $TargetObjectDN - CorrelationId: $CorrelationId" # Step 1: Validate and prepare inputs $workflowSteps += [PSCustomObject]@{ StepName = "Input Validation" StartTime = Get-Date Status = "InProgress" Duration = $null ErrorMessage = $null } $workflowSteps[-1].Status = "Completed" $workflowSteps[-1].Duration = (Get-Date) - $workflowSteps[-1].StartTime # Step 2: Determine restore mode and discover backups $workflowSteps += [PSCustomObject]@{ StepName = "Backup Discovery and Mode Detection" StartTime = Get-Date Status = "InProgress" Duration = $null BackupFiles = @() RestoreMode = $null ErrorMessage = $null } $backupFilesToProcess = @() $restoreMode = "Single" if ($BackupFile) { # Single file mode - specific backup file provided # Validate and sanitize file path $BackupFile = $BackupFile.Trim() # Normalize path to prevent traversal attacks try { $BackupFile = [System.IO.Path]::GetFullPath($BackupFile) } catch { throw "Invalid backup file path: $BackupFile" } if (-not (Test-Path $BackupFile)) { throw "Backup file not found" } # Validate file extension for security $allowedExtensions = @('.xml', '.clixml') $fileExtension = [System.IO.Path]::GetExtension($BackupFile) if ($fileExtension -notin $allowedExtensions) { throw "Invalid backup file extension: $fileExtension. Only .xml and .clixml files are allowed." } $backupFilesToProcess = @($BackupFile) $restoreMode = "Single" } elseif ($BackupPath) { # Directory mode - discover backup files # Validate and sanitize directory path $BackupPath = $BackupPath.Trim() # Normalize path to prevent traversal attacks try { $BackupPath = [System.IO.Path]::GetFullPath($BackupPath) } catch { throw "Invalid backup directory path: $BackupPath" } if (-not (Test-Path $BackupPath -PathType Container)) { throw "Backup directory not found" } # Get all XML backup files in the directory $allBackupFiles = @(Get-ChildItem -Path $BackupPath -Filter "*.xml" -File) if ($allBackupFiles.Count -eq 0) { throw "No backup files found in directory: $BackupPath" } Write-Verbose "Found $($allBackupFiles.Count) backup files in $BackupPath" # Convert to file paths immediately to avoid PowerShell collection issues $allBackupFilePaths = @() foreach ($file in $allBackupFiles) { if ($file -and $file.FullName) { $allBackupFilePaths += $file.FullName } } Write-Verbose "Successfully collected $($allBackupFilePaths.Count) valid file paths" # Determine if this is a bulk operation based on number of files and target DN # Check if the TargetObjectDN appears to be a search base (high-level OU/DC) with multiple backup files if ($allBackupFilePaths.Count -gt 1 -and ($TargetObjectDN -match '^(OU|DC)=')) { # Bulk restore mode - find all backup files that represent objects within the search base $restoreMode = "Bulk" foreach ($backupFilePath in $allBackupFilePaths) { try { Write-Verbose "Processing backup file: $backupFilePath" # Quick peek at backup file to get the actual object DN $backupMetadata = Import-Clixml -Path $backupFilePath -ErrorAction Stop $backupObjectDN = $backupMetadata.ObjectDN # Check if this backup object is within the search base scope if ($backupObjectDN -like "*$TargetObjectDN") { Write-Verbose "Adding backup file for object: $backupObjectDN" $backupFilesToProcess += $backupFilePath } } catch { Write-Warning "Failed to process backup file '$backupFilePath': $($_.Exception.Message)" } } } else { # Single restore mode - find exact match for the target object $restoreMode = "Single" # Try to find exact match based on filename patterns $possibleMatches = @() $firstComponent = $TargetObjectDN.Split(',')[0] $sanitizedFirstComponent = $firstComponent -replace '[\\/:*?"<>|,=]', '_' -replace '\s+', '_' foreach ($backupFilePath in $allBackupFilePaths) { $filename = [System.IO.Path]::GetFileNameWithoutExtension($backupFilePath) # Check both original format and sanitized format if ($filename -like "*$firstComponent*" -or $filename -like "*$sanitizedFirstComponent*") { $possibleMatches += $backupFilePath } } if ($possibleMatches.Count -gt 0) { # Sort by modification time and take the most recent $sortedMatches = $possibleMatches | Sort-Object {(Get-Item $_).LastWriteTime} -Descending $backupFilesToProcess = @($sortedMatches[0]) Write-Verbose "Using exact match backup: $($sortedMatches[0])" } else { throw "No backup files found for object $TargetObjectDN in path $BackupPath" } } } else { throw "Either BackupFile or BackupPath must be specified" } $workflowSteps[-1].Status = "Completed" $workflowSteps[-1].Duration = (Get-Date) - $workflowSteps[-1].StartTime $workflowSteps[-1].BackupFiles = $backupFilesToProcess $workflowSteps[-1].RestoreMode = $restoreMode Write-Verbose "Restore mode: $restoreMode - Processing $($backupFilesToProcess.Count) backup files" # Step 3: Process each backup file $workflowSteps += [PSCustomObject]@{ StepName = "Bulk Restoration Processing" StartTime = Get-Date Status = "InProgress" Duration = $null ProcessedObjects = 0 SuccessfulRestores = 0 FailedRestores = 0 ErrorMessage = $null } foreach ($backupFile in $backupFilesToProcess) { try { Write-Verbose "Processing backup file: $backupFile" # Load backup data to get the actual target object DN $backupData = Import-Clixml -Path $backupFile -ErrorAction Stop $actualTargetDN = $backupData.ObjectDN # Set-ObjectACL honours $WhatIfPreference and prints the preview itself, so under # -WhatIf the restore still runs through validation; this guard only answers -Confirm. if (-not $WhatIfPreference -and -not $PSCmdlet.ShouldProcess($actualTargetDN, "Restore ACL from backup $backupFile")) { Write-Verbose "Restore declined at the confirmation prompt: $actualTargetDN" continue } # Restore this individual object $individualResult = Restore-IndividualObject -TargetObjectDN $actualTargetDN -BackupData $backupData -BackupFile $backupFile -ValidationLevel $ValidationLevel -VerifyRestoration:$VerifyRestoration -CorrelationId $CorrelationId # Add workflow-specific type name $individualResult.PSObject.TypeNames.Insert(0, 'RestoreWorkflowResult') $allResults += $individualResult $workflowSteps[-1].ProcessedObjects++ if ($individualResult.Success) { $workflowSteps[-1].SuccessfulRestores++ Write-Verbose "Successfully restored: $actualTargetDN" } else { $workflowSteps[-1].FailedRestores++ Write-Warning "Failed to restore: $actualTargetDN - $($individualResult.ErrorMessage)" } } catch { $workflowSteps[-1].FailedRestores++ $workflowSteps[-1].ProcessedObjects++ # Preserve original error message for debugging $originalErrorMessage = $_.Exception.Message $failedResult = [PSCustomObject]@{ Success = $false TargetObjectDN = "Unknown (from $backupFile)" ErrorMessage = $originalErrorMessage BackupFile = $backupFile CorrelationId = $CorrelationId } $failedResult.PSTypeNames.Insert(0, 'RestoreWorkflowResult') $allResults += $failedResult Write-Warning "Failed to process backup file $backupFile : $originalErrorMessage" } } $workflowSteps[-1].Status = "Completed" $workflowSteps[-1].Duration = (Get-Date) - $workflowSteps[-1].StartTime # Determine overall success $successfulRestores = $workflowSteps[-1].SuccessfulRestores $totalProcessed = $workflowSteps[-1].ProcessedObjects $overallSuccess = ($successfulRestores -gt 0) -and ($workflowSteps[-1].FailedRestores -eq 0) if ($restoreMode -eq "Bulk") { Write-Verbose "Bulk restore completed - $successfulRestores/$totalProcessed successful - CorrelationId: $CorrelationId" } else { Write-Verbose "Single restore completed - Success: $overallSuccess - CorrelationId: $CorrelationId" } } catch { $overallSuccess = $false $errorMessage = $_.Exception.Message # Mark current step as failed if in progress if ($workflowSteps.Count -gt 0 -and $workflowSteps[-1].Status -eq "InProgress") { $workflowSteps[-1].Status = "Failed" $workflowSteps[-1].Duration = (Get-Date) - $workflowSteps[-1].StartTime $workflowSteps[-1].ErrorMessage = $errorMessage } Write-Error "Restore workflow failed for $TargetObjectDN : $errorMessage - CorrelationId: $CorrelationId" } $workflowDuration = (Get-Date) - $workflowStartTime # Compile comprehensive workflow result if ($restoreMode -eq "Bulk") { # Return summary result for bulk operations $result = [PSCustomObject]@{ Success = $overallSuccess TargetObjectDN = $TargetObjectDN RestoreMode = $restoreMode TotalObjects = $workflowSteps[-1].ProcessedObjects SuccessfulRestores = $workflowSteps[-1].SuccessfulRestores FailedRestores = $workflowSteps[-1].FailedRestores IndividualResults = $allResults EntriesRestored = [int](($allResults | Where-Object { $_.Success } | Measure-Object -Property EntriesRestored -Sum).Sum -as [int]) WorkflowSteps = $workflowSteps ValidationLevel = $ValidationLevel Duration = $workflowDuration WhatIfMode = $WhatIfPreference CorrelationId = $CorrelationId CompletedAt = Get-Date ErrorMessage = if ($overallSuccess) { $null } else { $errorMessage } } $result.PSTypeNames.Insert(0, 'RestoreWorkflowResult') } else { # Return individual result for single operations if ($allResults.Count -gt 0) { $result = $allResults[0] # Add workflow-level properties $result | Add-Member -MemberType NoteProperty -Name "WorkflowSteps" -Value $workflowSteps -Force $result | Add-Member -MemberType NoteProperty -Name "ValidationLevel" -Value $ValidationLevel -Force $result | Add-Member -MemberType NoteProperty -Name "Duration" -Value $workflowDuration -Force $result | Add-Member -MemberType NoteProperty -Name "WhatIfMode" -Value $WhatIfPreference -Force $result | Add-Member -MemberType NoteProperty -Name "CompletedAt" -Value (Get-Date) -Force } else { # Fallback result $result = [PSCustomObject]@{ Success = $overallSuccess TargetObjectDN = $TargetObjectDN RestoreMode = $restoreMode EntriesRestored = 0 WorkflowSteps = $workflowSteps ValidationLevel = $ValidationLevel Duration = $workflowDuration WhatIfMode = $WhatIfPreference CorrelationId = $CorrelationId CompletedAt = Get-Date ErrorMessage = if ($overallSuccess) { $null } else { $errorMessage } } $result.PSTypeNames.Insert(0, 'RestoreWorkflowResult') } } Write-Verbose "Restore workflow result compiled - Success: $overallSuccess - Duration: $($workflowDuration.TotalMilliseconds)ms - CorrelationId: $CorrelationId" return $result } end { Write-Verbose "Completed restore workflow orchestration - CorrelationId: $CorrelationId" } } function Restore-IndividualObject { <# .SYNOPSIS Restores a single object from its backup data. .DESCRIPTION Helper function that handles the restoration of a single Active Directory object from backup data. This function performs all the validation, verification, and ACL application steps for individual objects. .PARAMETER TargetObjectDN Distinguished name of the specific object to restore. .PARAMETER BackupData Pre-loaded backup data for the object. .PARAMETER BackupFile Path to the backup file (for reference/logging). .PARAMETER ValidationLevel Level of validation to perform. .PARAMETER VerifyRestoration Whether to verify the restoration was successful. .PARAMETER CorrelationId Correlation ID for tracking. .OUTPUTS [PSCustomObject] Individual restore result #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$TargetObjectDN, [Parameter(Mandatory)] [PSCustomObject]$BackupData, [Parameter(Mandatory)] [string]$BackupFile, [Parameter()] [string]$ValidationLevel = 'Standard', [Parameter()] [switch]$VerifyRestoration, [Parameter()] [string]$CorrelationId ) $startTime = Get-Date try { Write-Verbose "Restoring individual object: $TargetObjectDN from $BackupFile" # Validate backup data $backupValidation = Test-BackupIntegrity -BackupData $BackupData -ExpectedObjectDN $TargetObjectDN -ValidationLevel $ValidationLevel -CorrelationId $CorrelationId if (-not $backupValidation.IsValid) { throw "Backup validation failed: $($backupValidation.ErrorMessage)" } # Validate target object exists and is accessible $targetValidation = Get-RestorationTarget -TargetObjectDN $TargetObjectDN -CheckPermissions -CorrelationId $CorrelationId if (-not $targetValidation.IsValid) { throw "Target validation failed: $($targetValidation.Issues -join '; ')" } # Apply ACL restoration $aclOperation = Set-ObjectACL -TargetObjectDN $TargetObjectDN -BackupData $BackupData -VerifyApplication:$VerifyRestoration -CorrelationId $CorrelationId if (-not $aclOperation.Success) { throw "ACL application failed: $($aclOperation.ErrorMessage)" } # Create success result $result = [PSCustomObject]@{ Success = $true TargetObjectDN = $TargetObjectDN BackupFile = $BackupFile EntriesRestored = [int]$(if ($aclOperation.ModificationsApplied) { $aclOperation.ModificationsApplied } else { 0 }) BackupValidation = $backupValidation TargetValidation = $targetValidation ACLOperation = $aclOperation Duration = (Get-Date) - $startTime CorrelationId = $CorrelationId ErrorMessage = $null } $result.PSTypeNames.Insert(0, 'RestoreWorkflowResult') Write-Verbose "Successfully restored object: $TargetObjectDN ($($result.EntriesRestored) entries)" return $result } catch { $errorResult = [PSCustomObject]@{ Success = $false TargetObjectDN = $TargetObjectDN BackupFile = $BackupFile EntriesRestored = 0 Duration = (Get-Date) - $startTime CorrelationId = $CorrelationId ErrorMessage = $_.Exception.Message } $errorResult.PSTypeNames.Insert(0, 'RestoreWorkflowResult') Write-Warning "Failed to restore object: $TargetObjectDN - $($_.Exception.Message)" return $errorResult } } function Start-RestoreOperation { <# .SYNOPSIS Initializes and prepares for restore operation execution. .DESCRIPTION Performs initial setup and validation for restore operations including environment checks, permission validation, and resource preparation. .PARAMETER OperationScope Scope of the restore operation: Single, Multiple, or Bulk. .PARAMETER CorrelationId Unique identifier for tracking this operation initialization. .EXAMPLE PS> Start-RestoreOperation -OperationScope Single Initializes environment for single object restoration. .OUTPUTS [PSCustomObject] RestoreOperationContext #> [CmdletBinding()] [OutputType('RestoreOperationContext')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory operation context; nothing on the system changes')] param( [Parameter()] [ValidateSet('Single', 'Multiple', 'Bulk')] [string]$OperationScope = 'Single', [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) process { try { Write-Verbose "Initializing restore operation context - Scope: $OperationScope - CorrelationId: $CorrelationId" # Check Active Directory module availability $adModuleAvailable = $false try { Import-Module ActiveDirectory -ErrorAction Stop $adModuleAvailable = $true } catch { Write-Warning "Active Directory module not available: $($_.Exception.Message)" } # Check current user context and permissions $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $userGroups = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).Groups | ForEach-Object { try { $_.Translate([System.Security.Principal.NTAccount]).Value } catch { $_.Value } } # Prepare operation context $context = [PSCustomObject]@{ PSTypeName = 'RestoreOperationContext' OperationScope = $OperationScope CurrentUser = $currentUser UserGroups = $userGroups ADModuleAvailable = $adModuleAvailable InitializedAt = Get-Date CorrelationId = $CorrelationId MemoryUsage = [System.GC]::GetTotalMemory($false) ProcessId = $PID } Write-Verbose "Restore operation context initialized successfully - CorrelationId: $CorrelationId" return $context } catch { Write-Error "Failed to initialize restore operation context: $($_.Exception.Message) - CorrelationId: $CorrelationId" throw } } } function Complete-RestoreOperation { <# .SYNOPSIS Finalizes restore operation with cleanup and summary reporting. .DESCRIPTION Performs final cleanup, resource disposal, and summary reporting for completed restore operations. Ensures proper resource management and provides comprehensive operation statistics. .PARAMETER OperationContext Operation context from Start-RestoreOperation. .PARAMETER Results Array of restore workflow results to summarize. .PARAMETER CorrelationId Unique identifier for tracking this operation completion. .EXAMPLE PS> Complete-RestoreOperation -OperationContext $context -Results $results Finalizes restore operation with cleanup and reporting. .OUTPUTS [PSCustomObject] RestoreOperationSummary #> [CmdletBinding()] [OutputType('RestoreOperationSummary')] param( [Parameter(Mandatory)] [ValidateNotNull()] [PSCustomObject]$OperationContext, [Parameter()] [PSCustomObject[]]$Results = @(), [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) process { try { Write-Verbose "Finalizing restore operation - CorrelationId: $CorrelationId" # Calculate operation statistics $totalOperations = $Results.Count $successfulOperations = @($Results | Where-Object { $_.Success }).Count $failedOperations = $totalOperations - $successfulOperations # Calculate timing statistics $totalDuration = if ($Results.Count -gt 0) { ($Results | Measure-Object -Property Duration -Sum).Sum } else { [TimeSpan]::Zero } $averageDuration = if ($Results.Count -gt 0) { [TimeSpan]::FromTicks($totalDuration.Ticks / $Results.Count) } else { [TimeSpan]::Zero } # Memory usage comparison $finalMemoryUsage = [System.GC]::GetTotalMemory($false) $memoryDelta = $finalMemoryUsage - $OperationContext.MemoryUsage # Cleanup resources [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() $summary = [PSCustomObject]@{ PSTypeName = 'RestoreOperationSummary' OperationScope = $OperationContext.OperationScope TotalOperations = $totalOperations SuccessfulOperations = $successfulOperations FailedOperations = $failedOperations SuccessRate = if ($totalOperations -gt 0) { [math]::Round(($successfulOperations / $totalOperations) * 100, 2) } else { 0 } TotalDuration = $totalDuration AverageDuration = $averageDuration MemoryDelta = $memoryDelta CompletedAt = Get-Date CorrelationId = $CorrelationId } Write-Verbose "Restore operation completed - Success Rate: $($summary.SuccessRate)% - CorrelationId: $CorrelationId" return $summary } catch { Write-Error "Failed to complete restore operation: $($_.Exception.Message) - CorrelationId: $CorrelationId" throw } } } #endregion |