BatchMoveToTeams.ps1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
<#PSScriptInfo .VERSION 1.0.3 .GUID 448ba368-354d-481a-b886-5cae6de703ed .AUTHOR Alexey Smelovskiy .COMPANYNAME .COPYRIGHT 2021 .TAGS Teams Skype sfb move migrate move-csuser .LICENSEURI .PROJECTURI https://github.com/microsoft/BatchMoveToTeams .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES #> <# .DESCRIPTION This script is supposed to help large organizations to automate moving their Skype onprem users to Teams. Optimized to work up to 20 times faster on a large number of users in a batch (several thousand users and more) due to parallel processing. #> param ( #Path to CSV file with UPNs of users to migrate to Teams. First line should always be "UPN" (without double quotes) [Parameter(Mandatory=$true)] [string]$InputUsersCsv, [switch]$ForceSkypeEvUsersToTeamsNoEV, [switch]$SkipAllPrerequisiteChecks, [string]$OuToSkip ) <# .SYNOPSIS Script to automate Skype onprem user move to Teams only for large batch migrations (several thousand users in a batch). Features: - Move speed 10-20x faster due to parallel processing The script works 10-20 times faster than a traditional one as it executes user moves in parallel as much as possible (but not exceeding the cloud threshold of max user moves at a time) - Automatic re-try logic for failed to move users For users who failed to move initially (e.g. throttling or some other error) the script will automatically retry them (3 times by default) so you don't have to do it manually - Move prerequisite checks Sort out and report users who don't meet the prerequisites so that the identified missing prerequisites can be corrected for those users. - Rich reporting capabilities Script provides comprehensive reporting for every action or check and will report each user processing and results at various stages of migration and also a summary of the results, including how many were moved successfully, how many failed to migrate (grouped by the error message) how many were retried, etc. which is really helpful during the migration to identify bottlenecks and address issues at an early stage Limitations: - The script currently does not support the move of Skype onprem users enabled for EV as well as enabling EV capabilities in Teams for moved users. This is currently in testing and will be added soon with a new version of the script. .DESCRIPTION The script will process all users from the input CSV file (InputUsersCsv parameter). There are 2 main parts of the script: 1. Check pre-requisites before the move (Use SkipAllPrerequisiteChecks parameter to skip this step). Below are the conditions that will trigger user to NOT be moved to Teams only (checks are performed in the order below): - User does not exist onprem (onprem Get-CsUser fails) - User is located in a particular OU that should be skipped (if user is in the OU specified in $OuToSkip, the acccount won't be moved to Teams) - User is already in o365 - Either LineURI attribute is populated onprem or EnterpriseVoiceEnabled onprem attribute is set to True (the script will be updated later to support EV user moves) - User is not licensed for Skype and/or Teams in o365 2. Move to Teams only - Users will be moved in parallel batches (works 10-20 times faster than moving users one by one) - Users initially failed to move will be retried 3 times by default .PARAMETER InputUsersCsv File with users to be moved. "UPN" (without double quotes) must be the first line (header), than each user's UPN value on a separate line .PARAMETER ForceSkypeEvUsersToTeamsNoEV By default the script will not move Skype onprem users enabled for Enterprise Voice. If this parameter is used the script will forcefully move those users to Teams without EV functionality .PARAMETER SkipAllPrerequisiteChecks Will not perform any pre-requisite checks and try to move all users specified in the input file .PARAMETER OuToSkip Users located in this OU in local Active Directory will not be moved to Teams. Can be full OU DN or just a part of it. Wildcards are allowed. .INPUTS A CSV file with user UPNs to be moved from Skype onprem to Teams only. "UPN" (without double quotes) must be the first line (header), than each user's UPN value on a separate line .OUTPUTS Log file to track the progress and results of the move. The file will be created in the same directory as the input csv file (specified in inputUsersCsv parameter) and have a DateTime stamp appended to its name: "$ScriptWorkDir\MoveResults$(Get-Date -Format '_MM-dd-yyyy_HH-mm-ss').txt", e.g.: "c:\scripts\teamsmove\MoveResults_02-09-2021_17-15-01.txt" Log file structure. Use Excel to easily analyze: - Individual user entries: Date/Time, Operation, UPN, Result, Result Details e.g.: 02/16/2021 22:26:23,PrerequisiteCheck,Testmove3@contoso.com,ReadyToMove,User is ready to be moved to Teams 02/16/2021 22:26:24,PrerequisiteCheck,Testmove4@contoso.com,Skipped,User not found - Summary entries: Date/Time, Summary Operation, Succeeded #, Failed #, Time Taken e.g.: 02/16/2021 22:26:25,PrereqSummary,Ready to move: 4,Pre-reqs not met: 3,Time taken: 00:00:04.3187720 02/16/2021 22:26:34,MoveSummary,Moved Successfully: 4,Failed to move: 0,Time taken: 00:00:09.3513578 .NOTES Version: 1.0 Author: ALEXEY SMELOVSKIY Creation Date: 2/17/2021 Purpose/Change: Initial script development .EXAMPLE The below command will move all users specified in the input csv file bypassing any prerequisite check and will skip (not move) users in "OU=DisabledUsers,DC=contoso,DC=com" Organizational Unit in local Active directory C:\scripts\teamsmove\MigrateToTeams.ps1 -inputUsersCsv "C:\scripts\teamsmove\userlist.csv" -OuToSkip "OU=DisabledUsers,DC=contoso,DC=com" -SkipAllPrerequisiteChecks #> #Script working directory. Output log and some temporary files will be stored here. By default - same directory as input csv file withe users $ScriptWorkDir = Split-Path $InputUsersCsv #$PSScriptRoot #Log file to track the results of the move $MoveResultsLog = "$ScriptWorkDir\MoveResults$(Get-Date -Format '_MM-dd-yyyy_HH-mm-ss').txt" #Connect to SfBO/Teams powershell. Cred prompt will only be displayed if $cred is blank (credentials haven't been prompted yet). sfbsession will be re-established automatically if it times out If (!($cred)) {$cred = get-credential -Message "Enter the credentials of your Teams/Skype admin in o365"} if ($msteamsconnection -eq $null) { Import-Module MicrosoftTeams #$sfbSession = New-CsOnlineSession -Credential $cred #Import-PSSession $sfbSession -AllowClobber $msteamsconnection = Connect-MicrosoftTeams -Credential $cred } #Number of users to be moved from Skype onprem to Teams only in a single batch in parallel [int]$ParallelExecutions = 25 #Number of retry cycles for users that initially failed the migration [int]$RetryCycles = 3 #Import users from CSV file $InputUsersList = Import-Csv -Path $InputUsersCsv #Catch all errors $ErrorActionPreference = "SilentlyContinue" #Stores users that met the pre-requisites check $global:UsersWithPrereqsMet = @() #Stores users that failed (to be able to re-try them during next cycle) $RetryUsers = @() #Check if user has required licenses with the following components enabled: Teams and SfBO function UserIsLicensed([string]$UserUpn) { $AssignedPlans = (Get-CsOnlineUser $UserUpn).AssignedPlan $LicArray = @() foreach ($AssignedPlan in $AssignedPlans) { [xml]$xmlAssignedPlan = $AssignedPlan $AssignedPlanStatus = $xmlAssignedPlan.XmlValueAssignedPlan.Plan.CapabilityStatus if ($AssignedPlanStatus -eq "Enabled") { $AssignedPlanName = $xmlAssignedPlan.XmlValueAssignedPlan.Plan.Capability.Capability.Plan $LicArray += $AssignedPlanName } } If ($licArray.Contains("Teams") -and $licArray.Contains("MCOProfessional")) {$CommandResult = "True"} else {$CommandResult = "False"} return $CommandResult <# License names in Get-CsOnlineUser.AssignedPlan: MCOMEETADD - Audio Conferencing MCOEV - Phone System MCOProfessional - Skype Online Plan 2 Teams - Teams #> } Function GetHostedMigrationServiceUrl { [string]$TenantIdentity = (Get-CsTenant).Identity [int]$strStart = $TenantIdentity.IndexOf("lync") + 4 #4 is the length of "lync" $strLength = $TenantIdentity.Length - $indStart - 12 #12 is the length of "001,DC=local" (tenant id always ends with "001,DC=local") #"https://admin1a.online.lync.com/HostedMigration/hostedmigrationService.svc" [string]$HostedMigrationServiceUrl = "https://admin$($TenantIdentity.Substring($indStart,$strLength)).online.lync.com/HostedMigration/hostedmigrationService.svc" return $HostedMigrationServiceUrl } function CheckPrerequisites($UserList) { $i = 0 $StartTime = Get-Date foreach($user in $UserList) { #Progress counter $i++ Write-Host "Checking pre-reqs for: " -NoNewline; Write-Host "$($user.UPN): " -NoNewline -ForegroundColor Cyan $timestamp = Get-Date -Format "MM/dd/yyyy HH:mm:ss" $GetCsUserError = $null $SkypeUser = get-csUser "sip:$($user.UPN)" -ErrorVariable GetCsUserError #If get-csuser fails, throw the error below if ($GetCsUserError) { $ActionType = "Pre-requisite check" $ActionResult = "Skipped" $ActionResultDetails = "User not found" Write-Host "UserNotFound" -ForegroundColor Red } else { #Check if user is in OU that should be skipped (OuToSkip parameter) #if (2 -eq 1) - uncomment this line and comment the line below to disable this pre-req check if ($SkypeUser.Identity.Parent.ToString() -like $OuToSkip) { $ActionType = "Pre-requisite check" $ActionResult = "Skipped" $ActionResultDetails = "User is in Skipped OU" Write-Host "$ActionResult - $ActionResultDetails" -ForegroundColor Yellow } else { #Check if user is already in o365 #if (2 -eq 1) - uncomment this line and comment the line below to disable this pre-req check if ($SkypeUser.HostingProvider -contains "sipfed.online.lync.com") { $ActionType = "Pre-requisite check" $ActionResult = "Skipped" $ActionResultDetails = "User is already in the cloud" Write-Host "$ActionResult - $ActionResultDetails" -ForegroundColor Yellow } else { #Check for EV attributes Onprem - skip user if it is EV enabled onprem or line uri is set #if (2 -eq 1) - uncomment this line and comment the line below to disable this pre-req check if (($SkypeUser.LineURI -or $SkypeUser.EnterpriseVoiceEnabled) -and (!($ForceSkypeEvUsersToTeamsNoEV))) { $ActionType = "Pre-requisite check" $ActionResult = "Skipped" $ActionResultDetails = "User is either EV enabled onprem or has onprem LineURI" Write-Host "$ActionResult - $ActionResultDetails" -ForegroundColor Yellow } else { #Check for required licenses #if (2 -eq 1) - uncomment this line and comment the line below to disable this pre-req check if ((UserIsLicensed($user.UPN)) -ne "True") { $ActionType = "Pre-requisite check" $ActionResult = "Skipped" $ActionResultDetails = "User doesn't have proper licenses assigned" Write-Host "$ActionResult - $ActionResultDetails" -ForegroundColor Yellow } else #Pre-reqs satisfied, prepare for the move { $ActionType = "Pre-requisite check" $ActionResult = "ReadyToMove" $ActionResultDetails = "User is ready to be moved to Teams" Write-Host $ActionResult -ForegroundColor Green $global:UsersWithPrereqsMet += $user }#final else ends }#EV else ends }#User already in the cloud else ends }#Suspended OU ends }#Usernotfound else ends "$timestamp,PrerequisiteCheck,$($user.UPN),$ActionResult,$ActionResultDetails" | Out-File $MoveResultsLog -Append Write-Progress -Activity �Checking Prerequisites - $([math]::Round($i/$InputUsersList.count*100))%� -status �Checking user $($user.UPN)� -PercentComplete ($i/$InputUsersList.count*100) }#Foreach end $EndTime = Get-Date Write-Host "" Write-Host "====================================================" Write-Host " Pre-Reqs SUMMARY " -BackgroundColor DarkYellow Write-Host "Ready to move: `t`t $($UsersWithPrereqsMet.Count)" #-ForegroundColor Green Write-Host "Pre-reqs not met: `t $($UserList.Count - $UsersWithPrereqsMet.Count)" #-ForegroundColor Red Write-Host "Time taken: `t`t $($EndTime-$StartTime)" #-BackgroundColor Magenta Write-host "Log file: `t`t`t $MoveResultsLog" Write-Host " " -BackgroundColor DarkYellow Write-Host "====================================================" Write-Host "" "$EndTime,PrereqSummary,Ready to move: $($UsersWithPrereqsMet.Count),Pre-reqs not met: $($UserList.Count - $UsersWithPrereqsMet.Count),Time taken: $($EndTime-$StartTime)" | Out-File $MoveResultsLog -Append } function BatchMoveUsers($MoveUserList) { #Re-create tmp folder Remove-Item "$ScriptWorkDir\tmp" -Recurse -Force -Confirm:$false | out-null New-Item -Path "$ScriptWorkDir\tmp" -ItemType "directory" | out-null #Store start timestamp $StartTime = Get-Date #write-host $UserBatch -BackgroundColor Blue #Initialize the batch variables $currentUserBatch = @() $BatchUserCounter = 0 $CurrentUserCounter = 0 $Global:RetryUsers = @() foreach ($user in $MoveUserList) { #Start batch counters $BatchUserCounter++ $CurrentUserCounter++ $MoveCsUserResultError = $null $currentUserBatch += $user #If last batch is less than $ParallelExecutions then we need to split it in smaller batches $TotalUsersLeft = $moveuserlist.Count - $CurrentUserCounter + $BatchUserCounter <# write-host "####################################" write-host "ParallelExecutions = $ParallelExecutions" write-host "TotalUsersLeft = $TotalUsersLeft" write-host "CurrentUserCounter = $CurrentUserCounter" write-host "BatchUserCounter = $BatchUserCounter" write-host "####################################" #> Write-Progress -Activity �Moving a batch of $BatchUserCounter users - $([math]::Round(($CurrentUserCounter-$BatchUserCounter)/$MoveUserList.count*100))%� -status �Current number of users moved: $($CurrentUserCounter - $BatchUserCounter)� -PercentComplete (($CurrentUserCounter - $BatchUserCounter)/$MoveUserList.count*100) if (($BatchUserCounter -eq $ParallelExecutions) -or (($BatchUserCounter -lt $ParallelExecutions) -and ($currentUserBatch.Count -ge $TotalUsersLeft))) #(($TotalUsersLeft -le $ParallelExecutions) -and ($BatchUserCounter -eq $TotalUsersLeft))) #and -gt than #formula to calc optimal batch size for the rest# { Write-Host "Processing a batch of $($currentUserBatch.Count) users: $($currentUserBatch.UPN)" -ForegroundColor Cyan #<main parallell processing> $ScriptBlock = { param($BatchUser, $cred, $ScriptWorkDir) Write-Host "Processing $($BatchUser.Upn) inside the batch job" -ForegroundColor grey Move-CsUser -Identity "sip:$($BatchUser.UPN)" -Target sipfed.online.lync.com -MoveToTeams -HostedMigrationOverrideUrl $(GetHostedMigrationServiceUrl) -Credential $cred -confirm:$false -BypassAudioConferencingCheck -BypassEnterpriseVoiceCheck -errorVariable MoveCsUserResultError -Report "$ScriptWorkDir\tmp\$($BatchUser.UPN).csv" -UseOAuth #| Out-Null } foreach($BatchUser in $currentUserBatch) { Start-Job -Name UserMoveJob $ScriptBlock -ArgumentList $BatchUser,$cred, $ScriptWorkDir | Out-Null $UserMoveJob = Get-Job -Name UserMoveJob } #</main parallell processing> #<Process batch results> #Wait for all jobs to complete While ($UserMoveJob.State -eq "Running") { Start-Sleep 1 } #Get batch job output $JobOutput = Receive-Job $UserMoveJob #-Keep $UserMoveJob | Remove-Job foreach($BatchUser in $currentUserBatch) { $UserMoveReport = import-csv "$ScriptWorkDir\tmp\$($BatchUser.UPN).csv" "$($UserMoveReport.StartTime),MoveToTeams,$($BatchUser.UPN),$($UserMoveReport.ErrorMsg)" | Out-File $MoveResultsLog -Append #If user move failed, add it to an array if ($UserMoveReport.ErrorMsg -ne "Success") { $global:RetryUsers += New-Object -TypeName psobject -Property @{ UPN = $BatchUser.UPN ErrorMsg = $UserMoveReport.ErrorMsg} } } #</Process batch results> #Clear the batch variables $BatchUserCounter = 0 $currentUserBatch = @() } } #Report move summary $EndTime = Get-Date $SuccessfullyMigrated = $UsersWithPrereqsMet.Count - $global:RetryUsers.Count $EncountedErrors = $($global:RetryUsers | Group-Object -Property ErrorMsg -NoElement | ft) Write-Host "" Write-Host "====================================================" Write-Host " Move SUMMARY " -BackgroundColor DarkMagenta Write-Host "Moved Successfully:`t$($MoveUserList.Count - $global:RetryUsers.Count)" #-ForegroundColor Green Write-Host "Failed to move: `t$($global:RetryUsers.Count)" -ForegroundColor Red Write-Host "Time taken: `t`t$($EndTime-$StartTime)" #-BackgroundColor Magenta Write-Host "====================================================" Write-Host "" Write-Host "Encounted errors:" -ForegroundColor Red $global:RetryUsers | Group-Object -Property ErrorMsg -NoElement | ft -AutoSize Write-host "Log file:`t`t`t $MoveResultsLog" Write-Host " " -BackgroundColor DarkMagenta Write-Host "====================================================" Write-Host "" "$EndTime,MoveSummary,Moved Successfully: $($MoveUserList.Count - $global:RetryUsers.Count),Failed to move: $($global:RetryUsers.Count),Time taken: $($EndTime-$StartTime)" | Out-File $MoveResultsLog -Append } if ($SkipAllPrerequisiteChecks) { #Process ALL users from input csv BatchMoveUsers $inputuserslist } else { #Check pre-reqs and store users that met them in $UsersWithPrereqsMet CheckPrerequisites $InputUsersList #Process users that met the pre-reqs ($UsersWithPrereqsMet) BatchMoveUsers $UsersWithPrereqsMet } #Retry users that failed to move ($RetryCycles x times) for ($i = 1; $i -le $RetryCycles; $i++) { #Start-Sleep 2400 Write-Host "########### Retrying users that failed to move (Total: $($global:RetryUsers.Count)). Retry Attempt#: $i)" -ForegroundColor Yellow -BackgroundColor Blue BatchMoveUsers $global:RetryUsers } |