ActionPlans/Start-MailboxMigrationAnalyzer.ps1
# MIT License # # Copyright (c) 2020 O365Troubleshooters Team # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Description: ### This script is analyzing the migration reports # Author: ### Cristian Dimofte # Versions: ##################################################################### # Version # Date # Description # ##################################################################### # 1.0 # 08/28/2020 # Initial script # # # # # ##################################################################### ############################## # Common space for functions # ############################## #region Functions ### LogsToAnalyze (Scope: Script) variable will contain mailbox migration logs for all affected users [System.Collections.ArrayList]$script:LogsToAnalyze = @() ### ParsedLogs (Scope: Script) variable will contain parsed mailbox migration logs for all affected users [System.Collections.ArrayList]$script:ParsedLogs = @() ### Get the timestamp from the time the scenario was accessed [string]$ts = Get-Date -Format yyyyMMdd_HHmmss ### Create the location where to save logs related to MailboxMigration scenario [string]$ExportPath = "$global:WSPath\MailboxMigration_$ts" $null = New-Item -ItemType Directory -Path $ExportPath -Force ### Create the full path of the HTML report [string]$script:HTMLFilePath = $ExportPath + "\MailboxMigration_Hybrid_SummaryReport.html" ### Create the PSObject in which to store details about the log used to provide report $Script:DetailsAboutMigrationLog = New-Object PSObject $Script:DetailsAboutMigrationLog | Add-Member -NotePropertyName XMLFullName -NotePropertyValue "" # $Script:DetailsAboutMigrationLog | Add-Member -NotePropertyName XMLShortName -NotePropertyValue "" ### Not implemented yet # $Script:DetailsAboutMigrationLog | Add-Member -NotePropertyName ZipFullName -NotePropertyValue "" ### Not implemented yet $Script:DetailsAboutMigrationLog | Add-Member -NotePropertyName CommandUsedToCollectLogs -NotePropertyValue "" ### <summary> ### Show-MailboxMigrationMenu function is used if the script is started without any parameters ### </summary> function Show-MailboxMigrationMenu { $MailboxMigrationMenu=@" 1 If you have the migration logs in an .xml file 2 If you want to connect to Exchange Online in order to collect the logs B Back to Action plans Select a task by number, or, B to go back to main menu: "@ Write-Log -function "MailboxMigration - Show-MailboxMigrationMenu" -step "Loading the mailbox migration menu" -Description "Success" Clear-Host Write-Host $MailboxMigrationMenu -ForegroundColor White -NoNewline $SwitchFromKeyboard = Read-Host ### Providing a list of options Switch ($SwitchFromKeyboard) { ### If "1" is selected, the script will assume you have the mailbox migration logs in an .xml file "1" { Write-Log -function "MailboxMigration - Show-MailboxMigrationMenu" -step "Loading option 1" -Description "Success" Selected-FileOption } ### If "2" is selected, the script will connect you to Exchange Online "2" { Write-Log -function "MailboxMigration - Show-MailboxMigrationMenu" -step "Loading option 2" -Description "Success" Selected-ConnectToExchangeOnlineOption } ### If "B" is selected, move back to the "O365TroubleshootersMenu" "B" { Write-Log -function "MailboxMigration - Show-MailboxMigrationMenu" -step "Loading option `"B`"" -Description "Success" Start-O365TroubleshootersMenu } ### If you selected anything different than "1", "2" or "B", the Menu will reload default { Write-Host "You selected an option that is not present in the menu (Value inserted from keyboard: `"$SwitchFromKeyboard`")" -ForegroundColor Yellow Write-Host "Press any key to re-load the menu" Write-Log -function "MailboxMigration - Show-MailboxMigrationMenu" -step "Loading option `"default`"" -Description "Reload MailboxMigrationMenu" Read-Host Show-MailboxMigrationMenu } } } ### <summary> ### Selected-FileOption function is used when the information is already saved on a .xml file. ### </summary> ### <param name="FilePath">FilePath parameter is used when the script is started with the FilePath parameter.</param> function Selected-FileOption { [CmdletBinding()] Param ( [string]$FilePath ) [int]$TheNumberOfChecks = 1 ### If FilePath was provided, the script will use it in order to validate if the information from this variable is a correct ### full path of an .xml file. if ($FilePath){ try { ### The script validates that the path provided is of a valid .xml file. Write-Log -function "MailboxMigration - Selected-FileOption" -step "Start validation of `"$FilePath`" file" -Description "Success" [string]$PathOfXMLFile = Validate-XMLPath -XMLFilePath $FilePath } catch { ### In case of error, the script will ask to provide again the full path of the .xml file Write-Log -function "MailboxMigration - Selected-FileOption" -step "Ask for .xml path. Iteration $TheNumberOfChecks" -Description "Error validating initially provided .xml path" [string]$PathOfXMLFile = Ask-ForXMLPath -NumberOfChecks $TheNumberOfChecks } } ### If no FilePath was provided, the script will ask to provide the full path of the .xml file else{ Write-Log -function "MailboxMigration - Selected-FileOption" -step "Ask for .xml path. Iteration $TheNumberOfChecks" -Description "Success. No initial .xml path provided" [string]$PathOfXMLFile = Ask-ForXMLPath -NumberOfChecks $TheNumberOfChecks } ### If PathOfXMLFile variable will match "NotAValidXMLFile|NotAValidPath|ValidationOfFileFailed", we will stop the script if ($PathOfXMLFile -match "NotAValidXMLFile|NotAValidPath|ValidationOfFileFailed") { Write-Log -function "MailboxMigration - Selected-FileOption" -step "Ask for .xml path. Iteration $TheNumberOfChecks" -Description "Error. $PathOfXMLFile matches `"NotAValidXMLFile|NotAValidPath|ValidationOfFileFailed`"" throw "The script will end, because the .xml file provided is not valid from PowerShell's perspective" } else { ### TheMigrationLogs variable will represent MigrationLogs collected using the Collect-MigrationLogs function. Write-Log -function "MailboxMigration - Selected-FileOption" -step "Start analyze of data from `"$PathOfXMLFile`" file" -Description "Success" Create-DetailsAboutMigrationOutput -InfoCollectedFrom XMLFile -XMLPath $PathOfXMLFile Collect-MigrationLogs -XMLFile $PathOfXMLFile } } ### <summary> ### Validate-XMLPath function is used to check if the path provided is a valid .xml file. ### </summary> ### <param name="XMLFilePath">XMLFilePath parameter represents the path the script has to check if it is a valid .xml file.</param> function Validate-XMLPath { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [ValidateScript({Test-Path $_})] [string] $XMLFilePath ) ### Validating if the path has a length greater than 4, and if it is of an .xml file Write-Log -function "MailboxMigration - Validate-XMLPath" -step "Validating if `"$XMLFilePath`" is valid from PowerShell's perspective" -Description "Success" if (($XMLFilePath.Length -gt 4) -and ($XMLFilePath -like "*.xml")) { ### Validating if the .xml file was created by PowerShell $fileToCheck = new-object System.IO.StreamReader($XMLFilePath) if ($fileToCheck.ReadLine() -like "*http://schemas.microsoft.com/powershell*") { Write-Host Write-Host $XMLFilePath -ForegroundColor Cyan -NoNewline Write-Host " seems to be a valid .xml file. We will use it to continue the investigation." -ForegroundColor Green Write-Log -function "MailboxMigration - Validate-XMLPath" -step "`"$XMLFilePath`" is valid from PowerShell's perspective" -Description "Success" } ### If not, the script will set the XMLFilePath to NotAValidXMLFile. This will help in next checks, in order to start collecting the mailbox ### migration logs using other methods else { Write-Log -function "MailboxMigration - Validate-XMLPath" -step "`"$XMLFilePath`" is not valid from PowerShell's perspective" -Description "We will set: XMLFilePath = `"NotAValidXMLFile`"" $XMLFilePath = "NotAValidXMLFile" } $fileToCheck.Close() } ### If the path's length is not greater than 4 characters and the file is not an .xml file the script will set XMLFilePath to NotAValidPath. ### This will help in next checks, in order to start collecting the mailbox migration logs using other methods else { Write-Log -function "MailboxMigration - Validate-XMLPath" -step "`"$XMLFilePath`" is not valid from PowerShell's perspective" -Description "We will set: XMLFilePath = `"NotAValidPath`"" $XMLFilePath = "NotAValidPath" } ### The script returns the value of XMLFilePath return $XMLFilePath } ### <summary> ### Ask-ForXMLPath function is used to ask for the full path of a .xml file. ### </summary> ### <param name="NumberOfChecks">NumberOfChecks is used in order to do an 1-time effort to provide another path of the .xml file, ### in case first time when it was entered, there was a typo </param> function Ask-ForXMLPath { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [int]$NumberOfChecks ) [string]$PathOfXMLFile = "" if ($NumberOfChecks -eq "1") { ### Asking to provide the full path of the .xml file for the first time Write-Host Write-Log -function "MailboxMigration - Ask-ForXMLPath" -step "We are asking to provide the path of the .xml file. Iteration $NumberOfChecks" -Description "Success" Write-Host "Please provide the path of the .xml file: " -ForegroundColor Cyan Write-Host "`t" -NoNewline try { ### PathOfXMLFile variable will contain the full path of the .xml file, if it will be validated (it will be inserted from the keyboard) $PathOfXMLFile = Validate-XMLPath -XMLFilePath (Read-Host) } catch { ### If error, the script is doing the 1-time effort to collect again the full path of the .xml file Write-Log -function "MailboxMigration - Ask-ForXMLPath" -step "Ask for the .xml path" -Description "Error when asked for a new .xml path. Retrying." $NumberOfChecks++ $PathOfXMLFile = Ask-ForXMLPath -NumberOfChecks $NumberOfChecks } } else { ### The script is doing the 1-time effort to collect again the full path of the .xml file Write-Host Write-Log "[INFO] || Asking to provide the path of the .xml file again" -NonInteractive $true Write-Log -function "MailboxMigration - Ask-ForXMLPath" -step "Asking to provide the path of the .xml file again. Iteration $NumberOfChecks" -Description "Success" Write-Host "Would you like to provide the path of the .xml file again?" -ForegroundColor Cyan Write-Host "`t[Y] Yes`t`t[N] No`t`t(default is `"N`"): " -NoNewline -ForegroundColor White $ReadFromKeyboard = Read-Host ### Checking if the path will be provided again, or no. If no, we will continue to collect the mailbox migration logs, using other methods. [bool]$TheKey = $false Switch ($ReadFromKeyboard) { Y {$TheKey=$true} N {$TheKey=$false} Default {$TheKey=$false} } if ($TheKey) { ### If YES was selected, we are asking to provide the path of the .xml file again Write-Host Write-Log -function "MailboxMigration - Ask-ForXMLPath" -step "Please provide again the path of the .xml file" -Description "Success" Write-Host "Please provide again the path of the .xml file: " -ForegroundColor Cyan Write-Host "`t" -NoNewline try { ### Validating the path of the .xml file $PathOfXMLFile = Validate-XMLPath -XMLFilePath (Read-Host) } catch { ### If error, the script will set PathOfXMLFile to ValidationOfFileFailed, which will be used to stop the collection of the logs Write-Log -function "MailboxMigration - Ask-ForXMLPath" -step "Agreed to provide new .xml file. Unable to get an .xml file valid from PowerShell's perspective" -Description "We will set: PathOfXMLFile = `"ValidationOfFileFailed`"" $PathOfXMLFile = "ValidationOfFileFailed" } } else { ### If NO was selected, the script will set PathOfXMLFile to ValidationOfFileFailed, which will be used to collect the logs using other methods Write-Log -function "MailboxMigration - Ask-ForXMLPath" -step "Agreed not to provide new .xml file" -Description "We will set: PathOfXMLFile = `"ValidationOfFileFailed`"" $PathOfXMLFile = "ValidationOfFileFailed" } } ### The function returns the full path of the .xml file, or ValidationOfFileFailed return $PathOfXMLFile } ### <summary> ### Collect-MigrationLogs function is used to collect the mailbox migration logs ### </summary> ### <param name="XMLFile">XMLFile represents the .xml file from which we want to import the mailbox migration logs </param> ### <param name="ConnectToExchangeOnline">ConnectToExchangeOnline parameter will be used to connect to Exchange Online, and collect the ### needed mailbox migration logs, based on the migration type used </param> ### <param name="ConnectToExchangeOnPremises">ConnectToExchangeOnPremises parameter will be used to connect to Exchange On-Premises, and collect the ### the output of Get-MailboxStatistics (the MoveHistory part), for the affected user </param> function Collect-MigrationLogs { [CmdletBinding()] Param ( [parameter(ParameterSetName="XMLFile", Mandatory=$true)] [string]$XMLFile, [Parameter(ParameterSetName = "ConnectToExchangeOnline", Mandatory = $true)] [switch]$ConnectToExchangeOnline, # [Parameter(ParameterSetName = "ConnectToExchangeOnPremises", Mandatory = $true)] ### not implemented yet # [switch]$ConnectToExchangeOnPremises, ### not implemented yet [Parameter(ParameterSetName = "ConnectToExchangeOnline", Mandatory = $false)] # [Parameter(ParameterSetName = "ConnectToExchangeOnPremises", Mandatory = $false)] ### not implemented yet [string[]]$AffectedUsers, [Parameter(ParameterSetName = "ConnectToExchangeOnline", Mandatory = $false)] [ValidateSet("Hybrid", "IMAP", "Cutover", "Staged")] [string]$MigrationType = "Hybrid", [Parameter(ParameterSetName = "ConnectToExchangeOnline", Mandatory = $false)] [string]$AdminAccount ) if ($XMLFile) { ### Importing data in the LogsToAnalyze (Scope: Script) variable Write-Log -function "MailboxMigration - Collect-MigrationLogs" -step "Importing data from `"$XMLFile`" file, in the LogsToAnalyze variable" -Description "Success" $TheMigrationLogs = Import-Clixml $XMLFile foreach ($Log in $TheMigrationLogs) { $LogEntry = New-Object PSObject $LogEntry | Add-Member -NotePropertyName GUID -NotePropertyValue $($Log.MailboxIdentity.ObjectGuid.ToString()) $LogEntry | Add-Member -NotePropertyName Name -NotePropertyValue $($Log.MailboxIdentity.Name.ToString()) $LogEntry | Add-Member -NotePropertyName DistinguishedName -NotePropertyValue $($Log.MailboxIdentity.DistinguishedName.ToString()) $LogEntry | Add-Member -NotePropertyName SID -NotePropertyValue $($Log.MailboxIdentity.SecurityIdentifierString.ToString()) $LogEntry | Add-Member -NotePropertyName Logs -NotePropertyValue $Log $null = $script:LogsToAnalyze.Add($LogEntry) } $TheEnvironment = "FromFile" $LogFrom = "FromFile" $CommandRanToCollect = "FromFile" $FileLocation = $XMLFile $LogType = "FromFile" $TheMigrationType = "FromFile" } elseif ($ConnectToExchangeOnline) { ### Connecting to Exchange Online in order to collect the needed/correct mailbox migration logs #Write-Host "This part is not yet implemented" -ForegroundColor Red if ($MigrationType -eq "Hybrid") { Collect-MoveRequestStatistics -AffectedUsers $AffectedUsers $LogType = "MoveRequestStatistics" $CommandRanToCollect = "MoveRequestStatistics" $FileLocation = "" } elseif ($MigrationType -eq "IMAP") { Collect-SyncRequestStatistics -AffectedUsers $AffectedUsers $LogType = "SyncRequestStatistics" $CommandRanToCollect = "SyncRequestStatistics" $FileLocation = "" } elseif (($MigrationType -eq "Cutover") -or ($MigrationType -eq "Staged")) { Collect-MigrationUserStatistics -AffectedUsers $AffectedUsers $LogType = "MigrationUserStatistics" $CommandRanToCollect = "MigrationUserStatistics" $FileLocation = "" } $TheEnvironment = "Exchange Online" $LogFrom = "FromExchangeOnline" $TheMigrationType = $MigrationType } if ($script:LogsToAnalyze) { foreach ($LogEntry in $script:LogsToAnalyze) { $TheInfo = Create-MoveObject -MigrationLogs $LogEntry -TheEnvironment $TheEnvironment -LogFrom $LogFrom -CommandRanToCollect $CommandRanToCollect -FileLocation $FileLocation -LogType $LogType -MigrationType $TheMigrationType $null = $script:ParsedLogs.Add($TheInfo) } } } ### <summary> ### Create-MoveObject function is used to create the custom MoveObject used to analyze the migration ### </summary> ### <param name="MigrationLogs">MigrationLogs represents the migration logs that need to be parsed </param> ### <param name="TheEnvironment">TheEnvironment represents the environment from which the logs were collected. ### For the moment they came from Exchange Online, or from .xml file </param> ### <param name="LogFrom">LogFrom represents the environment from which the logs were collected. ### For the moment they came from Exchange Online, or from .xml file </param> ### <param name="CommandRanToCollect">CommandRanToCollect represents the exact command ran to collect the logs </param> ### <param name="FileLocation">FileLocation represents the FullName of the .xml file from where the migration log was imported </param> ### <param name="LogType">LogType represents the type of the migration log. ### For the moment the expected values are "FromFile" or "MoveRequestStatistics" </param> ### <param name="MigrationType">MigrationType represents migration type of the logs collected </param> ### <return $MoveAnalysis>MoveAnalysis represents the object containing all parsed logs that need to be listed in the report </return> function Create-MoveObject { param ( $MigrationLogs, [ValidateSet("Exchange Online", "Exchange OnPremises", "FromFile")] [string]$TheEnvironment, [ValidateSet("FromFile", "FromExchangeOnline", "FromExchangeOnPremises")] [string]$LogFrom, [string]$CommandRanToCollect, [string]$FileLocation, [ValidateSet("MoveRequestStatistics", "MoveRequest", "MigrationUserStatistics", "MigrationUser", "MigrationBatch", "SyncRequestStatistics", "SyncRequest", "MailboxStatistics", "FromFile")] [string]$LogType, [ValidateSet("Hybrid", "IMAP", "Cutover", "Staged", "FromFile")] [string]$MigrationType ) # List of fields to output [Array]$OrderedFields = "MailboxInformation","BasicInformation","ExtendedMoveInfo","PerformanceStatistics","FailureSummary","FailureStatistics","LargeItemSummary","BadItemSummary", "MailboxVerificationIfMissingItems","MailboxVerificationAll" # Create the Result object that will be used to store all results $MoveAnalysis = New-Object PSObject $OrderedFields | foreach { $MoveAnalysis | Add-Member -Name $_ -Value $null -MemberType NoteProperty } # Pull everything that we need, that is common to all status types $MoveAnalysis.MailboxInformation = New-MailboxInformation -RequestStats $($MigrationLogs.Logs) $MoveAnalysis.BasicInformation = New-BasicInformation -RequestStats $($MigrationLogs.Logs) $MoveAnalysis.ExtendedMoveInfo = New-ExtendedMoveInfo -RequestStats $($MigrationLogs.Logs) $MoveAnalysis.PerformanceStatistics = New-PerformanceStatistics -RequestStats $($MigrationLogs.Logs) $MoveAnalysis.FailureSummary = New-FailureSummary -RequestStats $($MigrationLogs.Logs) $MoveAnalysis.FailureStatistics = New-FailureStatistics -RequestStats $($MigrationLogs.Logs) $MoveAnalysis.LargeItemSummary = New-LargeItemSummary -RequestStats $($MigrationLogs.Logs) $MoveAnalysis.BadItemSummary = New-BadItemSummary -RequestStats $($MigrationLogs.Logs) $MoveAnalysis.MailboxVerificationIfMissingItems = New-MailboxVerificationIfMissingItems -RequestStats $($MigrationLogs.Logs) $MoveAnalysis.MailboxVerificationAll = New-MailboxVerificationAll -RequestStats $($MigrationLogs.Logs) $DetailsAboutTheMove = New-Object PSObject $DetailsAboutTheMove | Add-Member -NotePropertyName Environment -NotePropertyValue $TheEnvironment $DetailsAboutTheMove | Add-Member -NotePropertyName LogFrom -NotePropertyValue $LogFrom $DetailsAboutTheMove | Add-Member -NotePropertyName LogType -NotePropertyValue $LogType $DetailsAboutTheMove | Add-Member -NotePropertyName MigrationType -NotePropertyValue $MigrationType $DetailsAboutTheMove | Add-Member -NotePropertyName PrimarySMTPAddress -NotePropertyValue $($MigrationLogs.Name) $MoveAnalysis | Add-Member -NotePropertyName DetailsAboutTheMove -NotePropertyValue $DetailsAboutTheMove return $MoveAnalysis } ### <summary> ### New-MailboxInformation function is used to list mailbox information (Alias, DisplayName, ExchangeGUID) ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return PSObject>The function returns a PSObject with information about the mailbox </return> Function New-MailboxInformation { Param( [Parameter(Mandatory = $true)] $RequestStats ) # Build all properties to be added to the oubject New-Object PSObject -Property ([ordered]@{ Alias = [string]$RequestStats.Alias DisplayName = [string]$RequestStats.DisplayName ExchangeGuid = [string]$RequestStats.ExchangeGuid }) } ### <summary> ### New-BasicInformation function is used to list basic information related to the migration ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return PSObject>The function returns a PSObject containing basic information about the mailbox migration </return> Function New-BasicInformation { Param( [Parameter(Mandatory = $true)] $RequestStats ) [string]$TheDirection = "" if (($($RequestStats.WorkloadType.ToString()) -eq "Onboarding") -and ($($RequestStats.RequestStyle.ToString()) -eq "CrossOrg")) { $TheDirection = "On-Premises to Exchange Online" } elseif (($($RequestStats.WorkloadType.ToString()) -eq "Offboarding") -and ($($RequestStats.RequestStyle.ToString()) -eq "CrossOrg")) { $TheDirection = "Exchange Online to On-Premises" } else { $TheDirection = ([String]$RequestStats.Flags) } # Build all properties to be added to the oubject New-Object PSObject -Property ([ordered]@{ Status = ([String]$RequestStats.Status) DataConsistencyScore = [string]$RequestStats.DataConsistencyScore ### Need to provide details about DataConsistencyScoringFactors # [string]$DataConsistencyScoringFactors = "" # foreach ($Factor in $RequestStats.DataConsistencyScoringFactors) { # $DataConsistencyScoringFactors = $DataConsistencyScoringFactors + [string]$Factor # } # DataConsistencyScoringFactors = $DataConsistencyScoringFactors BadItemLimit = ([int][String]$RequestStats.BadItemLimit) BadItemsEncountered = ([int][String]$RequestStats.BadItemsEncountered) LargeItemLimit = ([int][String]$RequestStats.LargeItemLimit) LargeItemsEncountered = ([int][String]$RequestStats.LargeItemsEncountered) BatchName = [string]$RequestStats.BatchName Created = $RequestStats.QueuedTimestamp Completed = $RequestStats.CompletionTimeStamp OverallDuration = [string]$RequestStats.OverallDuration TotalInProgressDuration = [string]$RequestStats.TotalInProgressDuration TotalSuspendedDuration = [string]$RequestStats.TotalSuspendedDuration TotalFailedDuration = [string]$RequestStats.TotalFailedDuration TotalQueuedDuration = [string]$RequestStats.TotalQueuedDuration TotalTransientFailureDuration = [string]$RequestStats.TotalTransientFailureDuration TotalStalledDueToContentIndexingDuration = [string]$RequestStats.TotalStalledDueToContentIndexingDuration TotalStalledDueToMdbReplicationDuration = [string]$RequestStats.TotalStalledDueToMdbReplicationDuration TotalStalledDueToMailboxLockedDuration = [string]$RequestStats.TotalStalledDueToMailboxLockedDuration TotalStalledDueToReadThrottle = [string]$RequestStats.TotalStalledDueToReadThrottle TotalStalledDueToWriteThrottle = [string]$RequestStats.TotalStalledDueToWriteThrottle TotalStalledDueToReadCpu = [string]$RequestStats.TotalStalledDueToReadCpu TotalStalledDueToWriteCpu = [string]$RequestStats.TotalStalledDueToWriteCpu TotalStalledDueToReadUnknown = [string]$RequestStats.TotalStalledDueToReadUnknown TotalStalledDueToWriteUnknown = [string]$RequestStats.TotalStalledDueToWriteUnknown Direction = $TheDirection Flags = ([String]$RequestStats.Flags) RemoteHostName = [string]$RequestStats.RemoteHostName "TotalMailboxSize (bytes)" = Get-Bytes -datasize $RequestStats.TotalMailboxSize TotalMailboxItemCount = [string]$RequestStats.TotalMailboxItemCount "TotalPrimarySize (bytes)" = Get-Bytes -datasize $RequestStats.TotalPrimarySize TotalPrimaryItemCount = [string]$RequestStats.TotalPrimaryItemCount "TotalArchiveSize (bytes)" = Get-Bytes -datasize $RequestStats.TotalArchiveSize TotalArchiveItemCount = [string]$RequestStats.TotalArchiveItemCount TargetDeliveryDomain = [string]$RequestStats.TargetDeliveryDomain SourceEndpointGuid = [string]$RequestStats.SourceEndpointGuid SourceVersion = [string]$RequestStats.SourceVersion SourceDatabase = [string]$RequestStats.SourceDatabase SourceServer = [string]$RequestStats.SourceServer SourceArchiveDatabase = [string]$RequestStats.SourceArchiveDatabase SourceArchiveVersion = [string]$RequestStats.SourceArchiveVersion SourceArchiveServer = [string]$RequestStats.SourceArchiveServer TargetVersion = [string]$RequestStats.TargetVersion TargetDatabase = [string]$RequestStats.TargetDatabase TargetServer = [string]$RequestStats.TargetServer TargetArchiveDatabase = [string]$RequestStats.TargetArchiveDatabase TargetArchiveVersion = [string]$RequestStats.TargetArchiveVersion TargetArchiveServer = [string]$RequestStats.TargetArchiveServer FailureCode = [string]$RequestStats.FailureCode FailureType = [string]$RequestStats.FailureType FailureSide = [string]$RequestStats.FailureSide FailureTimestamp = [string]$RequestStats.FailureTimestamp LastFailure = [string]$RequestStats.LastFailure }) } ### <summary> ### New-ExtendedMoveInfo function is used to list extended information related to the migration ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return PSObject>The function returns a PSObject containing extended information about the mailbox migration </return> Function New-ExtendedMoveInfo { Param( [Parameter(Mandatory = $true)] $RequestStats ) # Build all properties to be added to the oubject New-Object PSObject -Property ([ordered]@{ TotalStalledDueToMailboxLockedDuration = [string]$RequestStats.TotalStalledDueToMailboxLockedDuration FailureSide = [string]$RequestStats.FailureSide PercentComplete = [int][string]$RequestStats.PercentComplete Protected = [string]$RequestStats.Protect StatusDetail = [string]$RequestStats.StatusDetail WorkloadType = [string]$RequestStats.WorkloadType BytesTransferred = Get-Bytes -datasize $RequestStats.BytesTransferred }) } ### <summary> ### New-PerformanceStatistics function is used to list performance statistics related to the migration ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return PSObject>The function returns a PSObject containing performance statistics about the mailbox migration </return> Function New-PerformanceStatistics { Param( [Parameter(Mandatory = $true)] $RequestStats ) New-Object PSObject -Property ([ordered]@{ MigrationDuration = [string]$RequestStats.TotalInProgressDuration AverageSourceLatency = Eval-Safe { $RequestStats.report.sessionstatistics.sourcelatencyinfo.average } AverageDestinationLatency = Eval-Safe { $RequestStats.report.sessionstatistics.destinationlatencyinfo.average } SourceSideDuration = Eval-Safe { $RequestStats.Report.SessionStatistics.SourceProviderInfo.TotalDuration } DestinationSideDuration = Eval-Safe { $RequestStats.Report.SessionStatistics.DestinationProviderInfo.TotalDuration } PercentDurationIdle = Eval-Safe { ((DurationToSeconds $RequestStats.TotalIdleDuration) / (DurationtoSeconds $RequestStats.OverallDuration)) * 100 } -DefaultValue 0 PercentDurationSuspended = Eval-Safe { ((DurationToSeconds $RequestStats.TotalSuspendedDuration) / (DurationtoSeconds $RequestStats.OverallDuration)) * 100 } -DefaultValue 0 PercentDurationFailed = Eval-Safe { ((DurationToSeconds $RequestStats.TotalFailedDuration) / (DurationtoSeconds $RequestStats.OverallDuration)) * 100 } -DefaultValue 0 PercentDurationQueued = Eval-Safe { ((DurationToSeconds $RequestStats.TotalQueuedDuration) / (DurationtoSeconds $RequestStats.OverallDuration)) * 100 } -DefaultValue 0 PercentDurationLocked = Eval-Safe { ((DurationToSeconds $RequestStats.TotalStalledDueToMailboxLockedDuration) / (DurationtoSeconds $RequestStats.OverallDuration)) * 100 } -DefaultValue 0 PercentDurationTransient = Eval-Safe { ((DurationToSeconds $RequestStats.TotalTransientFailureDuration) / (DurationtoSeconds $RequestStats.OverallDuration)) * 100 } -DefaultValue 0 DataTransferRateBytesPerHour = Eval-Safe { ((Get-Bytes $RequestStats.BytesTransferred) / (DurationtoSeconds $RequestStats.TotalInProgressDuration)) * 3600 } DataTransferRateMBPerHour = Eval-Safe { (((Get-Bytes $RequestStats.BytesTransferred) / 1MB) / (DurationtoSeconds $RequestStats.TotalInProgressDuration)) * 3600 } DataTransferRateGBPerHour = Eval-Safe { (((Get-Bytes $RequestStats.BytesTransferred) / 1GB) / (DurationtoSeconds $RequestStats.TotalInProgressDuration)) * 3600 } }) } ### <summary> ### New-FailureSummary function is used to list a summary of failures found in the migration log ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return $compactFailures>The function returns an Array containing summary about failures found in the mailbox migration log </return> Function New-FailureSummary { Param( [Parameter(Mandatory = $true)] $RequestStats ) # Create the object $compactFailures = @() # If we have no failures make sure we write something if ($RequestStats.report.failures -eq $null) { $compactFailures += New-Object PSObject -Property @{ TimeStamp = "None" FailureType = "No Failures Found" } } # Pull out just what we want in the compact report else { $compactFailures += $RequestStats.report.failures | Select-object -Property TimeStamp,Failuretype,Message # Pull in the entries that indicate us starting a mailbox move $compactFailures += ($RequestStats.report.entries | where { $_.message -like "*examining the request*" } | select-Object @{ Name = "TimeStamp"; Expression = { $_.CreationTime } }, @{ Name = "FailureType"; Expression = { "-->MRSPickingUpMove" } }, Message) } $compactFailures = $compactFailures | sort-Object -Property timestamp Return $compactFailures } ### <summary> ### New-LargeItemSummary function is used to list a summary of large items found in the migration log ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return $compactLargeItems>The function returns an Array containing summary about large items found in the mailbox migration log </return> Function New-LargeItemSummary { Param( [Parameter(Mandatory = $true)] $RequestStats ) # Create the object $compactLargeItems = @() # If we have no failures make sure we write something if ($($RequestStats.Report.LargeItems) -eq $null) { $compactLargeItems += New-Object PSObject -Property @{ TimeStamp = "None" FailureType = "No Large Items Found" } } # Pull out just what we want in the compact report else { $compactLargeItems += $($RequestStats.Report.LargeItems) | Select-object -Property TimeStamp, ItemSize, SizeLimit, FolderName, Subject } $compactLargeItems = $compactLargeItems | sort-Object -Property timestamp Return $compactLargeItems } ### <summary> ### New-BadItemSummary function is used to list a summary of bad items found in the migration log ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return $compactBadItems>The function returns an Array containing summary about bad items found in the mailbox migration log </return> Function New-BadItemSummary { Param( [Parameter(Mandatory = $true)] $RequestStats ) # Create the object $compactBadItems = @() # If we have no failures make sure we write something if ($($RequestStats.Report.BadItems) -eq $null) { $compactBadItems += New-Object PSObject -Property @{ TimeStamp = "None" FailureType = "No Bad Items Found" } } # Pull out just what we want in the compact report else { $compactBadItems += $($RequestStats.Report.Failures) | Select-object -Property TimeStamp, FailureType, Message } $compactBadItems = $compactBadItems | sort-Object -Property timestamp Return $compactBadItems } ### <summary> ### New-FailureStatistics function is used to list statistics of failures found in the migration log ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return $FailureStatistics>The function returns an ArrayList containing statistics of failures found in the mailbox migration log </return> function New-FailureStatistics { Param( [Parameter(Mandatory = $true)] $RequestStats ) [System.Collections.ArrayList]$FailureStatistics = @() if ($RequestStats.Report.Failures) { $GroupedFailures = $RequestStats.Report.Failures | group FailureType | select Name, Count foreach ($Failure in $GroupedFailures) { $TheObject = New-Object PSObject $TheObject | Add-Member -NotePropertyName FailureType -NotePropertyValue $($Failure.Name) $TheObject | Add-Member -NotePropertyName FailureCount -NotePropertyValue $($Failure.Count) $null = $FailureStatistics.Add($TheObject) } } else { $TheObject = New-Object PSObject $TheObject | Add-Member -NotePropertyName FailureType -NotePropertyValue "No Failures" $TheObject | Add-Member -NotePropertyName FailureCount -NotePropertyValue "None" $null = $FailureStatistics.Add($TheObject) } return $FailureStatistics } ### <summary> ### New-MailboxVerificationIfMissingItems function is used to list missing items found during the mailbox verification process ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return $MailboxVerificationEntries>The function returns an ArraryList containing the list of missing items found during the mailbox verification process </return> function New-MailboxVerificationIfMissingItems { Param( [Parameter(Mandatory = $true)] $RequestStats ) [System.Collections.ArrayList]$MailboxVerificationEntries = @() if (-not($($RequestStats.Status) -like "*Completed*")) { $TheObject = New-Object PSObject $TheObject | Add-Member -NotePropertyName EntryType -NotePropertyValue "Mailbox Verification Summary" $TheObject | Add-Member -NotePropertyName EntryValue -NotePropertyValue "Migration not completed" $null = $MailboxVerificationEntries.Add($TheObject) } else { $GroupedMailboxVerificationEntries = $RequestStats.Report.MailboxVerification | where {($($_.Source.Count) -ne $($_.Target.Count)) -or ($($_.Corrupt.Count) -ne 0) -or ($($_.Large.Count) -ne 0) -or ($($_.Skipped.Count) -ne 0)} | select Source, Target, Corrupt, Large, Skipped, FolderIsMissing, FolderIsMisplaced, FolderSourcePath, FolderTargetPath if ($GroupedMailboxVerificationEntries) { foreach ($MailboxVerificationEntry in $GroupedMailboxVerificationEntries) { $TheObject = New-Object PSObject $TheObject | Add-Member -NotePropertyName Source -NotePropertyValue $($MailboxVerificationEntry.Source) $TheObject | Add-Member -NotePropertyName Target -NotePropertyValue $($MailboxVerificationEntry.Target) $TheObject | Add-Member -NotePropertyName Corrupt -NotePropertyValue $($MailboxVerificationEntry.Corrupt) $TheObject | Add-Member -NotePropertyName Large -NotePropertyValue $($MailboxVerificationEntry.Large) $TheObject | Add-Member -NotePropertyName Skipped -NotePropertyValue $($MailboxVerificationEntry.Skipped) $TheObject | Add-Member -NotePropertyName FolderIsMissing -NotePropertyValue $($MailboxVerificationEntry.FolderIsMissing) $TheObject | Add-Member -NotePropertyName FolderIsMisplaced -NotePropertyValue $($MailboxVerificationEntry.FolderIsMisplaced) $TheObject | Add-Member -NotePropertyName FolderSourcePath -NotePropertyValue $($MailboxVerificationEntry.FolderSourcePath) $TheObject | Add-Member -NotePropertyName FolderTargetPath -NotePropertyValue $($MailboxVerificationEntry.FolderTargetPath) $null = $MailboxVerificationEntries.Add($TheObject) } } else { $TheObject = New-Object PSObject $TheObject | Add-Member -NotePropertyName EntryType -NotePropertyValue "Mailbox Verification Summary" $TheObject | Add-Member -NotePropertyName EntryValue -NotePropertyValue "Mailbox verification completed. All folders are up-to-date after migration." $null = $MailboxVerificationEntries.Add($TheObject) } } return $MailboxVerificationEntries } ### <summary> ### New-MailboxVerificationAll function is used to list all items found during the mailbox verification process ### </summary> ### <param name="RequestStats">RequestStats represents the migration logs that need to be parsed, in order to extract the information about the mailbox </param> ### <return $MailboxVerificationEntries>The function returns an ArraryList containing all items found during the mailbox verification process </return> function New-MailboxVerificationAll { Param( [Parameter(Mandatory = $true)] $RequestStats ) [System.Collections.ArrayList]$MailboxVerificationEntries = @() if (-not($($RequestStats.Status) -like "*Completed*")) { $TheObject = New-Object PSObject $TheObject | Add-Member -NotePropertyName EntryType -NotePropertyValue "Mailbox Verification Summary" $TheObject | Add-Member -NotePropertyName EntryValue -NotePropertyValue "Migration not completed" $null = $MailboxVerificationEntries.Add($TheObject) } else { $GroupedMailboxVerificationEntries = $RequestStats.Report.MailboxVerification | select Source, Target, Corrupt, Large, Skipped, FolderIsMissing, FolderIsMisplaced, FolderSourcePath, FolderTargetPath foreach ($MailboxVerificationEntry in $GroupedMailboxVerificationEntries) { $TheObject = New-Object PSObject $TheObject | Add-Member -NotePropertyName Source -NotePropertyValue $($MailboxVerificationEntry.Source) $TheObject | Add-Member -NotePropertyName Target -NotePropertyValue $($MailboxVerificationEntry.Target) $TheObject | Add-Member -NotePropertyName Corrupt -NotePropertyValue $($MailboxVerificationEntry.Corrupt) $TheObject | Add-Member -NotePropertyName Large -NotePropertyValue $($MailboxVerificationEntry.Large) $TheObject | Add-Member -NotePropertyName Skipped -NotePropertyValue $($MailboxVerificationEntry.Skipped) $TheObject | Add-Member -NotePropertyName FolderIsMissing -NotePropertyValue $($MailboxVerificationEntry.FolderIsMissing) $TheObject | Add-Member -NotePropertyName FolderIsMisplaced -NotePropertyValue $($MailboxVerificationEntry.FolderIsMisplaced) $TheObject | Add-Member -NotePropertyName FolderSourcePath -NotePropertyValue $($MailboxVerificationEntry.FolderSourcePath) $TheObject | Add-Member -NotePropertyName FolderTargetPath -NotePropertyValue $($MailboxVerificationEntry.FolderTargetPath) $null = $MailboxVerificationEntries.Add($TheObject) } } return $MailboxVerificationEntries } ### <summary> ### Eval-Safe function evaluates an expression and returns the result. If an exception is thrown, returns a default value ### </summary> ### <param name="Expression">Expression represents the expression that need to be evaluated </param> ### <param name="DefaultValue">DefaultValue represents the value that will be returned in case of exception </param> ### <return result of the evaluation, or the default value>The function returns result of the evaluation, or the default value </return> Function Eval-Safe { param( [Parameter(Mandatory=$true)] [ScriptBlock]$Expression, [Parameter(Mandatory=$false)] $DefaultValue = $null ) try { return (Invoke-Command -ScriptBlock $Expression) } catch { Write-Warning ("Eval-Safe: Error: '{0}'; returning default value: {1}" -f $_,$DefaultValue) return $DefaultValue } } ### <summary> ### DurationtoSeconds function transforms a time value in seconds ### </summary> ### <param name="time">Time represents the time value that need to be transformed in seconds </param> ### <return the value of "time" transformed in seconds>The function returns the value of "time" transformed in seconds </return> Function DurationtoSeconds { Param( [Parameter(Mandatory = $false)] $time = $null ) if ($time -eq $null) { 0 } else { $time.TotalSeconds } } ### <summary> ### Get-Bytes function transforms a size value to Bytes ### </summary> ### <param name="datasize">datasize represents the size that need to be transformed in Bytes </param> ### <return the value of "datasize" transformed in Bytes>The function returns the value of "datasize" transformed in Bytes </return> Function Get-Bytes { param ($datasize) if ($datasize) { try { $datasize.tobytes() } catch [Exception] { Parse-ByteQuantifiedSize $datasize } } } ### <summary> ### Parse-ByteQuantifiedSize function transforms a size value to Bytes ### </summary> ### <param name="SerializedSize">SerializedSize represents the size that need to be transformed in Bytes </param> ### <return the value of "SerializedSize" transformed in Bytes>The function returns the value of "SerializedSize" transformed in Bytes </return> Function Parse-ByteQuantifiedSize { param ([Parameter(Mandatory = $true)][string]$SerializedSize) $result = [regex]::Match($SerializedSize, '[^\(]+\((([0-9]+),?)+ bytes\)', [Text.RegularExpressions.RegexOptions]::Compiled) if ($result.Success) { [string]$extractedSize = "" $result.Groups[2].Captures | %{ $extractedSize += $_.Value } return [long]$extractedSize } return [long]0 } ### <summary> ### Selected-ConnectToExchangeOnlineOption function is used to connect to Exchange Online, and collect from there the mailbox migration logs, ### for the affected user, by running the correct commands, based on the migration type ### </summary> ### <param name="AffectedUser">AffectedUser represents the affected user for which we collect the mailbox migration logs </param> ### <param name="MigrationType">MigrationType represents the migration type for which we collect the mailbox migration logs </param> ### <param name="TheAdminAccount">TheAdminAccount represents username of an Admin that we will use in order to connect to Exchange Online </param> function Selected-ConnectToExchangeOnlineOption { Connect-O365PS "EXO" Write-Log -function "MailboxMigration - Selected-ConnectToExchangeOnlineOption" -step "Trying to collect the AffectedUser..." -Description "Success" [string]$AffectedUsers = Ask-ForDetailsAboutUser -NumberOfChecks 1 [System.Collections.ArrayList]$PrimarySMTPAddresses = @() $TheRecipients = Find-TheRecipient -TheEnvironment 'Exchange Online' -TheAffectedUsers $AffectedUsers foreach ($Recipient in $TheRecipients) { $null = $PrimarySMTPAddresses.Add($($Recipient.PrimarySMTPAddress)) } [string]$TheAddresses = "" [int]$Counter = 0 if ($($PrimarySMTPAddresses.Count) -eq 0) { Write-Log -function "MailboxMigration - Selected-ConnectToExchangeOnlineOption" -step "Get list of PrimarySMTPAddresses of the affected users" -Description "We were unable to find any valid SMTP Address to be used for further investigation" throw "We were unable to find any valid SMTP Address to be used for further investigation" } elseif ($($PrimarySMTPAddresses.Count) -eq 1) { $TheAddresses = $PrimarySMTPAddresses[0] } elseif ($($PrimarySMTPAddresses.Count) -gt 1) { foreach ($PrimarySMTPAddress in $PrimarySMTPAddresses) { if ($Counter -eq 0) { [string]$TheAddresses = $PrimarySMTPAddress $Counter++ } elseif (($Counter -le $($PrimarySMTPAddresses.Count))) { [string]$TheAddresses = $TheAddresses + ", $PrimarySMTPAddress" $Counter++ } } } Collect-MigrationLogs -ConnectToExchangeOnline -AffectedUsers $PrimarySMTPAddresses } ### <summary> ### Ask-ForDetailsAboutUser function is used to collect the Affected user. ### </summary> ### <param name="NumberOfChecks">NumberOfChecks is used in order to provide different messages when collecting the affected user ### for the first time, or if you are re-asking for the affected user </param> function Ask-ForDetailsAboutUser { [CmdletBinding()] Param ( [Parameter(Mandatory=$true)] [int] $NumberOfChecks ) Write-Host if ($NumberOfChecks -eq "1") { ### Asking for the affected user, for the first time Write-Log -function "MailboxMigration - Ask-ForDetailsAboutUser" -step "Asking to provide the affected user. Iteration 1" -Description "Success" Write-Host "Please provide the username of the affected user (Eg.: " -NoNewline -ForegroundColor Cyan Write-Host "User1@contoso.com" -NoNewline -ForegroundColor White Write-Host "): " -NoNewline -ForegroundColor Cyan $TheUserName = Read-Host $NumberOfChecks++ Write-Log -function "MailboxMigration - Ask-ForDetailsAboutUser" -step "The affected user provided is: $TheUserName" -Description "Success" } else { ### Re-asking for the affected user Write-Log -function "MailboxMigration - Ask-ForDetailsAboutUser" -step "Re-asking to provide the affected user. Iteration $NumberOfChecks" -Description "Success" Write-Host "Please provide again the username of the affected user (Eg.: " -NoNewline -ForegroundColor Cyan Write-Host "User1@contoso.com" -NoNewline -ForegroundColor White Write-Host "): " -NoNewline -ForegroundColor Cyan $TheUserName = Read-Host Write-Log -function "MailboxMigration - Ask-ForDetailsAboutUser" -step "The affected user provided is: $TheUserName" -Description "Success" } ### Validating if the user provided is the affected user Write-Host Write-Host "You entered " -NoNewline -ForegroundColor Cyan Write-Host "$TheUserName" -NoNewline -ForegroundColor White Write-Host " as being the affected user. Is this correct?" -ForegroundColor Cyan Write-Host "`t[Y] Yes [N] No (default is `"Y`"): " -NoNewline -ForegroundColor White $ReadFromKeyboard = Read-Host [bool]$TheKey = $true Switch ($ReadFromKeyboard) { Y {$TheKey=$true} N {$TheKey=$false} Default {$TheKey=$true} } if ($TheKey) { ### Received confirmation that the user provided is the affected user. Write-Log -function "MailboxMigration - Ask-ForDetailsAboutUser" -step "Got confirmation that `"$TheUserName`" is indeed the affected user" -Description "Success" } else { ### The user provided is not the affected user. Asking again for the affected user. Write-Log -function "MailboxMigration - Ask-ForDetailsAboutUser" -step "`"$TheUserName`" is not the affected user. Starting over the process of asking for the affected user" -Description "Success" [string]$TheUserName = Ask-ForDetailsAboutUser -NumberOfChecks $NumberOfChecks } ### The function will return the affected user return $TheUserName } ### <summary> ### Find-TheRecipient function is used to get output of Get-Recipient ### </summary> ### <param name="TheEnvironment">TheEnvironment represents the environment where to run the command. ### For the moment, we collect this just from Exchange Online </param> ### <param name="TheAffectedUsers">TheAffectedUsers represents the list of users for which to run Get-Recipient command </param> ### <return $Recipients>The function returns the list of Get-Recipient output </return> function Find-TheRecipient { [CmdletBinding()] Param ( [ValidateSet("Exchange Online", "Exchange OnPremises")] [string] $TheEnvironment, [string[]] $TheAffectedUsers ) [System.Collections.ArrayList]$Recipients = @() foreach ($User in $TheAffectedUsers) { $TheCommand = Create-CommandToInvoke -TheEnvironment $TheEnvironment -CommandFor "Recipient" try { Write-Log -function "MailboxMigration - Find-TheRecipient" -step "Collecting `"Get-Recipient`" for `"$User`"" -Description "Success" $ExpressionResults = Invoke-Expression $($TheCommand.FullCommand) Write-Log -function "MailboxMigration - Find-TheRecipient" -step "We were able to identify the recipient in $TheEnvironment for `"$User`".`n`tPrimarySmtpAddress:`t$($ExpressionResults.PrimarySmtpAddress)`n`tExchangeGuid:`t`t$($ExpressionResults.ExchangeGuid)`n`tRecipientType:`t`t$($ExpressionResults.RecipientType)`n`tRecipientTypeDetails:`t$($ExpressionResults.RecipientTypeDetails)" -Description "Success" Write-Log -function "MailboxMigration - Find-TheRecipient" -step "From now on, we will use its PrimarySMTPAddress, `"$($ExpressionResults.PrimarySmtpAddress)`", when providing details about `"$User`"" -Description "Success" $null = $Recipients.Add($ExpressionResults) } catch { Write-Log -function "MailboxMigration - Find-TheRecipient" -step "Unable to identify the Recipient using information you provided (`"$User`")" -Description "Success" } } if ($($Recipients.Count) -eq 0){ Write-Log -function "MailboxMigration - Find-TheRecipient" -step "No recipients in the Organization" -Description "We were unable to identify any Recipients in your organization, for the users you provided" throw "We were unable to identify any Recipients in your organization, for the users you provided" } else { return $Recipients } } ### <summary> ### Create-CommandToInvoke function is used to create the exact command to run, in order to collect the correct migration logs ### </summary> ### <param name="TheEnvironment">TheEnvironment represents the environment in which the command will run </param> function Create-CommandToInvoke { param ( [ValidateSet("Exchange Online", "Exchange OnPremises")] [string] $TheEnvironment, [ValidateSet("MoveRequestStatistics", "MoveRequest", "MigrationUserStatistics", "MigrationUser", "MigrationBatch", "SyncRequestStatistics", "SyncRequest", "MailboxStatistics", "Recipient")] [string] $CommandFor ) $TheResultantCommand = New-Object PSObject if ($TheEnvironment -eq "Exchange Online") { if ($CommandFor -eq "MoveRequestStatistics") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:EXOCommandsPrefix + "MoveRequestStatistics") [string]$TheCommand = "Get-"+ $script:EXOCommandsPrefix + "MoveRequestStatistics `$User -IncludeReport -DiagnosticInfo `"showtimeslots, showtimeline, verbose`" -ErrorAction Stop" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } elseif ($CommandFor -eq "MoveRequest") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:EXOCommandsPrefix + "MoveRequest") [string]$TheCommand = "Get-"+ $script:EXOCommandsPrefix + "MoveRequest `$User -ErrorAction Stop" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } elseif ($CommandFor -eq "MigrationUserStatistics") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:EXOCommandsPrefix + "MigrationUserStatistics") [string]$TheCommand = "Get-"+ $script:EXOCommandsPrefix + "MigrationUserStatistics `$User -IncludeSkippedItems -IncludeReport -DiagnosticInfo `"showtimeslots, showtimeline, verbose`" -ErrorAction Stop" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } elseif ($CommandFor -eq "MigrationUser") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:EXOCommandsPrefix + "MigrationUser") [string]$TheCommand = "Get-"+ $script:EXOCommandsPrefix + "MigrationUser `$User -ErrorAction Stop" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } elseif ($CommandFor -eq "MigrationBatch") { <# $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:EXOCommandsPrefix + "MigrationBatch") [string]$TheCommand = "(Get-"+ $script:EXOCommandsPrefix + "MigrationBatch `$User -IncludeReport -DiagnosticInfo `"showtimeslots, showtimeline, verbose`" -ErrorAction Stop" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand #> } elseif ($CommandFor -eq "SyncRequestStatistics") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:EXOCommandsPrefix + "SyncRequestStatistics") [string]$TheCommand = "Get-"+ $script:EXOCommandsPrefix + "SyncRequestStatistics `$User -IncludeReport -DiagnosticInfo `"showtimeslots, showtimeline, verbose`" -ErrorAction Stop" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } elseif ($CommandFor -eq "SyncRequest") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:EXOCommandsPrefix + "SyncRequest") [string]$TheCommand = "Get-"+ $script:EXOCommandsPrefix + "SyncRequest -Mailbox `$User -ErrorAction Stop" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } elseif ($CommandFor -eq "MailboxStatistics") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:EXOCommandsPrefix + "MailboxStatistics") [string]$TheCommand = "(Get-"+ $script:EXOCommandsPrefix + "MailboxStatistics `$User -IncludeMoveReport -IncludeMoveHistory -ErrorAction Stop).MoveHistory | where {[string]`$(`$_.WorkloadType) -eq `"Onboarding`"} | select -First 1" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } elseif ($CommandFor -eq "Recipient") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:EXOCommandsPrefix + "Recipient") [string]$TheCommand = "Get-"+ $script:EXOCommandsPrefix + "Recipient `$User -ResultSize Unlimited -ErrorAction Stop" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } } else { if ($CommandFor -eq "MailboxStatistics") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:ExOnPremCommandsPrefix + "MailboxStatistics") [string]$TheCommand = "(Get-"+ $script:ExOnPremCommandsPrefix + "MailboxStatistics `$User -IncludeMoveReport -IncludeMoveHistory -ErrorAction Stop).MoveHistory | where {[string]`$(`$_.WorkloadType) -eq `"Offboarding`"} | select -First 1" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } elseif ($CommandFor -eq "Recipient") { $TheResultantCommand | Add-Member -NotePropertyName Command -NotePropertyValue ("Get-"+ $script:ExOnPremCommandsPrefix + "Recipient") [string]$TheCommand = "Get-"+ $script:ExOnPremCommandsPrefix + "Recipient `$User -ResultSize Unlimited -ErrorAction Stop" $TheResultantCommand | Add-Member -NotePropertyName FullCommand -NotePropertyValue $TheCommand } } return $TheResultantCommand } ### <summary> ### Collect-MoveRequestStatistics function is used to get output of Get-MoveRequestStatistics ### </summary> ### <param name="AffectedUsers">AffectedUsers represents the list of users for which to run Get-MoveRequestStatistics command </param> function Collect-MoveRequestStatistics { param ( [string[]] $AffectedUsers ) Write-Log -function "MailboxMigration - Collect-MoveRequestStatistics" -step "Collecting Get-MoveRequestStatistics for each Affected users" -Description "Success" $TheCommand = Create-CommandToInvoke -TheEnvironment 'Exchange Online' -CommandFor "MoveRequestStatistics" if ($($TheCommand.Command)) { foreach ($User in $AffectedUsers) { try { $null = Get-Command $($TheCommand.Command) -ErrorAction Stop Write-Log -function "MailboxMigration - Collect-MoveRequestStatistics" -step "Running the following command:`n`t$($TheCommand.FullCommand.Replace("`$User", "$User"))" -Description "Success" $TheCommandUsedToCollectLogs = ($($TheCommand.FullCommand.Replace("`$User", "$User")) -Split " -ErrorAction")[0] Create-DetailsAboutMigrationOutput -InfoCollectedFrom MoveRequestStatistics -CommandUsedToCollectLogs $TheCommandUsedToCollectLogs try { $ExpressionResults = Invoke-Expression $($TheCommand.FullCommand) Write-Log -function "MailboxMigration - Collect-MoveRequestStatistics" -step "MoveRequestStatistics successfully collected for `"$User`" user" -Description "Success" $LogEntry = New-Object PSObject $LogEntry | Add-Member -NotePropertyName PrimarySMTPAddress -NotePropertyValue $User $LogEntry | Add-Member -NotePropertyName MigrationType -NotePropertyValue "Hybrid" $LogEntry | Add-Member -NotePropertyName LogType -NotePropertyValue "MoveRequestStatistics" $LogEntry | Add-Member -NotePropertyName Logs -NotePropertyValue $ExpressionResults # $LogEntry | Add-Member -NotePropertyName $CommandRanToCollect -NotePropertyValue $TheCommand $void = $script:LogsToAnalyze.Add($LogEntry) # [string]$XMLPath = $ExportPath + "\MoveRequestStatistics_" + [string]$User + ".xml" ### Not implemented yet # [string]$ZIPPath = $ExportPath + "\MoveRequestStatistics_" + [string]$User + ".zip" ### Not implemented yet # $LogEntry | Export-Clixml $XMLPath -Force ### Not implemented yet # Compress-Archive -LiteralPath $XMLPath -DestinationPath $ZIPPath ### Not implemented yet } catch { Write-Log -function "MailboxMigration - Collect-MoveRequestStatistics" -step "We were unable to collect MoveRequestStatistics for `"$User`" user" -Description "Error" } } catch { Write-Log -function "MailboxMigration - Collect-MoveRequestStatistics" -step "You do not have permissions to run `"$($TheCommand.Command)`" command" -Description "Error" #Collect-MoveRequest -AffectedUsers $AffectedUsers } } } } ### <summary> ### Export-MailboxMigrationReportToHTML function is used to create the object that will be converted to HTML report ### </summary> function Export-MailboxMigrationReportToHTML { [System.Collections.ArrayList]$TheObjectToConvertToHTML = @() if ($Script:DetailsAboutMigrationLog.XMLFullName) { [string]$TheString = "Current report was created based on the information we've collected from <b>$($Script:DetailsAboutMigrationLog.XMLFullName)</b>." } elseif ($Script:DetailsAboutMigrationLog.CommandUsedToCollectLogs) { [string]$TheString = "Current report was created based on the information we've collected using the <b>$($Script:DetailsAboutMigrationLog.CommandUsedToCollectLogs)</b> command." } ### Section "Details about log used to provide report" [string]$SectionTitle = "Details of log used to provide report" [string]$Description = "In this section you'll get details from the log used to create the current report." [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "String" -EffectiveDataString $TheString $null = $TheObjectToConvertToHTML.Add($TheCommand) foreach ($Entry in $script:ParsedLogs) { ### Section "Mailbox Information" [string]$SectionTitle = "Mailbox Information" [string]$Description = "Below is the `"Mailbox Information`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.MailboxInformation) -TableType "List" $null = $TheObjectToConvertToHTML.Add($TheCommand) ### Section "Basic Information" if ($($Entry.BasicInformation.Status) -eq "Failed") { $SectionTitleColor = "Red" } elseif ($($Entry.BasicInformation.Status) -eq "Completed") { $SectionTitleColor = "Green" } else { $SectionTitleColor = "Black" } [string]$SectionTitle = "Basic Information" [string]$Description = "Below are the `"Basic Information`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration`<p`>` *` The title of this section is colored red if the <u>Status</u> of the migration is <u>Failed</u>" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor $SectionTitleColor -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.BasicInformation) -TableType "List" $null = $TheObjectToConvertToHTML.Add($TheCommand) ### Section "Extended Move Info" if ($($Entry.ExtendedMoveInfo.StatusDetail) -like "*Fail*") { $SectionTitleColor = "Red" } elseif ($($Entry.ExtendedMoveInfo.StatusDetail) -eq "Completed") { $SectionTitleColor = "Green" } else { $SectionTitleColor = "Black" } [string]$SectionTitle = "Extended Move Info" [string]$Description = "Below are the `"Extended Move Info`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration`<p`>` *` The title of this section is colored in Red in case the <u>StatusDetail</u> of the migration contains <u>Failed</u> in it" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor $SectionTitleColor -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.ExtendedMoveInfo) -TableType "List" $null = $TheObjectToConvertToHTML.Add($TheCommand) ### Section "Performance Statistics" [string]$SectionTitle = "Performance Statistics" [string]$Description = "Below are the `"Performance Statistics`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.PerformanceStatistics) -TableType "List" $null = $TheObjectToConvertToHTML.Add($TheCommand) ### Section "Large Items Summary" if ($($Entry.LargeItemSummary.TimeStamp) -ne "None") { $SectionTitleColor = "Red" } else { $SectionTitleColor = "Green" } [string]$SectionTitle = "Large Items Summary" [string]$Description = "Below is the `"Large Items Summary`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration`<p`>` *` The title of this section is colored in Red in case the migration contains at least one Large item" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor $SectionTitleColor -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.LargeItemSummary) -TableType "Table" $null = $TheObjectToConvertToHTML.Add($TheCommand) ### Section "Bad Items Summary" if ($($Entry.BadItemSummary.TimeStamp) -ne "None") { $SectionTitleColor = "Red" } else { $SectionTitleColor = "Green" } [string]$SectionTitle = "Bad Items Summary" [string]$Description = "Below is the `"Bad Items Summary`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration`<p`>` *` The title of this section is colored in Red in case the migration contains at least one Bad item" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor $SectionTitleColor -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.BadItemSummary) -TableType "Table" $null = $TheObjectToConvertToHTML.Add($TheCommand) ### Section "Mailbox Verification - If Missing Items" if ($($Entry.MailboxVerificationIfMissingItems.EntryValue)) { if ($($Entry.MailboxVerificationIfMissingItems.EntryValue) -eq "Migration not completed") { $SectionTitleColor = "Black" } elseif ($($Entry.MailboxVerificationIfMissingItems.EntryValue) -eq "Mailbox verification completed. All folders are up-to-date after migration.") { $SectionTitleColor = "Green" } $TableType = "Table" } else { $SectionTitleColor = "Red" $TableType = "List" } [string]$SectionTitle = "Mailbox Verification - List Missing Items" [string]$Description = "Below is the `"Missing items found during Mailbox verification`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration`<p`>` *` The title of this section is colored in Red in case the migration contains at least one missing item" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor $SectionTitleColor -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.MailboxVerificationIfMissingItems) -TableType $TableType $null = $TheObjectToConvertToHTML.Add($TheCommand) ### Section "Mailbox Verification - All Items" if ($($Entry.MailboxVerificationAll.EntryValue)) { $TableType = "Table" } else { $TableType = "List" } [string]$SectionTitle = "Mailbox Verification - List All Items" [string]$Description = "Below are the details about `"All items found during Mailbox verification`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor "Black" -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.MailboxVerificationAll) -TableType $TableType $null = $TheObjectToConvertToHTML.Add($TheCommand) ### Section "Failure Statistics" if ($($Entry.FailureStatistics.FailureCount) -ne "None") { $SectionTitleColor = "Red" } else { $SectionTitleColor = "Green" } [string]$SectionTitle = "Failure Statistics" [string]$Description = "Below are the `"Failure Statistics`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration`<p`>` *` The title of this section is colored in Red in case the migration contains at least one Failure" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor $SectionTitleColor -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.FailureStatistics) -TableType "Table" $null = $TheObjectToConvertToHTML.Add($TheCommand) <# ### Section "Failure Summary" if ($($Entry.FailureSummary.TimeStamp)) { if ($($Entry.FailureSummary.TimeStamp) -ne "None") { $SectionTitleColor = "Red" } } else { $SectionTitleColor = "Green" } [string]$SectionTitle = "Failure Summary" [string]$Description = "Below are the `"Failure Summary`" for <u>$($Entry.MailboxInformation.Alias)</u>'s migration`<p`>` *` The title of this section is colored in Red in case the migration contains at least one Failure" [PSCustomObject]$TheCommand = New-ObjectForHTMLReport -SectionTitle $SectionTitle -SectionTitleColor $SectionTitleColor -Description $Description -DataType "CustomObject" -EffectiveDataArrayList $($Entry.FailureSummary) -TableType "Table" $null = $TheObjectToConvertToHTML.Add($TheCommand) #> } Export-ReportToHTML -FilePath $script:HTMLFilePath -PageTitle "Mailbox Migration Report" -ReportTitle "Mailbox Migration - Hybrid - Summary Report" -TheObjectToConvertToHTML $TheObjectToConvertToHTML } ### <summary> ### Create-DetailsAboutMigrationOutput function is used to create details that will be used in the first section of the HTML report ### </summary> ### <param name="InfoCollectedFrom">InfoCollectedFrom represents the type of info collected. ### For the moment, the possible values to use are: "XMLFile" or "MoveRequestStatistics" </param> ### <param name="XMLPath">XMLPath represents the FullName of the XML file </param> ### <param name="CommandUsedToCollectLogs">CommandUsedToCollectLogs represents the exact command used to collect the migration log </param> function Create-DetailsAboutMigrationOutput { param ( [Parameter(Mandatory=$true, Position=0)] [ValidateSet("XMLFile", "MoveRequestStatistics")] [string]$InfoCollectedFrom, [Parameter(ParameterSetName = "XML", Mandatory=$false, Position=1)] [string]$XMLPath, [Parameter(ParameterSetName = "MoveRequestStatistics", Mandatory=$false, Position=1)] [string]$CommandUsedToCollectLogs ) if ($InfoCollectedFrom -eq "XMLFile") { $Script:DetailsAboutMigrationLog.XMLFullName = $XMLPath } elseif ($InfoCollectedFrom -eq "MoveRequestStatistics") { # $Script:DetailsAboutMigrationLog.XMLShortName = {NeedToCalculateValueForThis} ### Not implemented yet # $Script:DetailsAboutMigrationLog.ZipFullName = {NeedToCalculateValueForThis} ### Not implemented yet $Script:DetailsAboutMigrationLog.CommandUsedToCollectLogs = $CommandUsedToCollectLogs } } ### <summary> ### Start-MailboxMigrationMainScript function is used to start the main script ### </summary> function Start-MailboxMigrationMainScript { Write-Log -function "MailboxMigration - Start-MailboxMigrationMainScript" -step "Show-MailboxMigrationMenu" -Description "Success" Show-MailboxMigrationMenu Write-Log -function "MailboxMigration - Start-MailboxMigrationMainScript" -step "Export-MailboxMigrationReportToHTML" -Description "Success" Export-MailboxMigrationReportToHTML Write-Host "For more details please check the logs located on:" -ForegroundColor White Write-Host "`t$ExportPath" -ForegroundColor Cyan write-Log -function "MailboxMigration - Start-MailboxMigrationMainScript" -step "Write on screen location of the logs: $ExportPath" -Description "Success" Write-Host "In order to check summary report of this migration, please take a look on the following HTML report:" -ForegroundColor White Write-Host "`t$script:HTMLFilePath" -ForegroundColor Cyan write-Log -function "MailboxMigration - Start-MailboxMigrationMainScript" -step "Write on screen location of the HTML report: $script:HTMLFilePath" -Description "Success" Write-Log -function "MailboxMigration - Start-MailboxMigrationMainScript" -step "Read-Key" -Description "Success" Read-Key Write-Log -function "MailboxMigration - Start-MailboxMigrationMainScript" -step "Start-O365TroubleshootersMenu" -Description "Success" Start-O365TroubleshootersMenu } #endregion Functions ############### # Main script # ############### #region Main script try { Write-Log -function "MailboxMigration" -step "Start-MailboxMigrationMainScript" -Description "Success" Start-MailboxMigrationMainScript } catch { Write-Log -function "MailboxMigration" -step "MainScript" -Description "$_" Write-Log -function "MailboxMigration" -step "MainScript" -Description "Error. Script will now exit" Write-Host "[ERROR] || $_" -ForegroundColor Red Write-Host "[ERROR] || Script will now exit" -ForegroundColor Red } #endregion Main script |