More Fun with COM: Importing Integration Packs via PowerShell

More Fun with COM: Importing Integration Packs via PowerShell

  • Comments 5
  • Likes

Around the office, we have more and more internal teams that are latching on to Orchestrator and wanting to use it for building all sorts of integrations and automations, but one of the things that comes up from time to time is how to get the ability to have some sort of complete installation process that doesn’t require a separate step for installing Integration Packs or runbooks. In this case, enabling the automatic registering and deployment of an Integration Pack would really enable a streamlined installation process.

Note from the legal dept: The process described here is not officially supported by Microsoft and are provided only as an example to the community.

Neither I nor Microsoft, nor any other person, animal, vegetable or mineral assumes responsibility for the process demonstrated here. USE AT YOUR OWN RISK!

Before I start, here are links to the previous parts of the “COM” series:

Going back into the COM interface for Orchestrator, I can see that there is an “AddIntegrationPack” method available, and it takes a “Variant” type (which is usually XML for the Orchestrator Interface). The only thing is I don’t know what that XML looks like. However, luckily enough, there is also a “GetIntegrationPacks” method that I can use to return XML and I can figure out the format needed! As it turns out, the XML format looks like this:

<IntegrationPacks>
  <IntegrationPack>
    <UniqueID datatype="string">{7A17B8D2-D916-4397-B774-26D9EDE3D9F1}</UniqueID>
    <Name datatype="string">System Center Configuration Manager</Name>
    <Description datatype="string">Workflow Activities for System Center Configuration Manager</Description>
    <Version datatype="int64">1</Version>
    <Library datatype="string">SCCMClientServerExtension.dll</Library>
    <ProductName datatype="string">IP_SYSTEMCENTERCONFIGURATIONMANAGER_1.0.OIP</ProductName>
    <ProductID datatype="string">{FD5A9BAC-E55F-4F35-AB26-381EE1D85B23}</ProductID>
  </IntegrationPack>
</IntegrationPacks>

So now I know that if I use this format when adding an Integration Pack, I should be fine. But where do I get this information, and what does this really get me?

First of all, it’s useful to know how the whole process of registering and deploying Integration Packs works. If you missed the earlier post on the topic, you should see it here:

Understanding IP Installation: What Does Register/Unregister/Deploy/Undeploy Really Mean?

As it turns out, the format of the XML is actually the format of the .CAP file located inside the .OIP file. What this process of "AddIntegrationPack” does is the registration part. It doesn’t cover the rest of the stuff that Deployment manager does like copying the files to the right places or deploying the MSI to make the IP functional. It’s really just adding info to the database. However, this is a crucial part to getting an IP working. So in addition to the COM interface stuff, I will have to add some other stuff to be fully functional. Here’s the whole process in a nutshell:

  1. Crack open the OIP file and extract the files within to a subdirectory
  2. Copy the MSI File to %Common Files%\Microsoft System Center 2012\Orchestrator\Management Server\Components\Objects
  3. Read the MSI database info to get the product name and product code (since those are only in the MSI)
  4. Read the .CAP file into an XML object and update the ProductName and ProductID elements to be the product code from #3.
  5. Use AddIntegrationPack to import the CAP file info into the database
  6. Rename the OIP file to be {ProductCode}.OIP and copy to %Common Files%\Microsoft System Center 2012\Orchestrator\Management Server\Components\Packs
  7. Run msiexec.exe to install the MSI file

In order to do all of this from PowerShell, I relied on some cool code provided on Rikard Ronnkvist’s blog that allowed me to read properties from within an MSI file, including loading another COM object (WindowsInstaller.Installer) and some new type definitions that I load dynamically in my script.

Here is the script:




Function LoadTypeData()
{
$contents = @"
<!--Copied from: Abhishek's PowerShell Blog - http://abhishek225.spaces.live.com/blog/
    Original post: http://abhishek225.spaces.live.com/blog/cns!13469C7B7CE6E911!165.entry-->

<Types>
  <Type>
    <Name>System.__ComObject</Name>
    <Members>
      <ScriptMethod>
        <Name>GetProperty</Name>
        <Script>
          $type = $this.gettype();
          $type.invokeMember($args[0],[System.Reflection.BindingFlags]::GetProperty,$null,$this,$null)
        </Script>
      </ScriptMethod>
      <ScriptMethod>
        <Name>SetProperty</Name>
        <Script>
           $type = $this.gettype();
           $type.invokeMember($args[0],[System.Reflection.BindingFlags]::GetProperty,$null,$this,@($args[1]))
         </Script>
      </ScriptMethod>
      <ScriptMethod>
        <Name>InvokeParamProperty</Name>
        <Script>
          $type = $this.gettype();
          $index = $args.count -1 ;
          $methodargs=$args[1..$index]
          $type.invokeMember($args[0],[System.Reflection.BindingFlags]::GetProperty,$null,$this,$methodargs)
        </Script>
      </ScriptMethod>
      <ScriptMethod>
        <Name>InvokeMethod</Name>
        <Script>
          $type = $this.gettype();
          $index = $args.count -1 ;
          $methodargs=$args[1..$index]
          $type.invokeMember($args[0],[System.Reflection.BindingFlags]::InvokeMethod,$null,$this,$methodargs)
        </Script>
      </ScriptMethod>
    </Members>
  </Type>
</Types>
"@
$contents | Out-File -FilePath $(Join-Path $(Get-Location) "comObject.types.ps1xml")  -Force
Update-TypeData -AppendPath $(Join-Path $(Get-Location) "comObject.types.ps1xml") -ErrorAction SilentlyContinue
}

function global:Get-MsiProperty
{    
    PARAM (        
    [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)]
    [String]$Filename,

    [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)]
    [String]$Propertyname
    )
    
    # A quick check to see if the file exist    
    if(!(Test-Path $Filename))
    {        
        throw "Could not find " + $Filename    
    }     

    # Create an empty hashtable to store properties in    
    $msiProp = ""
    # Creating WI object and load MSI database    
    $wiObject = New-Object -com WindowsInstaller.Installer    
    $wiDatabase = $wiObject.InvokeMethod("OpenDatabase", (Resolve-Path $Filename).Path, 0)     
    # Open the Property-view    
    $view = $wiDatabase.InvokeMethod("OpenView", "SELECT * FROM Property")    
    $view.InvokeMethod("Execute")     
    # Loop thru the table    
    $r = $view.InvokeMethod("Fetch")    
    while($r -ne $null)
    {        
        # Add property and value to hash table        
        $prop = $r.InvokeParamProperty("StringData",1)
        $value = $r.InvokeParamProperty("StringData",2)
        if ($prop -eq $PropertyName)
        {
            $msiProp = $value
        }
        # Fetch the next row        
        $r = $view.InvokeMethod("Fetch")    
    }     
    $view.InvokeMethod("Close")     
    
    return $msiProp
}


function Connect-SCOServer{
<#
.SYNOPSIS
   Establishes a connection handle to the Orchestrator COM interface

.DESCRIPTION
    Establishes a connection handle to the Orchestrator COM interface that can be used
    with other functions that require authentication to succeed. This script must
    be run from the Orchestrator Management Server.

.PARAMETER UserName
    A valid user account with rights to perform actions against Orchestrator
    objects such as runbooks, folders, etc. Must be entered in the form of:
    DOMAIN\USERNAME
    
.PARAMETER Password
    The password associated with the provided user account.
    
.EXAMPLE
    $ConnectionHandle = Connect-SCOServer -Username "domain\user" -Password "Password"
    
.OUTPUTS
    System.Int32

#>

[CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true, ParameterSetName="SpecifyUser")]
        [ValidateNotNullOrEmpty()]
        [String] $UserName = $(throw "Provide a valid user account (domain\username)"),
        
        [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true, ParameterSetName="SpecifyUser")]
        [ValidateNotNullOrEmpty()]
        [String] $password = $(throw "Provide a password for the user account)")
    )
    
    PROCESS
    {
        try
        {
            $oismgr = new-object -com OpalisManagementService.OpalisManager
            $ohandle = New-Object object
            $handle = $handle = New-Object Runtime.InteropServices.VariantWrapper($ohandle)
            $retval = $oismgr.Connect($UserName, $password, [ref]$handle)
            
            if ($handle.GetType().FullName -eq "System.Int32")
            {
                return $handle
            }
            else
            {
                Write-Error "Unable to get a valid connection handle to Orchestrator!" -ErrorAction Stop
            }
        }
        catch
        {
            Write-Error "Exception occurred in $($MyInvocation.MyCommand): `n$($_.Exception)"
            throw new-object system.formatexception
        }
    }
}


function Install-SCOIntegrationPack{
<#
.SYNOPSIS
  Registers and optionally deploys an Integration Pack to the current computer.
  Assumes the current computer is a Management Server (and a Designer/Runbook Server
  in the case of deploying the IP)

.DESCRIPTION
    
.PARAMETER Handle
  The handle to the COM interface created using New-SCOConnection

.PARAMETER Filename
  The path and filename of the OIP file to be imported

.PARAMETER Deploy
  Switch parameter used when you want to also deploy the IP.
      
.EXAMPLE
  Install-SCOIntegrationPack -OIPFile "C:\Files\Test.OIP" -Deploy
    

.OUTPUTS
   
#>
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)]
        [Int] $Handle,

        [Parameter(Mandatory=$true)]
        [String] $Filename,
                       
        [Parameter(Mandatory=$false)]
        [Switch] $Deploy
        
        
    )
    BEGIN
    {
        $oismgr = new-object -com OpalisManagementService.OpalisManager
        $sh = new-object -com shell.application
        LoadTypeData
    }
    PROCESS
    {
        try
        {

            if ($(Test-Path $filename) -eq $false)
            {
                Write-Error "File Not Found!"
                return
            }
            [System.IO.FileInfo]$oipFile = gi $filename
            
            $extractDir = $(Join-Path -Path $oipFile.DirectoryName -ChildPath $oipFile.BaseName)
            if (Test-Path $extractDir)
            {
                ri $extractDir -Recurse -Force
            }

            $ipSourceDirObj = $sh.namespace($oipFile.DirectoryName)
            $ipSourceDirObj.NewFolder($oipFile.BaseName)
            $extractDirObj = $sh.namespace($extractDir)

            Write-Debug "`n`nExtracting files from the OIP"
            $zipFileName = Join-Path $oipFile.DirectoryName "$($oipFile.BaseName).zip"
            if (Test-Path $zipFileName)
            {
                ri $zipFileName -Force
            }
            $zipFile = $oipFile.CopyTo($zipFileName)
            $zipFileObj = $sh.namespace($zipFile.FullName)

            
            $extractDirObj.CopyHere($zipFileObj.Items(),8 -and 16 -and 256)

            $commonFiles = ${Env:CommonProgramFiles(x86)}
            if ($null -eq $commonFiles)
            {
                $commonFiles = ${Env:CommonProgramFiles}
            }
            $PacksDir = Join-Path $commonFiles "Microsoft System Center 2012\Orchestrator\Management Server\Components\Packs"
            $ObjectsDir = Join-Path $commonFiles "Microsoft System Center 2012\Orchestrator\Management Server\Components\Objects"

            if ($(Test-Path $PacksDir) -eq $false)
            {
                Write-Error "Could not find $($PacksDir)"
                return
            }
            if ($(Test-Path $ObjectsDir) -eq $false)
            {
                Write-Error "Could not find $($ObjectsDir)"
                return
            }

           

            # Copy the MSI File to %Common Files%\Microsoft System Center 2012\Orchestrator\Management Server\Components\Objects
            $msiFile = $extractDirObj.self | gci | Where {$_.extension -eq ".msi"}
            

            $newMSIfile = $(Join-Path $ObjectsDir $msiFile.Name)
            if (Test-Path $newMSIfile)
            {
                ri $newMSIfile -Force
            }
            $msiFile.CopyTo($newMSIfile)
            $productName = Get-MSIProperty -Filename $msiFile.FullName -Propertyname "ProductName"
            $productCode =  Get-MSIProperty -Filename $msiFile.FullName -Propertyname "ProductCode"


            #now use the MgmtService to install the IP
            [System.IO.FileInfo]$capfile = $extractDirObj.self | gci | Where {$_.extension -eq ".cap"}
            if ($capfile)
            {
                Write-Host "     Extracting $($capFile)"
                $capXml = New-Object XML
                $capXml.Load($capfile.Fullname)

            
                # Need to modify the CAP file to add Product ID and Product Name because Deployment Manager
                # reads the MSI file for this and inserts it into the DB so it displays in the UI. The COM
                # interface does not do this, so it needs to be done manually if you want it displayed.
                #
                #     <ProductName datatype="string">IP_SYSTEMCENTERDATAPROTECTIONMANAGER_1.0.OIP</ProductName>
                #    <ProductID datatype="string">{9422FCC6-11C4-4827-AC49-C5FD352C8AA0}</ProductID>
                

                [Xml]$prodName = "<ProductName datatype=`"string`">$($ProductName)</ProductName>"
                [Xml]$prodID = "<ProductID datatype=`"string`">$($ProductCode)</ProductID>"
                
                $c = $capXml.ImportNode($prodName.ProductName, $true)
                $d = $capXml.ImportNode($prodID.ProductID, $true)
           
                $capXml.Cap.AppendChild($c)
                $capXml.Cap.AppendChild($d)

                $oIPinfo = new-object object
                [ref]$ipinfo = $ipinfo = New-Object Runtime.InteropServices.VariantWrapper($capXml.get_innerxml())
                Write-Host "     Importing Integration Pack $($capFile)"
                $retval = $oismgr.AddIntegrationPack($Handle, $ipinfo)
            }

            # Copy the OIP File to the %Common Files%\Microsoft System Center 2012\Orchestrator\Management Server\Components\Packs
            # directory and change the name to the GUID of the IP
            $productCodeOipFilename = "$($ProductCode).OIP"

            $newOIPfile = $(Join-Path $PacksDir $productCodeOipFilename)
            if (Test-Path $newOIPfile)
            {
                ri $newOIPfile -Force
            }
            $oipFile.CopyTo($newOIPfile)



            if ($PSBoundParameters.ContainsKey('Deploy') -eq $false)
            {
                return
            }

            if ($(Test-Path $newMSIfile) -eq $false)
            {
                Write-Error "Could not find $($newMSIfile)"
                return
            }

            Write-Verbose "Running msiexec to install  $($newMSIfile)"

            $proc = New-Object System.Diagnostics.Process
            $proc.StartInfo.FileName = "msiexec.exe"
            $proc.StartInfo.Arguments =  "/i `"$($newMSIfile)`" /qn"
            $proc.Start() | out-null
            $proc.WaitForExit()

        }
        catch
        {
            Write-Error "Exception occurred in $($MyInvocation.MyCommand): `n$($_.Exception)"
            
        }
        
    }    
        
}

To run the script, open a PowerShell(x86) console on the Management Server using Run As Administrator (required to access the Common Files directories), and then just do the following:

  1. Create a connection to the COM interface :

         $conn = Connect-SCOServer –username <domain\user> -password <password>
  2. Register the IP:

        Install-SCOIntegrationPack –handle $conn –filename <path and filename of OIP>

    if you also want to deploy the IP to the local machine (assuming it’s a Runbook Designer or Runbook Server):

        Install-SCOIntegrationPack –handle $conn –filename <path and filename of OIP> –Deploy
  3. You’re done!

You can see how this process can enable you do incorporate IP installation and deployment into the actual installation (like via an MSI) of an application. Keep watching for more fun and exciting articles on automating administration of Orchestrator.

The script shown above is also attached to the article. Enjoy!

 

Attachment: Install-IP.ps1
Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • <p>Hi Robert,</p> <p>I&#39;m currently trying your script, but I&#39;ve got the error below. An idea of the problem ?</p> <p>Thank you</p> <p>Christopher</p> <p>PS C:\Users\christopher\Desktop\SCORCH IPs\SCORCH IPs&gt; .\Install-IP.ps1</p> <p>Mode &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;LastWriteTime &nbsp; &nbsp; Length Name</p> <p>---- &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;------------- &nbsp; &nbsp; ------ ----</p> <p>-a--- &nbsp; &nbsp; &nbsp; &nbsp; 9/17/2013 &nbsp; 5:48 PM &nbsp; &nbsp; 942080 Microsoft.SystemCenter.Orchestrator.Integration.AzureIP.msi</p> <p>Install-SCOIntegrationPack : Exception occurred in Install-SCOIntegrationPack:</p> <p>System.Management.Automation.RuntimeException: Method invocation failed because [System.__ComObject] does not contain</p> <p>a method named &#39;InvokeMethod&#39;.</p> <p> &nbsp; at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception</p> <p>exception)</p> <p> &nbsp; at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)</p> <p> &nbsp; at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)</p> <p> &nbsp; at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)</p> <p> &nbsp; at System.Management.Automation.Interpreter.Interpreter.Run(InterpretedFrame frame)</p> <p> &nbsp; at System.Management.Automation.Interpreter.LightLambda.RunVoid1[T0](T0 arg0)</p> <p> &nbsp; at System.Management.Automation.PSScriptCmdlet.RunClause(Action`1 clause, Object dollarUnderbar, Object</p> <p>inputToProcess)</p> <p> &nbsp; at System.Management.Automation.PSScriptCmdlet.DoEndProcessing()</p> <p> &nbsp; at System.Management.Automation.CommandProcessorBase.Complete()</p> <p>At C:\Users\christopher\Desktop\SCORCH IPs\SCORCH IPs\Install-IP.ps1:355 char:1</p> <p>+ Install-SCOIntegrationPack –handle $conn –filename &quot;$path\SC2012R2_Integration_P ...</p> <p>+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~</p> <p> &nbsp; &nbsp;+ CategoryInfo &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;: NotSpecified: (:) [Write-Error], WriteErrorException</p> <p> &nbsp; &nbsp;+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Install-SCOIntegrationPack</p>

  • <p>Christopher, not sure if you sorted this or not, but do this to resolve the issue:</p> <p>On line 6, replace = @&quot; with = @&#39;</p> <p>NOTE: Single quotation</p> <p>On line 49, again, single quotation, replace &quot;@ with &#39;@</p> <p>Sorted it for me.</p>

  • <p>Robert, thanks for this!!</p> <p>Koz</p>

  • I've added some information concerning the automated import at Robert's other article (<a href="http://blogs.technet.com/b/orchestrator/archive/2012/05/23/understanding-ip-installation-what-does-register-unregister-deploy-undeploy-really-mean.aspx">http://blogs.technet.com/b/orchestrator/archive/2012/05/23/understanding-ip-installation-what-does-register-unregister-deploy-undeploy-really-mean.aspx</a>).<br/>At least from what I've experienced, the script might need some minor adjustments, so if someone has problems with the import of some IPs, check if this information helps.