FIM 2010: Configuration Deployment (Part 1: Schema)

My main customer has an environment with 3 stages (Development, Test, Production), so very perfect to work with. But deployment from one stage to the other is not very neat with the default tools that are shipped with Forefront Identity Manager 2010, as (you all know it) there are only a few PowerShell scripts you deal with.

I’ve searched a lot of blogs, also TechNet forum and wiki, but it seems nobody has ever wrote an article about this part of work on a FIM solution (or possibly I’m unable to find 😉 ).

So for the schema deployment you have to export schema with PowerShell on both (source and target) stages, then copy the file together and use another PowerShell script to join them and calculate the delta. Finally import the delta with another script.

As I have to deal with a multi-language environment, the –AllLocales parameter slows up the things, in addition I want to get rid of jumping between the RDP sessions to do all this.

So I modified the default scripts and put them all together in one and use PowerShell jobs to get the export from both environments in parallel (using jobs can be a little bit tricky). After that the delta is calculated and you are asked if you want to import the changes into FIM.

So here is the script I use to deploy my schema changes from one environment to the other:
(This script is currently desinged to run from the target system)

param([switch]$AllLocales)
# use -AllLocales parameter to deploy schema data incl. localizations

#### Configuration Section ####
$sourceFilename="D:\Deploy\Schema_Dev.xml"
$sourceServer="fim.dev.domain.com"
$destFilename="D:\Deploy\Schema_Prod.xml"
$destServer="fim.prod.domain.com"

$deployFilename="D:\Deploy\Schema_DeployData.xml"
$undoneFile = "D:\Deploy\undone.xml"

#### Cleanup old data files ####
remove-item $sourceFilename -ErrorAction:SilentlyContinue
remove-item $destFilename -ErrorAction:SilentlyContinue
remove-item $deployFilename -ErrorAction:SilentlyContinue
remove-item $undoneFile -ErrorAction:SilentlyContinue

#### Define Functions ####
function CommitPortalChanges($deployFile)
{
	$imports = ConvertTo-FIMResource -file $deployFile
	if($imports -eq $null)
	  {
		throw (new-object NullReferenceException -ArgumentList "Changes is null.  Check that the changes file has data.")
	  }
	Write-Host "Importing changes into T&A environment"
	$undoneImports = $imports | Import-FIMConfig
	if($undoneImports -eq $null)
	  {
		Write-Host "Import complete."
	  }
	else
	  {
		Write-Host
		Write-Host "There were " $undoneImports.Count " uncompleted imports."
		$undoneImports | ConvertFrom-FIMResource -file $undoneFile
		Write-Host
		Write-Host "Please see the documentation on how to resolve the issues."
	  }
}

function SyncSchema($sourceFile, $destFile, $deployFile)
{
	$joinrules = @{
		# === Schema configuration ===
		# This is based on the system names of attributes and objects
		# Notice that BindingDescription is joined using its reference attributes.
		ObjectTypeDescription = "Name";
		AttributeTypeDescription = "Name";
		BindingDescription = "BoundObjectType BoundAttributeType";
	}

	if(@(get-pssnapin | where-object {$_.Name -eq "FIMAutomation"} ).count -eq 0) {add-pssnapin FIMAutomation}

	$destination = ConvertTo-FIMResource -file $destFile
	if($destination -eq $null)
		{ throw (new-object NullReferenceException -ArgumentList "Destination Schema is null.  Check that the destination file has data.") }

	Write-Host "Loaded destination file: " $destFile " with "  $destination.Count " objects."

	$source = ConvertTo-FIMResource -file $sourceFile
	if($source -eq $null)
		{ throw (new-object NullReferenceException -ArgumentList "Source Schema is null.  Check that the source file has data.") }

	Write-Host "Loaded source file: " $sourceFile " with " $source.Count " objects."
	Write-Host
	Write-Host "Executing join between source and destination."
	$matches = Join-FIMConfig -source $source -target $destination -join $joinrules -defaultJoin DisplayName
	if($matches -eq $null)
		{ throw (new-object NullReferenceException -ArgumentList "Matches is null.  Check that the join succeeded and join criteria is correct for your environment.") }
	Write-Host "Executing compare between matched objects in source and destination."
	$changes = $matches | Compare-FIMConfig
	if($changes -eq $null)
		{ throw (new-object NullReferenceException -ArgumentList "Changes is null.  Check that no errors occurred while generating changes.") }
	Write-Host
	Write-Host "Identified " $changes.Count " changes to apply to destination."
	Write-Host "Saving changes to " $deployFile "."
	$changes | ConvertFrom-FIMResource -file $deployFile
	Write-Host
	Write-Host "Sync complete. The next step is to commit the changes using CommitChanges.ps1."
}

$functions = {
	function GetSchema($filename, $serverFQDN, $creds, $AllLocales)
	{
		if(@(get-pssnapin | where-object {$_.Name -eq "FIMAutomation"} ).count -eq 0) {add-pssnapin FIMAutomation}
		$uri="http://" + $serverFQDN + ":5725/ResourceManagementService"
		if ($AllLocales -eq $true)
			{ $schema = Export-FIMConfig -uri $uri -credential $creds -allLocales -schemaConfig -customConfig "/SynchronizationFilter" }
		else
			{ $schema = Export-FIMConfig -uri $uri -credential $creds -schemaConfig -customConfig "/SynchronizationFilter" }
		$schema | ConvertFrom-FIMResource -file $filename
	}
}

#### Main Script ####
$creds=Get-Credential -message "Enter credentials for source FIM"
$myargs=@($sourceFileName, $sourceServer, $creds, $AllLocales.IsPresent)
start-job -name "SourceFIM" -init $functions -script { GetSchema $args[0] $args[1] $args[2] $args[3] } -ArgumentList $myargs

$creds=Get-Credential -message "Enter credentials for destination FIM"
$myargs=@($destFileName, $destServer, $creds, $AllLocales.IsPresent)
start-job -name "DestFIM" -init $functions -script { GetSchema $args[0] $args[1] $args[2] $args[3] } -ArgumentList $myargs

Write-Host "Waiting for Schema Export to complete..."
Write-Host
get-job | wait-job

Write-Host "Exports complete: Starting Schema compare..."
SyncSchema $sourceFilename $destFilename $deployFilename

$input=Read-Host "Do you want commit changes to destination FIM ? (y/n)"
if ($input -eq "y")
{
	CommitPortalChanges $deployFilename
}

Due to the implementation of PowerShell jobs, if you get any error or exceptions you can retrieve the output from both background jobs with the following command:

Receive-Job <JobNumber>

Or

Receive-Job –name <JobName>

The script also creates the undone.xml file like the original join script, so you can use the original ResumeUndoneImport.ps1 script to retry importing the unprocessed changes.

So this was the easiest part, next time I will take a look on the deployment of the policies (like Sets, MPRs and Workflows) in which I have to deal with possible different values in the stages (like for e.g. NetBIOS Domain Names or Part of the DN) and also some objects for which I don’t want to have changes deployed, as they have to be different within these stages.

So, come back in a few days or use the RSS feed to don’t miss Part 2: Policy Deployment.