Mike Skutta Blog

Sitecore Update and Zip Package Installer Web Service

2015-09-24
Mike Skutta

Overview

I am in the process of automating Sitecore deployments using Octopus Deploy. As part of this process, I need to install .update packages that are generated by TDS and also .zip packages generated by developers. Out of the box, Sitecore provides a way to manually upload these packages through the backend GUI. They also provide an API to upload the packages.

Octopus Deploy has the ability to run PowerShell scripts as part of its deployment process. I need a way to install the packages through a PowerShell script. One way to achieve this is to create a custom web service that uses the Sitecore API to install the packages. The PowerShell script can then call the web service. Using a blocking web service, Octopus Deploy will wait until the installation of the packages completes.

Implementation

Hedgehog Development, the developers of TDS, created an UpdatePackageInstaller github repository. This repository contains example code of how to install .update packages via the API. I used the example provided in TdsPackageInstaller.cs as a starting point.

I also found the Exporting and Importing packages from Sitecore through Code using Sitecore API by Akshay Sura very helpful. I used this as a starting point to import .zip packages.

Putting both examples together, I created a web service that supports installing .update and .zip packages.

<%@ WebService Language="C#" Class="PackageInstaller" %>
using System;
using System.Configuration;
using System.IO;
using System.Web.Services;
using System.Xml;
using Sitecore.Data.Proxies;
using Sitecore.Data.Engines;
using Sitecore.Install.Files;
using Sitecore.Install.Framework;
using Sitecore.Install.Items;
using Sitecore.SecurityModel;
using Sitecore.Update;
using Sitecore.Update.Installer;
using Sitecore.Update.Installer.Utils;
using Sitecore.Update.Utils;
using log4net;
using log4net.Config;

/// <summary>
/// Summary description for UpdatePackageInstaller
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
// [System.Web.Script.Services.ScriptService]
public class PackageInstaller : System.Web.Services.WebService
{
  /// <summary>
  /// Installs a Sitecore Update Package.
  /// </summary>
  /// <param name="path">A path to a package that is reachable by the web server</param>
  [WebMethod(Description = "Installs a Sitecore Update Package.")]
  public void InstallUpdatePackage(string path)
  {
    // Use default logger
    var log = LogManager.GetLogger("root");
    XmlConfigurator.Configure((XmlElement)ConfigurationManager.GetSection("log4net"));

    var file = new FileInfo(path);  
    if (!file.Exists)  
      throw new ApplicationException(string.Format("Cannot access path '{0}'.", path)); 
        
    using (new SecurityDisabler())
    {
      var installer = new DiffInstaller(UpgradeAction.Upgrade);
      var view = UpdateHelper.LoadMetadata(path);

      //Get the package entries
      bool hasPostAction;
      string historyPath;
      var entries = installer.InstallPackage(path, InstallMode.Install, log, out hasPostAction, out historyPath);

      installer.ExecutePostInstallationInstructions(path, historyPath, InstallMode.Install, view, log, ref entries);

      UpdateHelper.SaveInstallationMessages(entries, historyPath);
    }
  }
	
  /// <summary>
  /// Installs a Sitecore Zip Package.
  /// </summary>
  /// <param name="path">A path to a package that is reachable by the web server</param>
  [WebMethod(Description = "Installs a Sitecore Zip Package.")]
  public void InstallZipPackage(string path)
  {
    // Use default logger
    var log = LogManager.GetLogger("root");
    XmlConfigurator.Configure((XmlElement)ConfigurationManager.GetSection("log4net"));

    var file = new FileInfo(path);  
    if (!file.Exists)  
      throw new ApplicationException(string.Format("Cannot access path '{0}'.", path)); 
		
    Sitecore.Context.SetActiveSite("shell");  
    using (new SecurityDisabler())  
    {  
      using (new ProxyDisabler())  
      {  
        using (new SyncOperationContext())  
        {  
          IProcessingContext context = new SimpleProcessingContext(); //   
          IItemInstallerEvents events =  
            new DefaultItemInstallerEvents(new Sitecore.Install.Utils.BehaviourOptions(Sitecore.Install.Utils.InstallMode.Overwrite, Sitecore.Install.Utils.MergeMode.Undefined));  
          context.AddAspect(events);  
          IFileInstallerEvents events1 = new DefaultFileInstallerEvents(true);  
          context.AddAspect(events1);  
          var installer = new Sitecore.Install.Installer();  
          installer.InstallPackage(Sitecore.MainUtil.MapPath(path), context);  
        }  
      }  
    }  
  }
}

Usage

All we need to do is drop the PackageInstaller.asmx in Sitecore. From there, we can call it from a PowerShell script.

CAUTION: The PackageInstaller.asmx does not have security restrictions. Anyone that knows about the web service can install both .update and .zip packages as long as the packages are on the file system. For security reasons, I deploy the PackageInstaller.asmx at the beginning of the Octopus Deploy process and remove it once the process is complete.

Here is an example of how you can use PowerShell to call the web service. This example was taken from the Octopus Deploy PostDeploy script. The script automatically installs all .update and .zip packages located at the standard locations in the Sitecore file structure.

Write-Host "PostDeploy"

##################################################################################
#                    VARIABLES
##################################################################################
    
$excludeItemUpdates = [System.Convert]::ToBoolean($OctopusParameters["ExcludeItemUpdates"]) 
$internalPort = $OctopusParameters["InternalPort"]
$rootPath = $OctopusParameters["Octopus.Action.Package.CustomInstallationDirectory"]
$updatePackagesPath = [io.path]::combine($rootPath, 'Website', 'sitecore', 'admin', 'Packages')
$zipPackagesPath = [io.path]::combine($rootPath, 'Data', 'packages')

##################################################################################
#                    INSTALL PACKAGES
##################################################################################
Write-Host "Installing packages..."

$proxy = New-WebServiceProxy -uri "http://localhost:$($internalPort)/packageinstaller.asmx?WSDL"
$proxy.Timeout = 1800000

Write-Host "-> Installing .update packages located at: $updatePackagesPath"
Get-ChildItem $updatePackagesPath -Filter *.update | `
Foreach-Object{
	if ($excludeItemUpdates -and $_.FullName -like "*.scitems.*") {
		Write-Host "-> Skipping package" + $_.FullName
	} else {
		Write-Host "-> Installing package" + $_.FullName
		$proxy.InstallUpdatePackage($_.FullName)
	}
}

Write-Host "-> Installing .zip packages located at: $zipPackagesPath"
Get-ChildItem $zipPackagesPath -Filter *.zip | `
Foreach-Object{
	Write-Host "-> Installing package" + $_.FullName
	$proxy.InstallZipPackage($_.FullName)
}

Similar Posts

Comments