Private/Plan/Get-TcPlanAction.ps1
|
function Get-TcPlanAction { <# .SYNOPSIS Builds the actions of the plan: one per finding, plus the identity matrix and the go/no-go of each source. .DESCRIPTION A critical finding blocks the cutover. The draft matrix of a source blocks it too, until the customer validates it, and so do duplicates that point to a synced target account, because Exchange Online refuses to write addresses on those. A source whose accounts could not be read has no draft: its matrix action stays, blocking, and says why. The go/no-go sums up the blockers of its source without being one, and says what could not be read. It never says that nothing blocks a source it cannot see. Actions read in order of moment, blockers first, then by severity. #> [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param( [Parameter(Mandatory)][hashtable]$Context, [AllowEmptyCollection()][string[]]$IgnoreNameSuffix = @() ) $procedures = Get-TcProcedureCatalog $actions = [ordered]@{} $newAction = { param([string]$Id, [string]$Role, [string]$Title, [string]$Moment, [string]$Severity, [string]$Procedure, [string]$Origin) [ordered]@{ id = $Id; ruleId = ''; role = $Role; tenant = $Context['Names'][$Role]; wave = $Context['WaveOf'][$Role]; title = $Title; moment = $Moment; severity = $Severity blocking = $false; procedure = $(if ($procedures.Contains($Procedure)) { $Procedure } else { '' }); owner = 'Migration team and Customer'; effort = ''; workload = ''; category = '' relation = ''; finding = ''; action = ''; why = ''; question = ''; items = @(); itemCount = 0; origin = @($Origin) } } foreach ($finding in @(Get-TcProperty $Context['RuleSet'] 'findings' @())) { $role = if ($finding['code'] -eq 'Consolidation') { 'All' } else { $finding['code'] } $id = '{0}-{1}' -f $role, $finding['id'] $number = 2 while ($actions.Contains($id)) { $id = '{0}-{1}-{2}' -f $role, $finding['id'], $number; $number++ } $action = & $newAction $id $role $finding['title'] $finding['moment'] $finding['severity'] $finding['id'] 'Assessment' $action['ruleId'] = $finding['id'] foreach ($key in 'owner', 'effort', 'workload', 'category', 'finding', 'action', 'why', 'question') { $action[$key] = [string]$finding[$key] } if ($finding['role'] -eq 'Both') { $action['relation'] = [string]$finding['tenant'] } $action['items'] = @($finding['items']) $action['itemCount'] = [int]$finding['itemCount'] $action['blocking'] = $finding['severity'] -eq 'Critical' $actions[$id] = $action } $targetUsers = @(Get-TcProperty (Get-TcProperty (Get-TcProperty $Context['Snapshot']['tenants'] 'Target') 'tables') 'users' @()) $synced = @{} foreach ($user in $targetUsers) { if (Get-TcProperty $user 'Synced') { $synced[([string](Get-TcProperty $user 'UPN')).ToLowerInvariant()] = $true } } $targetAccounts = Get-TcTableState -Snapshot $Context['Snapshot'] -Role 'Target' -Table 'users' foreach ($source in @($Context['Sources'])) { $sourceAccounts = Get-TcTableState -Snapshot $Context['Snapshot'] -Role $source -Table 'users' $sourceRead = $sourceAccounts.State -notin 'NotCollected', 'NotFullyRead' $notRead = if (-not $sourceRead) { $sourceAccounts } elseif (-not $Context['Snapshot']['tenants'].Contains('Target')) { $targetAccounts } if ($notRead) { $whose = if (-not $sourceRead) { 'this source' } else { 'the target' } $missing = & $newAction "$source-IDENTITY-MATRIX" $source 'Read the accounts, then have the identity matrix validated' 'Now' 'High' 'IDENTITY-MATRIX' 'Matrix' $missing['workload'] = 'Entra ID' $missing['blocking'] = $true $missing['drafted'] = $false $missing['finding'] = ('No draft: the accounts of {0} could not be read ({1}). {2}' -f $whose, $notRead.Status, $notRead.Message).Trim() $missing['action'] = ('{0} Then review the draft with the customer and get a decision for each row.' -f $notRead.Remedy).Trim() $actions[$missing['id']] = $missing continue } $rows = @(ConvertTo-TcIdentityMatrix -Snapshot $Context['Snapshot'] -SourceRole $source -IgnoreNameSuffix $IgnoreNameSuffix) if ($rows.Count) { $count = { param($Decision) @($rows | Where-Object { $_['Decision'] -eq $Decision }).Count } $review = @($rows | Where-Object { $_['Decision'] -eq 'To review' }) $bookings = @($rows | Where-Object { $_['Notes'] -match 'Bookings' }).Count $matrix = & $newAction "$source-IDENTITY-MATRIX" $source 'Have the identity matrix validated' 'Now' 'High' 'IDENTITY-MATRIX' 'Matrix' $matrix['workload'] = 'Entra ID' $matrix['blocking'] = $true $matrix['finding'] = 'Draft: {0} row(s), {1} to create, {2} likely duplicate(s) in the target, {3} to review (including {4} Bookings page(s)).' -f $rows.Count, (& $count 'Create'), (& $count 'Duplicate'), $review.Count, $bookings if ($targetAccounts.State -in 'NotCollected', 'NotFullyRead') { $matrix['finding'] += ' The accounts of the target could not be read ({0}), so no duplicate was looked for.' -f $targetAccounts.Status } $matrix['action'] = 'Review the draft with the customer and get a decision for each row.' $matrix['items'] = @($review | ForEach-Object { '{0} ({1}){2}' -f $_['Display name'], $_['Source UPN'], $(if ($_['Notes']) { ": $($_['Notes'])" } else { '' }) }) $matrix['itemCount'] = $review.Count $actions[$matrix['id']] = $matrix $duplicates = @($rows | Where-Object { $_['Decision'] -eq 'Duplicate' -and $synced.ContainsKey(([string]$_['Target UPN']).ToLowerInvariant()) }) if ($duplicates.Count) { $action = & $newAction "$source-SYNCED-DUPLICATES" $source ('{0} synced account(s) of the target: addresses to add in on-premises Active Directory' -f $duplicates.Count) 'Before cutover' 'High' 'SYNCED-DUPLICATES' 'Matrix' $action['workload'] = 'Entra ID' $action['blocking'] = $true $action['finding'] = 'These duplicates point to a target account synced from on-premises Active Directory. Exchange Online refuses to write addresses on such an account.' $action['action'] = 'Add the source addresses in on-premises Active Directory, once the domain is verified in the target.' $action['items'] = @($duplicates | ForEach-Object { '{0} ({1}): {2}' -f $_['Display name'], $_['Source UPN'], $_['Target UPN'] }) $action['itemCount'] = $duplicates.Count $actions[$action['id']] = $action } } } foreach ($action in @($actions.Values)) { if ($action['blocking'] -and $action['severity'] -notin 'Critical', 'High') { $action['severity'] = 'High' } } foreach ($source in @($Context['Sources'])) { $blockers = @($actions.Values | Where-Object { $_['role'] -eq $source -and $_['blocking'] }) $go = & $newAction "$source-GO-NOGO" $source "Go/no-go decision for the cutover of $($Context['Names'][$source])" 'Cutover' 'High' 'GO-NOGO' 'Plan' $gaps = @(Get-TcPlanGap -Context $Context -Role $source) $go['finding'] = if ($blockers.Count) { '{0} blocking action(s) to close for this source.' -f $blockers.Count } elseif ($gaps.Count) { 'No blocking action found in what was read.' } else { 'No blocking action for this source.' } if ($gaps.Count) { $go['finding'] += ' Could not be read: {0}.' -f ($gaps -join ', ') } $go['action'] = 'Review the prerequisites with the customer, then record the decision and the decision maker.' $go['items'] = @($blockers | ForEach-Object { $_['title'] }) $go['itemCount'] = $blockers.Count $actions[$go['id']] = $go } $moments = $Context['Moments'] $ranks = @{ Critical = 0; High = 1; Medium = 2; Low = 3; Info = 4 } @($actions.Values | Sort-Object -Stable -Culture 'en-US' { [array]::IndexOf($moments, $_['moment']) }, { -not $_['blocking'] }, { $ranks[$_['severity']] }, { $_['title'] }, { $_['id'] }) } |