Monday 22 July 2013

FIM 2010 metaverse - How to script changes to resources and attributes

Creating new resources in the FIM metaverse isn't a difficult process (especially when compared to creating custom resources in the FIM Portal) and copying the schema from one machine to another is simply a matter of right click, schema-export, move to second machine, right click schema-import.

However there are times when scripting changes has its advantages. For instance when moving from a development environment to production it can be useful to apply changes in a controlled manner rather than just copying the dev setup complete with all of those trial and error development objects.

That thought set me off thinking that I should be able to script metaverse extensions with Powershell and, as no one seemed to have done it already, I found myself writing this script to take a simple CSV file of the desired object model and then create and binds the new metaverse schema by writing to the standard xml transfer file.

Initially I exported the schema of the target machine to an xml file using the Synchronization Service manager. This is my base schema that I wanted to extend and it contains all of the custom changes that the target machine already knows about. I also created a CSV file to represent the desired changes. The CSV file specifies a series of additional (or existing) attributes and the new (or existing) resource objects that they should be bound to. My test CSV file looks like this is MS Excel


The script loads the existing xml schema into memory and parses it to create a collection of existing resource and attribute objects. It then parses each row in the csv file . If the named resource doesn't exist it is created, if the named attribute doesn't exist it is created. Finally the attribute is bound to the resource. All changes are made to the xml document held in memory which is later written to disk. At that point I could inspect the output for completeness and errors prior to committing the schema changes. I don't claim this is the most elegant solution but it is functional and works for my purposes. In practice there would be a little more error checking and it would probably be only a component of a change control/release package script;

Param (
    $inFile = "mv-schema.xml",
    $outFile = "new-mv-schema.xml",
    $csvFile = "import.csv"
)

#hash tables for existing objects
$hashClasses = @{}
$hashAttributes = @{}
$hashBindings = @{}

# Paul Chimicz, July 2013
# resolve the in and out paths
$path = (Get-Location -PSProvider FileSystem).ProviderPath
$inFile =  $path +"\"+ $inFile
$outFile =  $path +"\"+  $outFile
$csvFile =  $path +"\"+ $csvFile

# function to create a hash table of all the existing resource types (classes)
function makeClassHash ([System.Xml.XmlDocument]$xml) {
  $classes = $xml.'export-mv-schema'.dsml.'directory-schema'.class
  foreach ($class in $classes) {
    $name = $class.name
    $attribute = $class.attribute
    $hashClasses.add($name, $attribute)
  }
}

# function to create a hash table of all the existing attribute-types
function makeAttributeHash ([System.Xml.XmlDocument]$xml) {
  $attributes = $xml.'export-mv-schema'.dsml.'directory-schema'.'attribute-type'
  foreach ($attribute in $attributes) {
    $name = $attribute.name
    $syntax = $attribute.syntax
    $hashAttributes.add($name, $syntax)
  }
}

# function to create a hash table of all of the attributes in a class
function makeBindingHash ([String]$className) {
  $hashBindings = @{}
  $resource = $xml.'export-mv-schema'.dsml.'directory-schema'.class | where {$_.id -eq $className}
  $attr = $resource.attribute
  foreach ($binding in $attr) {
    $ref = $binding.ref
    $required = $binding.required
    $hashBindings.add($ref, $required)
  }
}

# Read in the existing schema file and populate the hashes
$xml = New-Object System.Xml.XmlDocument
$xml.PreserveWhitespace = $true
$xml.Load($inFile)
makeClassHash($xml)
makeAttributeHash($xml)

# Evaluate the namespaces
$nsDSML = $xml.DocumentElement.dsml.Attributes['xmlns:dsml'].Value
$nsMSDSML = $xml.DocumentElement.dsml.Attributes['xmlns:ms-dsml'].Value

# Read in the csv file
$csv = import-csv $csvFile
foreach ($row in $csv) {

    # Does the resource type already exist?
    if (!$hashClasses.ContainsKey($row.ResourceType)) {
        write-host "Resource" $row.ResourceType "is not present: Adding"
        $class = $xml.CreateNode('Element',"dsml","class",$nsDSML)
        $class.SetAttribute('type','structural')
        $class.SetAttribute('id',$row.ResourceType)
        $xml.'export-mv-schema'.dsml.'directory-schema'.AppendChild($class| out-null
        $className = $xml.CreateNode('Element',"dsml","name",$nsDSML)
        $className.innerText = $row.ResourceType
        $class.AppendChild($className| out-null
        # add to the hash collection so we don't add it again
        $name = $row.ResourceType
        $hashClasses.add($name, $null)
    }

    # Does the attribute-type already exist?
    if (!$hashAttributes.ContainsKey($row.Attribute)) {
        write-host "Attribute-type" $row.Attribute "is not present: Adding"

        # create the attribute-type and set its attributes
        $attrType = $xml.CreateNode('Element',"dsml","attribute-type",$nsDSML)
        $attrType.SetAttribute('id',$row.Attribute)
        if ($row.Multivalued.ToUpper() -eq "TRUE") {
           $attrType.SetAttribute('single-value','false'| Out-Null
        } else {
           $attrType.SetAttribute('single-value','true'| Out-Null
        }

        # create the attribute-type child nodes
        $syntax = $xml.CreateNode('Element',"dsml","syntax",$nsDSML)
        $name = $xml.CreateNode('Element',"dsml","name",$nsDSML)

        # set the properties depending upon the data type
        $name.innerText = $row.Attribute
        switch ($row.DataType) {
          'Indexed String' {
                $syntax.innerText = "1.3.6.1.4.1.1466.115.121.1.15"
                $attrType.SetAttribute('indexable',$nsMSDSML,'true'| Out-Null
                if ($row.Indexed.ToUpper() -eq "TRUE") {
                    $attrType.SetAttribute('indexed',$nsMSDSML,'true'| Out-Null
                    }
                }
          'Unindexed String' {
                $syntax.innerText = "1.3.6.1.4.1.1466.115.121.1.15"
                }
          'Reference' {
                $syntax.innerText = "1.3.6.1.4.1.1466.115.121.1.12"
                }
          'Binary' {
                $syntax.innerText = "1.3.6.1.4.1.1466.115.121.1.5"
                }
          'Boolean' {
                $syntax.innerText = "1.3.6.1.4.1.1466.115.121.1.7"
                }
          'Integer' {
                $syntax.innerText = "1.3.6.1.4.1.1466.115.121.1.27"
                }
          'Date' {
                $syntax.innerText = "1.3.6.1.4.1.1466.115.121.1.15"
                $attrType.SetAttribute('indexable',$nsMSDSML,'true'| Out-Null
                if ($row.Indexed.ToUpper() -eq "TRUE") {
                    $attrType.SetAttribute('indexed',$nsMSDSML,'true'| Out-Null
                    }             
                }
        }

        # add the attribute-type to the xml and remember the node
        $newNode = $xml.'export-mv-schema'.dsml.'directory-schema'.AppendChild($attrType)

        # add the child nodes to the new attribute-type node in the xml
        $newNode.AppendChild($syntax| out-null
        $newNode.AppendChild($name| out-null
      
        # add the attribute-type to the hash collection so we don't add it again
        $name = $row.Attribute
        $hashAttributes.add($name, $null)
    }

    # enumerate the attributes already bound to this resource type
    makeBindingHash($row.ResourceType)
    # and add our attribute if it isn't there
    if (!$hashBindings.ContainsKey('#' + $row.Attribute)) {
        write-host "Binding of" $row.Attribute " to" $row.ResourceType"is not present: Adding"
        $class = $xml.'export-mv-schema'.dsml.'directory-schema'.class | where {$_.id -eq $row.ResourceType}
        $attribute = $xml.CreateNode('Element',"dsml","attribute",$nsDSML)
        if ($row.Required.ToUpper() -eq "TRUE") {
            $attribute.SetAttribute('required','true'| Out-Null
        } else {
            $attribute.SetAttribute('required','false'| Out-Null
        }
        $attribute.SetAttribute('ref','#'+$row.Attribute) | Out-Null
        $class.AppendChild($attribute| out-null
    }
}

# and finally write it all out to a new file
$xml.Save($outFile)

After running the script the output file is available to load back into the target machine with the schema import command. The script uses the default name import.csv but can be overridden on the command line as can the schema import and output file names. The script is designed to be safe to run more than once as it only makes additions where necessary. There are no issues if the attribute already exists (or the resource exists but is bound to just a subset of the attributes). The definition is simply updated.

There are some restrictions and limitations
  • The script does not remove excess attributes that are already bound
  • The script cannot delete a resource class
  • The script does not modify the properties of an attribute if it already exists




No comments:

Post a Comment