Migrating TFS to a new data center
Applies to: TFS 2013 | TFS 2015 | TFS 2017
This article outlines an approach for performing TFS migrations to a new data center (in a different domain) where there is limited or no connectivity between the current and target environments. In this context, migrating the TFS deployment includes restoring backups of the TFS (config & collections), SharePoint (content), and SSRS (reports) databases in the new domain environment. If you are faced with this scenario, read on. Even if this does not match your scenario, some parts of the information covered here may still be useful when dealing with large numbers of changes to existing TFS, SharePoint, and SSRS user identities and permissions.
Background
There are two documented types of moves you can perform on Team Foundation Server: hardware and environment. In a hardware move, you are moving (or cloning) an existing TFS implementation to a new set of servers in the same domain. In an environment move, you are changing the domain of the TFS deployment. Documentation for both types of moves is available here:
- Move or Clone Team Foundation Server from one hardware to another
- Move Team Foundation Server from one environment to another
Both of these articles have the following important note:
"In some situations you might want to change the domain of a TFS deployment as well as its hardware. Changing the domain is an environment-based move, and you should never combine the two move types. First complete the hardware move, and then change the environment."
But what if your migration is to a new data center or servers located in a different domain, with restricted or no connectivity to the original domain? This scenario can occur when changing data centers, for example during mergers of separate organizations or transfer of responsibility for services. With limited or no connectivity between environments, it is not possible to join the existing TFS deployment to the new domain. In this case, the hardware move is the closer match for what we need to achieve.
Following the instructions in the hardware move documentation gets us though most of the migration process. After the hardware move has been completed we will still need to remap user identities/permissions for TFS, SharePoint, and SSRS. A common element to each of these three areas is the mapping of your user identities from the old domain into the new domain. We'll look at this requirement in the User Mapping File section below.
One thing you should pay special attention to during the process is ensuring that any TFS user identities in the new domain that you will be mapping TFS users from the old domain into are not part of the Local Administrators
group. This is important because the TFS identity synchronization service will automatically create TFS user identities for members of this group. Once a TFS user identity has been created in the new domain, it is not possible to map a TFS user identity from the old domain into it.
User Mapping File
To support the user identity and permission changes needed in the three identified areas, we will create a common user mapping text file. In this schema, OldDomain
represents the name of the original domain, OldAccount
represents the user account names in the original domain, NewDomain
represents the name of the target domain, and NewAccount
represents the user account names in the target domain.
The following illustrates a user mapping file for a sample scenario where TFSDEV
is the original domain and TFSPRD
is the target domain:
OldDomain,OldAccount,NewDomain,NewAccount
TFSDEV,Developer1,TFSPRD,Developer1
TFSDEV,Developer3,TFSPRD,Developer2
The first line of this file contains the attribute names, which must match exactly for the PowerShell scripts provided later to operate correctly. In this sample file, we are mapping the Developer1
account in the TFSDEV
domain to the same account name in the TFSPRD
domain. The Developer3
user account in the TFSDEV
domain is mapped to Developer2
in the TFSPRD
domain.
We've chosen the CSV format for the user mapping file in this example as it is relatively easy to manage with a wide range of application tools and utilities. However, you could also use other formats, with the appropriate changes to the sample scripts. For example, JSON and XML are two possible alternatives.
Once the user mapping file has been created, we can proceed with the changes detailed in the following three sections:
TFS Identities
Remapping TFS identities is accomplished using the TfsConfig Identities /change command. The TfsConfig.exe utility is installed with Team Foundation Server, under the \Tools
folder of the installation path. If all (or most) of your account names match between the two domain environments, then you can run the command directly, omitting the /account
and /toaccount
parameters. Often times however, you will need to map many user accounts that have different names in each domain.
tfs-change-identities.ps1
The following PowerShell script illustrates how you can handle the more complex account mapping scenario, using the user mapping file created in the previous section. The $TfsConfig
parameter in this sample script points to the location of the TfsConfig.exe
application in a default installation of TFS 2015. If its location is different in your environment, you will need to either change the default value in the script, or provide the location as a parameter value when invoking the script.
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, Position=0)]
[string] $UserMapFile,
[Parameter(Position=1)]
[string] $TfsConfig = "C:\Program Files\Microsoft Team Foundation Server 14.0\Tools\TfsConfig.exe"
)
if (-Not (Test-Path $TfsConfig))
{
Write-Error "TfsConfig.exe file does not exist in the specified location"
return
}
if (-Not (Test-Path $UserMapFile))
{
Write-Error "User map file does not exist in the specified location"
return
}
$usermap = Import-Csv $UserMapFile
foreach ($user in $usermap)
{
Write-Host "Changing $($user.OldDomain)\$($user.OldAccount)" -NoNewline
Write-Host " to $($user.NewDomain)\$($user.NewAccount)"
& $TfsConfig Identities /change `
/fromdomain:$($user.OldDomain) `
/todomain:$($user.NewDomain) `
/account:$($user.OldAccount) `
/toaccount:$($user.NewAccount)
}
Running this script with the user mapping file created in the previous section displays the following output:
PS C:\> .\tfs-change-identities.ps1 c:\docs\usermap.csv
Changing TFSDEV\Developer1 to TFSPRD\Developer1
Account Name Exists (see note 1) Matches (see note 2)
TFSPRD\Developer1 True False Changed
1 security identifier(s) (SIDs) were changed in Team Foundation Server.
== NOTES ==
(1) The Exists column indicates whether the listed account exists in Windows. For the List mode of the command, this is the account stored in Team Foundation Server. For the Change mode, it is the target of the change.
(2) The Matches column indicates whether the SID stored in Team Foundation Server matches with Windows.
The script executes the TfsConfig Identities /change
command for each entry in the user mapping file, and will display the Changed
value on success. Changed TFS identities will not match the Windows user identities until the next identity synchronization has occurred - by default, this happens at the top of every hour. It is possible to force an identity synchronization to happen immediately by invoking a web service call. Stay tuned for a future post describing that process.
SharePoint Identities
After moving your SharePoint content database for TFS to a new domain, the SharePoint user permissions still reference user identities in the original domain environment.
sp-change-identities.ps1
It is relatively easy to remap the SharePoint user identities by using the Move-SPUser
PowerShell cmdlet. The following PowerShell script illustrates how you can use the previously created User Mapping File to remap the SharePoint user identities into the new domain environment.
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, Position=0)]
[string] $WebAppUrl,
[Parameter(Mandatory=$true, Position=1)]
[string] $UserMapFile
)
$ErrorActionPreference = "Stop"
Add-PSSnapIn "Microsoft.SharePoint.Powershell"
$moveErrors = $null
$usermap = Import-Csv $UserMapFile
$users = Get-SPWebApplication $WebAppUrl | Get-SPSite | Get-SPWeb | Get-SPUser
foreach ($user in $users)
{
$parts = $user.UserLogin.Split('\')
if ($parts.Count -ne 2) { continue }
$domain = $parts[0]
$account = $parts[1]
foreach ($entry in $usermap)
{
if ($domain -eq $entry.OldDomain -and $account -eq $entry.OldAccount)
{
$oldAlias = $domain + "\" + $account
$newAlias = $entry.NewDomain + "\" + $entry.NewAccount
Write-Host "Changing SharePoint user from [" -ForegroundColor Yellow -NoNewline
Write-Host $oldAlias "] to [" $newAlias "]" -ForegroundColor Yellow
Move-SPUser -Identity $user -NewAlias $newAlias -IgnoreSID -ErrorAction SilentlyContinue
}
}
}
Run the above script, supplying the URL of the SharePoint web application associated with the TFS SharePoint site collections, and location of the user mapping file. Typically you will run this on the SharePoint server in the new environment, which should already have the Microsoft.SharePoint.Powershell
module present.
Running this script produces the following sample output:
PS C:\> .\sp-change-identities.ps1 https://tfs-prd-at/ .\usermap.csv
Changing SharePoint user from [TFSDEV\Developer1] to [TFSPRD\Developer1]
Changing SharePoint user from [TFSDEV\Developer3] to [TFSPRD\Developer2]
After remapping your SharePoint user identities, login with a user in the new domain and navigate to a SharePoint site associated with one or more of your TFS team projects to validate that the SharePoint user was remapped successfully.
SSRS Permissions
The last area we'll address is SQL Server Reporting Services (SSRS) permissions for your TFS team project reports. Managing SSRS user permissions (roles) can be done through the Report Manager web interface as documented in Grant user access to a report server, or via the command-line with the RS.exe utility.
This section describes a PowerShell-based solution to simplify the replication of hundreds (or thousands) of distinct SSRS user permissions between different domain environments. We will once again leverage the User Mapping File already created, combined with two PowerShell scripts to: 1) retrieve existing user permissions in the original domain environment, and 2) reapply those permissions in the new domain environment.
ssrs-list-permissions.ps1
The first step is to generate a textual representation of user permissions in the original domain environment. It takes two parameters: the SSRS report server starting path, and an SSRS instance name if this is not a default SSRS instance. The script should be run on the SSRS server in the original domain environment. It can be run from elsewhere by changing the $reportServerUri
variable to point to the SSRS Web Service URL (available in Reporting Services Configuration Manager).
<#
PowerShell script to iterate over non-inherited SSRS permissions.
1. Change the $sourceFolderPath value if you want to start at a lower
level than root of SSRS.
2. Change the $InstanceName if this is not the SSRS default instance
#>
[CmdletBinding()]
param (
[Parameter(Position=0)]
[string] $sourceFolderPath = "/",
[Parameter(Position=1)]
[string] $InstanceName = ""
)
if ($InstanceName -eq "")
{
$reportServerUri = "https://localhost/ReportServer/ReportService2010.asmx"
}
else
{
$reportServerUri = "https://localhost/ReportServer_" + $InstanceName + "/ReportService2010.asmx"
}
$scriptDirectory = Split-Path $MyInvocation.MyCommand.Path
$outfile = $scriptDirectory + "\ssrs-list-permissions.csv"
Write-Host "SSRS web service: " $reportServerUri -ForegroundColor Yellow
Write-Host "Working directory: " $scriptDirectory -ForegroundColor Yellow
Write-Host "Computer name: " $env:COMPUTERNAME -ForegroundColor Yellow
Write-Host "Logged in as: " $env:USERNAME -ForegroundColor Yellow
$inheritParent = $true
$output = @()
$proxy = New-WebServiceProxy -UseDefaultCredential -Uri $reportServerUri
$items = $proxy.ListChildren($sourceFolderPath, $true) `
| Select-Object TypeName, Path, Name
foreach ($item in $items)
{
Add-Member -InputObject $item -MemberType NoteProperty -Name RoleName -Value ''
Add-Member -InputObject $item -MemberType NoteProperty -Name FullyQualifiedUserName -Value ''
Add-Member -InputObject $item -MemberType NoteProperty -Name Domain -Value ''
Add-Member -InputObject $item -MemberType NoteProperty -Name UserName -Value ''
$needHeader = $true
foreach ($policy in $proxy.GetPolicies($item.path, [ref]$inheritParent))
{
if ($inheritParent) { continue }
if ($needHeader)
{
Write-Host
Write-Host "Type: " $item.TypeName -ForegroundColor Yellow
Write-Host "Path: " $item.Path -ForegroundColor Yellow
Write-Host "Name: " $item.Name -ForegroundColor Yellow
Write-Host "--------------------------------------------------"
$needHeader = $false
}
Write-Host $policy.GroupUserName -NoNewline
Write-Host " (" -NoNewline
$needComma = $false
foreach ($role in $policy.Roles)
{
if ($needComma) { Write-Host ", " -NoNewline } else { $needComma = $true }
Write-Host $role.Name -ForegroundColor Green -NoNewline
$temp = $item.PsObject.Copy();
$temp.RoleName = $role.Name
$temp.FullyQualifiedUserName = $policy.GroupUserName;
$temp.Domain = $policy.GroupUserName.Split('\')[0]
$temp.UserName = $policy.GroupUserName.Split('\')[1]
$output += $temp;
$temp.reset;
}
Write-Host ")"
}
}
$output | Export-csv $outfile -NoTypeInformation;
Write-Host
Write-Host "Completed with output stored in: " $outfile
Write-Host
This script creates an output file named ssrs-list-permissions.csv
in the same directory where the script resides. Remember the location and name of this output file as it will be needed in the next part of the process.
Sample console output from running the ssrs-list-permissions.ps1
script looks like this:
PS C:\> .\ssrs-list-permissions.ps1
Type: Folder
Path: /TfsReports/DefaultCollection/Agile
Name: Agile
TFSDEV\Developer1 (Content Manager, Publisher)
TFSDEV\Developer3 (Browser)
Completed with output stored in: ssrs-list-permissions.csv
The permissions output file has the following format:
TypeName,Path,Name,RoleName,FullyQualifiedUserName,Domain,UserName
Folder,/TfsReports/DefaultCollection/Agile,Agile,Content Manager,TFSDEV\Developer1,TFSDEV,Developer1
Folder,/TfsReports/DefaultCollection/Agile,Agile,Publisher,TFSDEV\Developer1,TFSDEV,Developer1
Folder,/TfsReports/DefaultCollection/Agile,Agile,Browser,TFSDEV\Developer3,TFSDEV,Developer3
If a user is assigned to multiple roles, each role will be specified on a separate line in the permissions output file generated by the ssrs-list-permissions.ps1
script.
ssrs-change-permissions.ps1
The second step is to use the User Mapping File along with the permissions output file generated in the previous step. Together, these two files are used as input to the script below that is run on the SSRS server in the new domain environment.
This script uses the SSRS web service to update permission policies. For each object/role permission identified in the the permissions output file generated in the original domain, it performs a lookup in the user mapping file to determine user identity in the new domain, and grants that identity the object/role permission.
<#
The first parameter (ssrsPermissionsFile) is the file path to the output
from the 'ssrs-list-permissions.ps1' script run in the old domain.
The second parameter (UserMapFile) is the file path to the user mapping file.
The third parameter (ssrsInstanceName) is the SSRS instance name. If running
on the default SSRS instance, press <Enter> if prompted to supply
an empty string value.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, Position=0)]
[string] $ssrsPermissionsFile,
[Parameter(Mandatory=$true, Position=1)]
[string] $UserMapFile,
[Parameter(Mandatory=$true, Position=2)]
[AllowEmptyString()]
[string] $ssrsInstanceName
)
if ($ssrsInstanceName -eq "")
{
$reportServerUri = "https://localhost/ReportServer/ReportService2010.asmx"
}
else
{
$reportServerUri = "https://localhost/ReportServer_" + $ssrsInstanceName + "/ReportService2010.asmx"
}
if (-Not (Test-Path $ssrsPermissionsFile))
{
Write-Error $ssrsPermissionsFile " file does not exist in the specified location"
return
}
if (-Not (Test-Path $UserMapFile))
{
Write-Error $UserMapFile " file does not exist in the specified location"
return
}
Write-Host "Loading SSRS permissions (old domain) from file: " $ssrsPermissionsFile
$permissions = Import-Csv $ssrsPermissionsFile
Write-Host "Loading user mapping file: " $UserMapFile
$usermap = Import-Csv $UserMapFile
$lookup = @{}
$usermap | % { $key = $_.OldDomain + "\" + $_.OldAccount; $lookup[$key] = $_ }
$inheritParent = $true
Write-Host "Connecting to SSRS web service: " $reportServerUri
$proxy = New-WebServiceProxy -UseDefaultCredential -Uri $reportServerUri
$type = $proxy.GetType().Namespace
$policyType = "{0}.Policy" -f $type
$roleType = "{0}.Role" -f $type
foreach ($permission in $permissions)
{
$key = $permission.FullyQualifiedUserName
$user = $lookup[$key]
if ($user -ne $null)
{
$identity = $user.NewDomain + "\" + $user.NewAccount
$message = "Retrieving policies for user '{0}' on '{1}'" -f $identity, $permission.Path
Write-Host $message -ForegroundColor Yellow
$policies = $proxy.GetPolicies($permission.Path, [ref]$inheritParent);
$policy = $policies |
where { $_.GroupUserName -eq $identity } |
select -First 1
if (-not $policy)
{
$policy = New-Object ($policyType)
$policy.GroupUserName = $identity
$policy.Roles = @()
$policies += $policy
$message = "Creating policy for user: '{0}'" -f $identity
Write-Host $message -ForegroundColor Cyan
}
$role = $policy.Roles |
where { $_.Name -eq $permission.RoleName } |
select -First 1
if (-not $role)
{
$role = New-Object ($roleType)
$role.Name = $permission.RoleName
$policy.Roles += $role
$message = "Adding user '{0}' to role '{1}'" -f $identity, $permission.RoleName
Write-Host $message -ForegroundColor Cyan
}
$message = "Saving policies for user '{0}' on '{1}'" -f $identity, $permission.Path
Write-Host $message -ForegroundColor Cyan
$proxy.SetPolicies($permission.Path, $policies);
}
else
{
$message = "'{0}' not found in user mapping file" -f $key
Write-Host $message -ForegroundColor Red
}
}
Sample console output from running the ssrs-change-permissions.ps1
script looks like this:
PS C:\> C:\Scripts\ssrs-change-permissions.ps1 .\ssrs-list-permissions.csv .\usermap.csv ""
Loading SSRS permissions (old domain) from file: ssrs-list-permissions.csv
Loading user mapping file: usermap.csv
Connecting to SSRS web service: https://localhost/ReportServer/ReportService2010.asmx
Retrieving policies for user 'TFSPRD\Developer1' on '/TfsReports/DefaultCollection/Agile'
Creating policy for user: 'TFSPRD\Developer1'
Adding user 'TFSPRD\Developer1' to role 'Content Manager'
Saving policies for user 'TFSPRD\Developer1' on '/TfsReports/DefaultCollection/Agile'
Adding user 'TFSPRD\Developer1' to role 'Publisher'
Saving policies for user 'TFSPRD\Developer1' on '/TfsReports/DefaultCollection/Agile'
Retrieving policies for user 'TFSPRD\Developer2' on '/TfsReports/DefaultCollection/Agile'
Creating policy for user: 'TFSPRD\Developer2'
Adding user 'TFSPRD\Developer2' to role 'Browser'
Saving policies for user 'TFSPRD\Developer2' on '/TfsReports/DefaultCollection/Agile'
Conclusion
We've demonstrated how you can leverage PowerShell scripting to simplify the process of managing user identities in a specific migration scenario for an enterprise-scale TFS deployment.