FIM 2010: Configuration Deployment (Part 2: Configuration Objects)

As you hopefully read in Part 1, we often had to deal with deploying configuration from one stage to another. Deploying the schema is the easier part as this mostly goes 1:1 from dev to test and finally production, but I have to deal with environment specific differences in the configuration objects like Sets, Workflows or MPRs.

In synchronization rules for example I often use fixed stings, like domain or constructing DNs for provision objects to LDAP directories and many other attributes. I also have some sets which must be different in the environments as in testing stage some more people have permissions to do something rather than in production, so I want to manage some of these sets manually and don’t want them to be deployed.

So, like in the schema deployment script I put all the example scripts together and also start the exports in parallel using PowerShell jobs. In addition after joining and calculating the delta configuration I do a search and replace of the fixed string values and even delete some objects which I don’t want to deploy. These replacements and deletes are defined in a configuration XML file.

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

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

#### Configuration Section ####
$sourceFilename="D:\Deploy\Policy_Source.xml"
$sourceServer="fim.source.com"
$destFilename="D:\Deploy\Policy_Target.xml"
$destServer="fim.target.com"

$tempDeltaFile = "D:\Deploy\Policy_DeltaTemp.xml"
$finalDeltaFile = "D:\Deploy\Policy_DeployData.xml"
$undoneFile = "D:\Deploy\undone.xml"

$config=[XML](Get-Content .\DeployReplaceConfig.xml)

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

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

#### Function Definitions ####
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 SyncPortalConfig($sourceFile, $destFile, $tempFile)
{
	$joinrules = @{
		# === Customer-dependent join rules ===
		# Person and Group objects are not configuration will not be migrated.
		# However, some configuration objects like Sets may refer to these objects.
		# For this reason, we need to know how to join Person objects between
		# systems so that configuration objects have the same semantic meaning.
		Person = "AccountName";
		Group = "DisplayName";

		# === Policy configuration ===
		# Sets, MPRs, Workflow Definitions, and so on. are best identified by DisplayName
		# DisplayName is set as the default join criteria and applied to all object
		# types not listed here.
		#"ma-data" = "Description";

		# === 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";

		# === Portal configuration ===
		ConstantSpecifier = "BoundObjectType BoundAttributeType ConstantValueKey";
		SearchScopeConfiguration = "DisplayName SearchScopeResultObjectType Order";
		ObjectVisualizationConfiguration = "DisplayName AppliesToCreate AppliesToEdit AppliesToView"
	}

	$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."
	Write-Host
	$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 "Identified " $changes.Count " changes to apply to destination."
	$changes | ConvertFrom-FIMResource -file $tempFile
	Write-Host "Sync complete."
}

$functions = {
	function GetPortalConfig($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)
			{ $policy = Export-FIMConfig -uri $uri -credential $creds -policyConfig -portalConfig -MessageSize 9999999 –AllLocales }
		else
			{ $policy = Export-FIMConfig -uri $uri -credential $creds -policyConfig -portalConfig -MessageSize 9999999 }
		$policy | 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 { GetPortalConfig $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 { GetPortalConfig $args[0] $args[1] $args[2] $args[3] } -ArgumentList $myargs

write-host "Waiting for Policy Export to complete..."
get-job | wait-job

Write-Host "Exports complete: Starting Policy compare..."
Write-Host
SyncPortalConfig $sourceFilename $destFilename $tempDeltaFile

Write-Host "`nReplace configuration data with destination values"
$portalData=(get-content $tempDeltaFile)

foreach ($ReplaceConfig in $config.DeployConfig.ReplaceData)
{
	$newPortalData=@()
	foreach ($CurrentLine in $portalData)
	{
		$newPortalData+=$CurrentLine.replace($ReplaceConfig.SearchString, $ReplaceConfig.ReplaceString)
	}
	$portalData=$newPortalData
}

$portalData | set-content $finalDeltaFile

Write-Host "`nRemoving excluded Objects from deploy data"
$deployXML=[XML](get-content $finalDeltaFile)

foreach ($ReplaceConfig in $config.DeployConfig.IgnoreData)
{
	$deleteObjects=$deployXML.Results.ImportObject | where { $_.TargetObjectIdentifier -eq "urn:uuid:"+$ReplaceConfig.SearchGUID }
	foreach ($delObj in $deleteObjects)
	{
		write-host "Deleting " $delObj.ObjectType ": " $delObj.AnchorPairs.JoinPair.AttributeValue
		try
		{
			$deployXML.Results.RemoveChild($delObj) | out-null
		}
		catch [System.Management.Automation.MethodInvocationException]
		{
			write-host "Ignoring " $delObj.ObjectType ": " $delObj.AnchorPairs.JoinPair.AttributeValue " not in DeploymentData"
		}
	}
}

$deployXML.Save($finalDeltaFile)
Write-Host "`n`nFinal file for deployment created: " $finalDeltaFile

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

This is the configuration XML file: In IgnoreData the Name is only for viewing what object this is, all search is done on the GUID, which is the GUID of the target system.

<?xml version="1.0" encoding="utf-8"?>
<DeployConfig>
	<ReplaceData Name="NetbiosDomain">
		<SearchString>SOURCE-DOM</SearchString>
		<ReplaceString>TARGET-DOM</ReplaceString>
	</ReplaceData>
	<ReplaceData Name="DomainSID">
		<SearchString>S-1-5-21-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxx</SearchString>
		<ReplaceString>S-1-5-21-xxxxxxxxxx-xxxxxxxxx-xxxxxxxxx</ReplaceString>
	</ReplaceData>
	<ReplaceData Name="LDAPDomain1">
		<SearchString>DC=source,DC=com</SearchString>
		<ReplaceString>DC=target,DC=com</ReplaceString>
	</ReplaceData>
	<IgnoreData Name="Set: All GroupAdmins">
		<SearchGUID>95833928-23e0-4e3d-bcc0-b824f3a8e123</SearchGUID>
	</IgnoreData>
	<IgnoreData Name="Set: All UserAdmin">
		<SearchGUID>28279e59-14b7-4b89-9d8a-b4c666a65301</SearchGUID>
	</IgnoreData>
</DeployConfig>

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

Or

Receive-Job –name

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.
In addition to this I have built a little helper script that would generate the XML for the objects I don’t want to be deployed and copy this directly to the clipboard, so you can easily add a lot of objects in a short time.

PARAM([string]$DisplayName)
set-variable -name URI -value "http://localhost:5725/resourcemanagementservice" -option constant

Write-Host "- Reading Set Information from Portal"
if(@(get-pssnapin | where-object {$_.Name -eq "FIMAutomation"} ).count -eq 0)
{add-pssnapin FIMAutomation}

$exportObject = export-fimconfig -uri $URI `
                               -onlyBaseResources `
                               -customconfig ("/Set[DisplayName='$DisplayName']")
if($exportObject -eq $null) {throw "Cannot find a set by that name"}
$ResourceID = $exportObject.ResourceManagementObject.ResourceManagementAttributes | `
                Where-Object {$_.AttributeName -eq "ObjectID"}

Write-Host "DisplayName: $DisplayName has ObjectID: " $ResourceID.Value
Write-Host
Write-Host "The following output is added to your clipboard:"
Write-Host
$objectGUID=$ResourceID.Value.Replace("urn:uuid:","")
$output=@()
$output+="`t"
$output+="`t`t$ObjectGUID"
$output+="`t"
$output | clip
$output
Write-Host

Notes:

This script has currently one small issue, each of the replacement objects are currently deployed every time you use this script, as the delta calculation see differences between the environments, and replacement is done after sync and calculate, but that’s ok it still works fine.

I will do an update of the script in maybe the near future to check if the replacement is already done to avoid this, but I haven’t find a way to do so right now.

Advertisement

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.

%d bloggers like this: