Wednesday, January 6, 2016

Modifying SCSM Management Pack to add Categories

Hello everyone!

There was some unfolding developments on my last post regarding the SCSM categories. The SCSM Admistrator asked me that he had received a request that required him to add about 50+ different categories through several parent categories, but that the way to add them through SCSM was extremely inefficient and was hoping that I could write a script or program to make it more streamlined.

I spoke with the Administrator and asked him to first tell me how it would be done manually, without the SCSM GUI, and found that it is all modified through the XML Management Pack. I first looked around for someone who maybe already created something for it, and I did found tools that allowed you to create MP through an Excel sheet, like this, but if you wanted to modify the existing MP I couldn't find anything.

I decided I'll have to create something. I originally planned to create him a desktop application with C#; however, my free trial for Visual Studio ran out, which I knew it was coming but I didn't want to request a license from the company because I have a feeling it would get denied. Anyways, so instead I decided to go with PowerShell.

I had been avoiding working with XML for a while, so it was a good opportunity to finally sit down and try to learn the ways. I got quite a big of help from the scripting guy, but mostly it was trial and error. In the end I was able to create 3 functions:

I created 3 cmdlets that will allow us to get quick information on the support group/category (list information, and treeview) and be able to add one or multiple lists in an instance.

Requirements:
Exported Management Pack containing the lists.
Name of the list that you wish to get information on/add more lists to.

The first cmdlet is 'Get-SCSMListItem' this cmdlet will provide you with information about list, you'll need to know the name of it, or GUID, and will provide the following output:


Notice that if you use the name parameter you can get multiple results, so you must know which one you need to use in order to accurately assess which one should be the parent when you are creating new lists. 

The second cmdlet is 'Get-SCSMListChildItems' this cmdlet will print out a 'structure' of the lists, depending on which parent you give it, please see below for an example that will further explain this:


Also notice the same parameter here, which i did, it will print out the structure for both lists. Making it simple to see which one is the one you wish to work with, and they'll print out in the same order, so once you know which one you need to work with you can use the necessary IDs. 

The last cmdlet is Add-SCSMListItems this cmdlet supports the following parameters:
  • -Lists: This parameter will contain the name of the lists that you want to add. This parameter will accept array. For example:
    • Single List:  -List ‘My New Category’
    • Multiple Lists: -List ‘My New Category1’,’My New Category2’
  • -ParentName: This parameter will be the name of the Parent where you wish to add your lists. Keep in mind that This cmdlet will not support multiple results for the ‘Name’ parameter, if multiple lists contain the same name, as shown in the past for ‘Group IT’, the command will fail. 
  • -ParentID: This parameter will be the ID of the Parent where you wish to add the lists. This parameter cannot be used with the ParentName parameter.
  • -ManagementPackXML: This parameter requires that you provide the exact path of the Management Pack XML file you exported from SCSM.
  • -ExportLocation: This is the file path where the modified XML file will be exported. If this is left blank the export will overwrite the imported Management Pack XML. If the export path provided does not exist the command will fail and no changes will be saved. 
There is no console output for this command.

Here is the command will do:
For each given list, it will create a new DisplayString item in the Management Pack, worth noting that this is hard coded to add it to the ENU LanguagePack; however, it can be easily modified to accept a parameter for the language pack as well, see the XML code examples below:

<DisplayString ElementID="$newGUID">
       <Name>$providedName</Name>
</DisplayString>

<EnumerationValue ID="$newGUID" Accessibility="Public" Parent="$providedParent" Ordinal="$ordinal" />

I'd also like to point out that as long as you know the parent you can use a CSV file to quickly create multiple lists. 

First you’ll need to create a CSV file that will look like the one below:

Parent
ListName
ParentGUID
Cat1_UnderRootParent
ParentGUID
Cat2_UnderRootParent
ParentGUID
Cat3_UnderRootParent
ParentGUID2
Cat1_UnderDifferentParent
ParentGUID2
Cat2_UnderDifferentParent
ParentGUID3
Cat1_SCSMParent
ParentGUID3
Cat2_SCSMParent

The first column will specify the parent that you wish to use, and the second the category that you wish to create. Once you have the CSV create you’ll use it in PowerShell like so:

$csv = Import-Csv C:\myCSVFile.csv

foreach($category in $csv)
{
    Add-SCSMListItems -Lists $category.ListName `
                      -ParentID $category.Parent `
                      -ManagementPackXML C:\ManagementPackPath.xml
                      -ExportLocation C:\ManagementPackPath_new.xml [see edit for details]

Once the changes are done then you can simply export the MP XML file back into SCSM and voila all done. 

Now for the source code:


function Get-SCSMListItem
{
    <#
      .SYNOPSIS
      This function will get information on an SCSM List Item.
      .DESCRIPTION
      This function will get information on the SCSM List Item specified. It will provide the Name, Parent, ID and Ordinance. 
      .EXAMPLE
      Get-SCSMList -Name "MyArea/CategoryName" -ManagementPackXML C:\MP_xmlFile.xml
      .EXAMPLE
      Get-SCSMList -ID "Enum.GUID" -ManagementPackXML C:\MP_xmlFile.xml
      .PARAMETER Name
      The name of the list you want to get information on
      .PARAMETER ID
      The ID of the list you want to get information on
      .PARAMETER ManagementPackXML
      The location where the XML File is saved
    #>
    [CmdletBinding()]
    Param
    (
        #Name
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='What name of the list do you want to get?',
            ParameterSetName="Name")]
        [Alias('ListName')]
        [ValidateLength(3,250)]
        [string]$Name,
        #ID
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='What ID of the list do you want to get?',
            ParameterSetName="ID")]
        [Alias('ElementID')]
        [ValidateLength(0,250)]
        [string]$ID,
        #ManagementPackXML
        [Parameter(Mandatory=$True,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='Where is the Management Pack XML Located?')]
        [Alias('XMLPath')]
        [ValidateLength(3,255)]
        [string]$ManagementPackXML
    )
    begin
    {        
    }
    process
    {
        Write-Verbose "Checking the XML file exists."
        Test-Path $ManagementPackXML -ErrorAction Stop | Out-Null
        Write-Verbose "File exists."

        Write-Verbose "Opening XML File with UTF8 encoding."
        [xml]$xml = Get-Content $ManagementPackXML -Encoding UTF8
        Write-Verbose "File opened successful."

        if($ID.Length -ne 0)
        {
            Write-Verbose "ID Provided. Attempting to find in the XMLElement DisplayStrings."
            $listResult = $xml.ManagementPack.LanguagePacks.LanguagePack | ? ID -eq "ENU" | %{$_.DisplayStrings.DisplayString} | ? ElementID -eq $ID
        }
        else
        {
            Write-Verbose "No ID Provided. Attempting to find name in the XMLElement DisplayStrings."
            $listResult = $xml.ManagementPack.LanguagePacks.LanguagePack | ? ID -eq "ENU" | %{$_.DisplayStrings.DisplayString} | ? Name -eq $name
        }
        
        if($listResult -ne $null)
        {
            foreach($list in $listResult)
            {
                Write-Verbose "The element has been found in the XML file. Gathering information."
                $ListInformationObject = New-Object psobject
                $ListInformationObject | Add-Member -MemberType NoteProperty -Name "ID" -Value $list.ElementID
                $ListInformationObject | Add-Member -MemberType NoteProperty -Name "Name" -Value $list.Name
                $ListInformationObject | Add-Member -MemberType NoteProperty -Name "Parent" -Value ($Xml.GetElementsByTagName("EnumerationValue") | ? ID -eq $list.ElementID).Parent
                $ListInformationObject | Add-Member -MemberType NoteProperty -Name "Ordinal" -Value ($Xml.GetElementsByTagName("EnumerationValue") | ? ID -eq $list.ElementID).Ordinal
                $ListInformationObject
            }
        }        
        else
        {
            Write-Error -Message "No lists were found by the name $Name" -Category InvalidArgument -RecommendedAction "Please ensure that the list exists in the XML File." `
                        -CategoryActivity "Get-SCSMListItem, Filter-XMLElement(DisplayString) by Name" `
                        -CategoryReason "The List Name provided was not found." `
        }
    }
    end
    {
        Remove-Variable -Name xml
    }
}

function Get-SCSMListChildItems
{
    <#
      .SYNOPSIS
      This function will get a view of the child items.
      .DESCRIPTION
      This function will get a view of the child lists. Output:
        :: ListName
        ::    SubListName_L1_1
        ::    SubListName_L1_2
        ::       SubListName_L2_1
        ::    SubListName_L1_3
      .EXAMPLE
      Get-SCSMListChildItems -Name "MyArea/CategoryName" -ManagementPackXML C:\MP_xmlFile.xml
      .EXAMPLE
      Get-SCSMListChildItems -ID "Enum.GUID" -ManagementPackXML C:\MP_xmlFile.xml
      .PARAMETER Name
      The name of the list you want to get information on
      .PARAMETER ID
      The ID of the list you want to get information on
      .PARAMETER ManagementPackXML
      The location where the XML File is saved
    #>
    [CmdletBinding()]
    Param
    (
        #ID
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='What is the ID of the list you wish to get child items for?',
            ParameterSetName="ID")]
        [Alias('ElementID')]
        [ValidateLength(3,250)]
        [string]$ID,
        #Name
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='What is the name of the list you wish to get child items for?',
            ParameterSetName="Name")]
        [Alias('ListName')]
        [ValidateLength(0,250)]
        [string]$Name,
        #ManagementPackXML
        [Parameter(Mandatory=$True,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='Where is the Management Pack XML Located?')]
        [Alias('XMLPath')]
        [ValidateLength(3,255)]
        [string]$ManagementPackXML
    )
    begin
    {
    }
    process
    {
        Write-Verbose "Checking the XML file exists."
        Test-Path $ManagementPackXML -ErrorAction Stop | Out-Null
        Write-Verbose "File exists."

        Write-Verbose "Opening XML File with UTF8 encoding."
        [xml]$xml = Get-Content $ManagementPackXML -Encoding UTF8
        Write-Verbose "File opened successful."

        if($ID.Length -eq 0)
        {
            Write-Verbose "If the ID is not provided, search by name to get the ID."
            $nameSearch = Get-SCSMListItem -Name $Name -ManagementPackXML $ManagementPackXML
        }

        function Get-ChildNodes($parentNode, $depth)
        {
            $depth++
            $childNodes = $listNodes | ? Parent -eq $parentNode.ID
            foreach($child in $childNodes)
            {
                "$("`t"*$depth) - $((Get-SCSMListItem -ID $child.ID -ManagementPackXML $ManagementPackXML).Name) - $((Get-SCSMListItem -ID $child.ID -ManagementPackXML $ManagementPackXML).Ordinal)"
                get-childNodes $child $depth
            }
    
        }

        foreach($result in $nameSearch)
        {
            Write-Verbose "Get all of the Elements from XMLElement EnumerationValue."
            $listNodes = $xml.GetElementsByTagName("EnumerationValue")
            foreach($node in $listNodes)
            {
                $depth = 0
                if($node.ID -eq $result.ID)
                {
                    Write-Verbose "If the Node.ID is equal to the provided (acquired) ID, then Print information and get child elements."
                    " - $((Get-SCSMListItem -ID $node.ID -ManagementPackXML $ManagementPackXML).Name) - $((Get-SCSMListItem -ID $node.ID -ManagementPackXML $ManagementPackXML).Ordinal)"
                    get-childNodes $node, $depth
                }
            }
        }
    }
    end
    {
        Remove-Variable -Name xml
    }
}

function Add-SCSMListItems
{
    <#
      .SYNOPSIS
      This function will add a new item to the XML Management Pack.
      .DESCRIPTION
      This function will add a new item to the XML Management Pack. This will include the EnumerationValue and the DisplayString. 
        XML Example:
            <EnumerationTypes ID="Enum.6a70247ad154475fb9138d6d2c52c8a3" Accessibility="Public" Parent="ServiceRequest!ServiceRequestAreaEnum.Directory" Ordinal="500" />

            <DisplayString ElementID="Enum.6a70247ad154475fb9138d6d2c52c8a3">
                <Name>Test</Name>
            </DisplayString>
      .EXAMPLE
      Add-SCSMListItems -Lists "LIST ITEM NAME" -ParentName "Group IT" -ManagementPackXMl C:\MP_XML.xml -ExportLocation C:\MP_XML_New.xml
      .EXAMPLE
      Add-SCSMListItems -Name "LIST ITEM NAME" -ParentID "Enum.GUID" -ManagementPackXMl C:\MP_XML.xml -ExportLocation C:\MP_XML_New.xml
      .PARAMETER ParentName
      The name of the Parent that will store the new list item
      .PARAMETER ParentID
      The ID of the Parent that will store the new list item
      .PARAMETER Lists
      The name of the new list items
      .PARAMETER ManagementPackXML
      The location where the Management Pack XML file is located
      .PARAMETER ExportLocation
      The location where the new XML file will be saved. It can be the same but it is not recommended to over write the file.
    #>
    [CmdletBinding()]
    Param
    (
        #Lists
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='What is the name of the list you wish to get child items for?')]
        [Alias('ListName')]
        [ValidateLength(3,250)]
        [string[]]$Lists,
        #ParentName
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='What is the name of the Parent where the Lists should be added?',
            ParameterSetName="ParentName")]
        [Alias('Parent')]
        [ValidateLength(3,250)]
        [string]$ParentName,
        #ParentID
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='What is the ID of the Parent where the Lists should be added?',
            ParameterSetName="ParentID")]
        [Alias('PID')]
        [ValidateLength(3,250)]
        [string]$ParentID,
        #ManagementPackXML
        [Parameter(Mandatory=$True,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='Where is the Management Pack XML Located?')]
        [Alias('XMLPath')]
        [ValidateLength(3,255)]
        [string]$ManagementPackXML,
        #ExportLocation
        [Parameter(Mandatory=$false,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$True,
            HelpMessage='Where is the Management Pack XML Located?')]
        [Alias('exportPath')]
        [ValidateLength(3,255)]
        [string]$ExportLocation
    )
    begin
    {
        if($ExportLocation -eq $null -or $ExportLocation.Length -eq 0)
        {
            $ExportLocation = $ManagementPackXML
        }
    }
    process
    {
        Test-Path $ManagementPackXML -ErrorAction Stop | Out-Null
        [xml]$xml = Get-Content $ManagementPackXML -Encoding UTF8
        
        if($ParentID.Length -eq 0)
        {
            $nameSearch = Get-SCSMListItem -Name $ParentName -ManagementPackXML $ManagementPackXML
            if($nameSearch.Count -eq 1)
            {
                $ParentID = $nameSearch.ID
            }
            else
            {
                Write-Error -Message "Unable to create the lists as multiple parents were found." -Category InvalidResult -RecommendedAction "Find the ID of the parent you wish to add the list and try again with the ID." -CategoryActivity "MultipleResultsError" -ErrorAction Stop
            }
            
        }

        function Get-NextOrdinal($parentID)
        {
            return ($xml.ManagementPack.TypeDefinitions.EntityTypes.EnumerationTypes.EnumerationValue | ? Parent -eq $parentID | Measure-Object -Property Ordinal -Maximum).Maximum + 1
        }

        foreach($list in $Lists)
        {
            $element_EnumartionTypes = $xml.CreateElement("EnumerationValue")
            $element_EnumartionTypes.SetAttribute('ID',"Enum.$(([GUID]::NewGuid().Guid).replace("-",''))")
            $element_EnumartionTypes.SetAttribute('Accessibility',"Public")
            $element_EnumartionTypes.SetAttribute('Parent',$ParentID)
            $element_EnumartionTypes.SetAttribute('Ordinal', $(Get-NextOrdinal -parentID $ParentID))
            $xml.ManagementPack.TypeDefinitions.EntityTypes.EnumerationTypes.AppendChild($element_EnumartionTypes) | Out-Null

            $Element_DisplayName = $xml.CreateElement("DisplayString")
            $Element_DisplayName.SetAttribute('ElementID',$element_EnumartionTypes.id)
            $Element_DisplayName_Name = $xml.CreateElement("Name")
            $Element_DisplayName_Name.InnerText = $list
            $Element_DisplayName.AppendChild($Element_DisplayName_Name) | Out-Null
            $xml.ManagementPack.LanguagePacks.LanguagePack | ? ID -eq "ENU" | %{$_.DisplayStrings.AppendChild($Element_DisplayName)} | Out-Null
        }
        
        if(Test-Path (Split-Path $ExportLocation))
        {
            $xml.Save($ExportLocation)
        }
        else
        {
            Write-Error -Message "The provided export file cannot be found." -RecommendedAction "Ensure that the file path exists and try again." -Category ResourceUnavailable -ErrorAction Stop
        }
        
    }
    end
    {
        Remove-Variable -Name element_EnumartionTypes
        Remove-Variable -Name Element_DisplayName_Name
        Remove-Variable -Name Element_DisplayName
    }
}

I hope that this may help someone out there. Do let me know if you have any questions, or issues, also if you find something else like it out there because I couldn't find that many people creating tools for SCSM. 

Kind regards,
Me.

Edit 1/14/2015: 

I found a bug when working with the batch addition of lists. The problem happens because for each loop the MP will overwrite the last change. 

In order to fix this you won't be able to use the 'Export-Path' parameter, and will have to overwrite the XML for every change.