Office 365 – Migrating Users Tenant-to-Tenant – Part Two Identifying Users and Objects

By | July 22, 2020

As discussed in Part One this project is to migrate 8k users between two office tenant. The source tenant contains 60k users and associated groups so scoping requires identifying the users, groups, lists, teams, etc that the users for the domains we are migrating own or access. I have shared my approach and various scripts below. These are used to generate reports for the client and to import the non-migrated objects back in to the target tenant.

Part One – Introduction and Approach
Part Two – Identifying Users and Objects
Part Three – Setup of New Tenant
Part Four – Data Migration with Quest-On-Demand
Part Five – Domain Migration
Part Six – Destination Tenant User Setup

All the scripts for this project are available at https://github.com/jmattmacd/O365-Tenant_to_tenant

The Objects I needed to identify and report on were:

  • Users (Including Mailbox info)
  • Groups
  • Shared Mailboxes
  • Distribution Lists
  • Resource Mailboxes
  • Intune Devices
  • PSTN Routing

These use the Azure AD1, ExchangeOnlineV22 and Teams3 modules, both managed with my Powershell ISE profile. The scripts will output all of the objects and required properties to csv, producing a handy bunch of csvs for reporting to clients/project managers/etc

Users

First off I needed to identify the users in scope and the amount of User Data we’d be shifting in mailboxes and onedrive.

I opted to do this as 3 separate scripts rather than one long one, that way I could use the output from the users script to rerun the the others at will without needing to re-iterate through every user each time.

First we generate a list of all scoped users, this is slightly complicated by us needing the proxy-addresses which being an array we have to iterate through and output without commas: [01-users.ps1]
($DomainName1 and $DomainName2 require setting)

# Connect-MSOLService
$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"
$Outfile = ".\01-output-inscope_users.csv"

$content = "userprincipalname, GivenName, Surname, DisplayName, mailNickName, proxyaddresses"
Add-Content -path $outfile $content

$Scopedusers = Get-MSOLUser -DomainName $DomainName1.Replace("*@","") 
$Scopedusers += Get-MSOLUser -DomainName $DomainName2.Replace("*@","") 

ForEach ($User in $ScopedUsers) {
    $ProxyString = ""
    ForEach ($address in $User.ProxyAddresses) {
        If ($address -like $DomainName1 -or $address -like $DomainName2) {
            $ProxyString = $ProxyString+";"+$address
            
        }
    }
    Write-Host $user.UserPrincipalName -foregroundcolor Red
    Write-Host $ProxyString.TrimStart(";")
    $content = $user.UserPrincipalName+","+$user.FirstName+","+$user.LastName+","+$user.DisplayName+","+($user.UserPrincipalName.Replace($DomainName1.Replace("*",""), "")).Replace($DomainName2.Replace("*",""), "")+","+$ProxyString.TrimStart(";")
    Add-Content -path $Outfile $content
}


Write-Host "Found" $Scopedusers.Count "users"

This generates the .\01-output-inscope_users.csv file, a simplelist of user UPNs and some related attributes data that are in scope. We’re gonna use it a lot.

Next we want to get the mailbox sizes for our users by reading this file back in and grabbing the info we want. [02-user_mailboxes.ps1]

#  connect-exchangeonline

$Outfile = ".\02-output-user-mailboxes.csv"
if (Test-Path $Outfile) 
{
  Remove-Item $Outfile
}
$content = "UserPrincipalName,Archive,ItemCount,TotalItemSize"
Add-Content -Path $OutFile $content

$scopedusers = import-csv -Path ".\01-output-inscope_users.csv"

$i = 0
ForEach ($user in $scopedusers){
    $i = $i+1
    Write-Host "Procesing User" $i "of" $scopedusers.Count -ForegroundColor Yellow
    Write-Host $user.UserPrincipalName -ForegroundColor Yellow
    $ThisMBX = Get-Mailbox $user.UserPrincipalName
    $ThisMBXStats = Get-MailboxStatistics $user.UserPrincipalName
    If ($ThisMBX) {
        $content = $ThisMBX.UserPrincipalName+","+$ThisMBX.ArchiveStatus+","+$ThisMBXStats.ItemCOunt+","+$ThisMBXStats.TotalItemSize
        $content
        Add-Content -Path $OutFile $content
    }
}

This outputs to .\02-output-user_mailboxes.csv which I used to work out the total data size for migration, the licensing requirements on the target tenant and report back to the client.

For my report back to the client and PM I also reported OneDrive usage for the users. I just used the GUI report in the office admin portal for this as it comes dangerously close to SharePoint work and I knew I’d be using Quest to migrate OneDrive directly anyway. You could script this and I may look at it and add an extra script in if I ever get bored.

Groups

The mechanism for outputting groups in scope is to increment through every group and every member to find any who have a UPN matching our in scope domain names: [03-groups_and_members.ps1]
($DomainName1 and $DomainName2 require setting)

# connect-exchangeonline

$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"

$OutfileMembers = ".\06-output-distribution_group_members.CSV"
$OutfileGroups = ".\06-output-distribution_groups.CSV"

if (Test-Path $OutfileMembers) 
{
  Remove-Item $OutfileMembers
}
$content = "DistroGroupName,Member"
Add-Content -Path $OutfileMembers $content

if (Test-Path $OutfileGroups) 
{
  Remove-Item $OutfileGroups
}
$content = "DistroGroupName,AcceptMessagesOnlyFrom,AcceptMessagesOnlyFromDLMembers,AcceptMessagesOnlyFromSendersOrMembers,AddressListMembership,Alias,BypassModerationFromSendersOrMembers,Description,EmailAddresses,EmailAddressPolicyEnabled,GrantSendOnBehalfTo,GroupType,HiddenFromAddressListsEnabled,Identity,IsDirSynced,ManagedBy,MemberJoinRestriction,ModeratedBy,ModerationEnabled,Name,PrimarySmtpAddress"
Add-Content -Path $OutfileGroups $content

$distrogroups=Get-DistributionGroup -ResultSize unlimited

$i=0
ForEach ($distrogroup in $distrogroups){
    $i = $i+1
    Write-Host "Processing Group" $i "of" $distrogroups.Count $distrogroup.displayname -ForegroundColor Yellow
    $DistroMembers = Get-DistributionGroupMember -Identity $distrogroup.PrimarySmtpAddress -ResultSize unlimited
    ForEach ($member in $DistroMembers) {
        If ($member.PrimarySMTPAddress -like $DomainName1 -or $member.PrimarySMTPAddress -like $DomainName2) {
            If ($MatchedGroup -ne $distrogroup) {
                Write-Host "New group" -foregroundcolor Red
                $managedby = ""
                ForEach ($person in $distrogroup.ManagedBy) {
                    $managedby = $managedby+";"+$person
                }
                $moderationbypass = ""
                ForEach ($person in $distrogroup.BypassModerationFromSendersOrMembers) {
                    $person
                    $moderationbypass = $moderationbypass+";"+$person
                }
                $moderatedby = ""
                ForEach ($person in $distrogroup.ModeratedBy) {
                    $person
                    $moderatedby = $moderatedby+";"+$person
                }
                $acceptmessagesfrom = ""
                ForEach ($person in $distrogroup.AcceptMessagesOnlyFrom) {
                    $person
                    $acceptmessagesfrom = $acceptmessagesfrom+";"+$person
                }
                $AcceptMessagesOnlyFromSendersOrMembers = ""
                ForEach ($person in $distrogroup.AcceptMessagesOnlyFromSendersOrMembers) {
                    $person
                    $AcceptMessagesOnlyFromSendersOrMembers = $AcceptMessagesOnlyFromSendersOrMembers+";"+$person
                }
                $content = $distrogroup.DisplayName+","+$acceptmessagesfrom.TrimStart(";")+","+$distrogroup.AcceptMessagesOnlyFromDLMembers+","+$AcceptMessagesOnlyFromSendersOrMembers.TrimStart(";")+","+$distrogroup.AddressListMembership+","`
                    +$distrogroup.Alias+","+$moderationbypass.TrimStart(";")+","+$distrogroup.Description+","+$distrogroup.EmailAddresses+","+$distrogroup.EmailAddressPolicyEnabled+","+$distrogroup.GrantSendOnBehalfTo+","`
                    +$distrogroup.GroupType.Replace(","," ")+","+$distrogroup.HiddenFromAddressListsEnabled+","+$distrogroup.Identity+","+$distrogroup.IsDirSynced+","+$managedby.TrimStart(";")+","+$distrogroup.MemberJoinRestriction+","+$moderatedby.TrimStart(";")+","`
                    +$distrogroup.ModerationEnabled+","+$distrogroup.Name+","+$distrogroup.PrimarySmtpAddress
                Add-Content -Path $OutFileGroups $content
            }          
            $MatchedGroup = $distrogroup
            write-host $member.PrimarySMTPAddress
            $content = $distrogroup.displayname+","+$member.PrimarySmtpAddress
            Add-Content -Path $OutFileMembers $content
        }
    }
} 

This outputs .\03-output-groups_and_members.csv which lists every user-group pair. You can just use excel and remove duplicates on the group column if you want to report on how many groups are in scope.

Teams

Teams are included on the group report but for the migration we want to identify them separately. We do this by ingesting the group report above. This script uses the teams module, if you live a good life there is hope this is the only time: [04-teams.csv]

#Connect-MicrosoftTeams

$Outfile = ".\04-output-teams.csv"
if (Test-Path $Outfile) 
{
  Remove-Item $Outfile
}

$groupmembers = import-csv .\03-output-groups_and_members.csv

$groups = $groupmembers.GroupName | get-unique | Sort-Object

$i = 0
ForEach ($group in $groups) {
$i = $i+1
    Write-host $i "of" $groups.Count $group
    $team = Get-Team -DisplayName $group
    If ($team) {
        write-host "Found One!" $team.DisplayName -ForegroundColor Red
        $team | Export-Csv -Path $Outfile -Append

    }
}

This will output a simple csv with a list of team names and configuration elements.

Shared Mailboxes

The next step was to locate all of the shared mailboxes. Again due to the lack of a consistent naming convention (or actually in this case too many naming conventions) these could really only be identified by checking who has permission to them. Again I used an iterative script to grab every shared mailbox and interrogate the permissions: [05-shared_mailboxes.ps1]
($DomainName1 and $DomainName2 require setting)

# connect-exchangeonline

$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"
$Outfile = ".\05-output-shared_mailboxes.csv"
if (Test-Path $Outfile) 
{
  Remove-Item $Outfile
}
$content = "SharedMailboxName,SharedMailboxAddress,User"
Add-Content -Path $OutFile $content


$AllSMBs = get-mailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited

$i=0
ForEach ($SMB in $AllSMBs){
    $i = $i+1
    Write-Host "Processing SMB" $i "of" $AllSMBs.Count -ForegroundColor Yellow
    Write-Host $SMB.Name -ForegroundColor Yellow
    $SMBMembers = Get-MailboxPermission -Identity $SMB.Alias
    $SMBStats = Get-MailboxStatistics -Identity $SMB.UserPrincipalName
    ForEach ($SMBMember in $SMBMembers) {
        If ($SMBMember.User -like $DomainName1 -or $SMBMember.User -like $DomainName2) {
        $SMB.Name 
        $SMBMember.User
        $content = $SMB.Name+","+$SMB.PrimarySmtpAddress+","+$SMBMember.User+","+$SMBStats.ItemCount+","+$SMBStats.TotalItemSize
        Add-Content -Path $OutFile $content
        }
    }
    
}

This outputs a file showing every member-shared mailbox pair, it also checks the shared mailbox size and item count to assist with scoping. Again a bit of Excel-Fu and you can get the various unique values for reporting.

Distribution Groups

Distribution groups are much like Groups and Mailboxes, I incremented them through to identify any who had members in scope and output. However Quest cannot migrate distribution groups directly (there’s no content anyway) So I had to grab bunch of configuration info for the lists too: [06-distribution_groups.ps1]
($DomainName1 and $DomainName2 require setting)

# connect-exchangeonline

$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"

$OutfileMembers = ".\06-output-distribution_group_members.CSV"
$OutfileGroups = ".\06-output-distribution_groups.CSV"

if (Test-Path $OutfileMembers) 
{
  Remove-Item $OutfileMembers
}
$content = "DistroGroupName,Member"
Add-Content -Path $OutfileMembers $content

if (Test-Path $OutfileGroups) 
{
  Remove-Item $OutfileGroups
}
$content = "DistroGroupName,AcceptMessagesOnlyFrom,AcceptMessagesOnlyFromDLMembers,AcceptMessagesOnlyFromSendersOrMembers,AddressListMembership,Alias,BypassModerationFromSendersOrMembers,Description,EmailAddresses,EmailAddressPolicyEnabled,GrantSendOnBehalfTo,GroupType,HiddenFromAddressListsEnabled,Identity,IsDirSynced,ManagedBy,MemberJoinRestriction,ModeratedBy,ModerationEnabled,Name,PrimarySmtpAddress"
Add-Content -Path $OutfileGroups $content


$distrogroups=Get-DistributionGroup -ResultSize unlimited

$i=0
ForEach ($distrogroup in $distrogroups){
    $i = $i+1
    Write-Host "Processing Group" $i "of" $distrogroups.Count $distrogroup.displayname -ForegroundColor Yellow
    $DistroMembers = Get-DistributionGroupMember -Identity $distrogroup.PrimarySmtpAddress -ResultSize unlimited
    ForEach ($member in $DistroMembers) {
        If ($member.PrimarySMTPAddress -like $DomainName1 -or $member.PrimarySMTPAddress -like $DomainName2) {
            If ($MatchedGroup -ne $distrogroup) {
                Write-Host "New group" -foregroundcolor Red
                $content = $distrogroup.DisplayName+","+$distrogroup.AcceptMessagesOnlyFrom+","+$distrogroup.AcceptMessagesOnlyFromDLMembers+","+$distrogroup.AcceptMessagesOnlyFromSendersOrMembers+","+$distrogroup.AddressListMembership+","`
                    +$distrogroup.Alias+","+$distrogroup.BypassModerationFromSendersOrMembers+","+$distrogroup.Description+","+$distrogroup.EmailAddresses+","+$distrogroup.EmailAddressPolicyEnabled+","+$distrogroup.GrantSendOnBehalfTo+","`
                    +$distrogroup.GroupType+","+$distrogroup.HiddenFromAddressListsEnabled+","+$distrogroup.Identity+","+$distrogroup.IsDirSynced+","+$distrogroup.ManagedBy+","+$distrogroup.MemberJoinRestriction+","+$distrogroup.ModeratedBy+","`
                    +$distrogroup.ModerationEnabled+","+$distrogroup.Name+","+$distrogroup.PrimarySmtpAddress
                Add-Content -Path $OutFileGroups $content
            }          
            $MatchedGroup = $distrogroup
            write-host $member.PrimarySMTPAddress
            $content = $distrogroup.displayname+","+$member.PrimarySmtpAddress
            Add-Content -Path $OutFileMembers $content
        }
    }
} 

This outputs two files, one with the groups and one with the groups and members. The reason will become clear when we come to rebuild these on the target tenant.

Resource Mailboxes

The room mailboxes all had the primary SMTP addresses matching one of the scoped domains, so this script is very simple: [07-resource_mailboxes.ps1]
($DomainName1 and $DomainName2 require setting)

# connect-exchangeonline

$DomainName1 = "*@thisdomain.com"
$DomainName2 = "*@thatdomain.com"

Get-Mailbox -ResultSize unlimited -Filter {(RecipientTypeDetails -eq 'RoomMailbox')} | where {$_.PrimarySMTPAddress -like $DomainName1 -or $_.PrimarySMTPAddress -like $DomainName2} | select PrimarySMTPAddress | Export-Csv -Path ".\07-output-resource_mailboxes.CSV"

This outputs a single column csv file of room mailboxes to migrate in quest

Intune Devices, PSTN Routing, Licensing

All of these were outside the scope of this project – The users of the domains in question didn’t use any PSTN capabilities in 365, and only had a few test devices in InTune which I reported on so they could be reset. Should I ever be involved in migrating these I will write it up and link from this post.

For licensing I ran the below script, it users the users output from above and just grabs the current licenses, this was only for completeness and reporting, it is not functionally necessary [08-licensing_report.ps1]

# connect-azuread

$Outfile = ".\08-output-licensing_report.CSV"
if (Test-Path $Outfile) 
{
  Remove-Item $Outfile
}
$content = "UserPrincipalName,License"
Add-Content -Path $OutFile $content


$scopedusers = import-csv -Path .\01-output-inscope_users.csv
$OurSkus = Get-AzureADSubscribedSku
$i = 0

ForEach ($scopeduser in $scopedusers){
    $i = $i+1
    Write-Host "Processing User" $i "of" $scopedusers.Count -ForegroundColor Yellow
    $ThisUser = Get-AzureADUser -ObjectId $scopeduser.UserPrincipalName
    Write-Host $scopeduser.UserPrincipalName -ForegroundColor Yellow
    $userskus = $ThisUser | select UserPrincipalName -ExpandProperty AssignedLicenses
        ForEach ($usersku in $userskus) {
        write-host $usersku.UserPrincipalName $usersku.SkuID
        $ThisSku = $OurSkus | where {$_.SkuId -eq $usersku.SkuId} |select SkuPartNumber
        $content = $ThisUser.UserPrincipalName+","+$ThisSku
        Add-Content -Path $OutFile $content
    }
}

This outputs the file “.\08-output-licensing_report.CSV” which simply lists each user-sku part number pair

Links

1 – Microsoft Azure AD Module – https://docs.microsoft.com/en-us/powershell/module/azuread/?view=azureadps-2.0
2 – Microsoft Exchange Online V2 Module –https://docs.microsoft.com/en-us/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps
3 – Microsoft Teams Module – https://docs.microsoft.com/en-us/microsoftteams/teams-powershell-overview

Loading

3 thoughts on “Office 365 – Migrating Users Tenant-to-Tenant – Part Two Identifying Users and Objects

    1. jmattmacd Post author

      When I wrote these scripts i was writing them to migrate a subset of users who had one of two different domain names. The tenant contained users with around 30 different custom domain names. $DomainName1 and $DomainName2 are variables you can use to reference those domains and find the users you want to migrate, if there’s only one domain name just set $DomainName2 to something that doesn’t exist on your tenant.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *