Sitecore Stuff and Such

- by Christian Kay Linkhusen

NAVIGATION - SEARCH

Run Unicorn Sync from Octopus Deploying to Azure With PowerShell Script

In a setup where you deploy your web applications to Azure with Octopus, you’ll find out, when you execute an Azure PowerShell Script the context you execute the script is in the scope of the Octopus Server. This causes trouble when you try to execute a synchronization of Unicorn from PowerShell as described on GitHub Unicorn for Sitecore – PowerShell Remote Scripting.

The reason to this is that, when you execute your PowerShell script in the Azure PowerShell Script Step, you are not able to load the MicroCHAP.dll as it is not present on the Octopus Server. The solution to get access to the local resources on the Azure WebApp is thru the Kudu API.

A setup in Octopus Deploying a Sitecore solution with a final step that executes a Unicorn Sync could look like this in Octopus:
  1. Provision Sitecore to Azure if Sitecore is not present
  2. Stop the Content Editing Site
  3. Cleanup before deploying the application
  4. Deployment of the application itself
  5. Start the Content Editing Site
  6. Hold on a moment for the CM to get alive
  7. Sync your Unicorn stuff
The last step executes a PowerShell made with a strong inspiration from Kam’s example of executing the sync from a remote script.

    function Get-AzureRmWebAppPublishingCredentials($resourceGroupName, $webAppName, $slotName = $null){
	if ([string]::IsNullOrWhiteSpace($slotName)){
		$resourceType = "Microsoft.Web/sites/config"
		$resourceName = "$webAppName/publishingcredentials"
	}
	else{
		$resourceType = "Microsoft.Web/sites/slots/config"
		$resourceName = "$webAppName/$slotName/publishingcredentials"
	}
   
	$publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $resourceGroupName -ResourceType $resourceType -ResourceName $resourceName -Action list -ApiVersion 2015-08-01 -Force
    return $publishingCredentials
}

function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $webAppName, $slotName = $null){
    $publishingCredentials = Get-AzureRmWebAppPublishingCredentials $resourceGroupName $webAppName $slotName
    return ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword))))
}
function Get-KuduApiUrl($webAppName, $slotName = "", $kuduPath){
    if ($slotName -eq ""){
        $kuduApiUrl = "https://$webAppName.scm.azurewebsites.net/api/vfs/site/wwwroot/$kuduPath"
    }
    else{
        $kuduApiUrl = "https://$webAppName`-$slotName.scm.azurewebsites.net/api/vfs/site/wwwroot/$kuduPath"
    }
    $virtualPath = $kuduApiUrl.Replace(".scm.azurewebsites.", ".azurewebsites.").Replace("/api/vfs/site/wwwroot", "")

    [hashtable]$Return = @{} 
    $Return.VirtualPath = $virtualPath
    $Return.KuduApiUrl = $kuduApiUrl

    return $Return
}

function Download-FileFromWebApp($resourceGroupName, $webAppName, $slotName = "", $kuduPath, $localPath){
    $kuduApiAuthorisationToken = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName
    $kuduApiObj = Get-KuduApiUrl $webAppName $slotName $kuduPath
   
    Write-Host "Downloading File from WebApp. Source: '$($kuduApiObj.VirtualPath)'. Target: '$localPath'..." -ForegroundColor DarkGray

    Invoke-RestMethod -Uri $kuduApiObj.KuduApiUrl `
                        -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
                        -Method GET `
                        -OutFile $localPath `
                        -ContentType "multipart/form-data" `
                        -ErrorAction SilentlyContinue
}
$ScriptPath = Split-Path $MyInvocation.MyCommand.Path

function Sync-Unicorn {
	param(
		[Parameter(Mandatory=$TRUE)]
		[string]$ControlPanelUrl,
		[Parameter(Mandatory=$TRUE)]
		[string]$SharedSecret,
		[string[]]$Configurations,
		[string]$Verb = 'Sync',
		[switch]$SkipTransparentConfigs,
		[switch]$DebugSecurity
	)

	# PARSE THE URL TO REQUEST
	$parsedConfigurations = '' # blank/default = all
	
	if($Configurations) {
		$parsedConfigurations = ($Configurations) -join "^"
	}

	$skipValue = 0
	if($SkipTransparentConfigs) {
		$skipValue = 1
	}

	$url = "{0}?verb={1}&configuration={2}&skipTransparentConfigs={3}" -f $ControlPanelUrl, $Verb, $parsedConfigurations, $skipValue 

	if($DebugSecurity) {
		Write-Host "Sync-Unicorn: Preparing authorization for $url"
	}

	# GET AN AUTH CHALLENGE
	$challenge = Get-Challenge -ControlPanelUrl $ControlPanelUrl

	if($DebugSecurity) {
		Write-Host "Sync-Unicorn: Received challenge from remote server: $challenge"
	}

	# CREATE A SIGNATURE WITH THE SHARED SECRET AND CHALLENGE
	$signatureService = New-Object MicroCHAP.SignatureService -ArgumentList $SharedSecret

	$signature = $signatureService.CreateSignature($challenge, $url, $null)

	if($DebugSecurity) {
		Write-Host "Sync-Unicorn: MAC '$($signature.SignatureSource)'"
		Write-Host "Sync-Unicorn: HMAC '$($signature.SignatureHash)'"
		Write-Host "Sync-Unicorn: If you get authorization failures compare the values above to the Sitecore logs."
	}

	Write-Host "Sync-Unicorn: Executing $Verb..."

	# USING THE SIGNATURE, EXECUTE UNICORN
	[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
	$result = Invoke-StreamingWebRequest -Uri $url -Mac $signature.SignatureHash -Nonce $challenge

	if($result.TrimEnd().EndsWith('****ERROR OCCURRED****')) {
		throw "Unicorn $Verb to $url returned an error. See the preceding log for details."
	}

	# Uncomment this if you want the console results to be returned by the function
	# $result
}

function Get-Challenge {
	param(
		[Parameter(Mandatory=$TRUE)]
		[string]$ControlPanelUrl
	)

	$url = "$($ControlPanelUrl)?verb=Challenge"

	[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
	$result = Invoke-WebRequest -Uri $url -TimeoutSec 360 -UseBasicParsing

	$result.Content
}

function Invoke-StreamingWebRequest($Uri, $MAC, $Nonce) {
	$responseText = new-object -TypeName "System.Text.StringBuilder"

	$request = [System.Net.WebRequest]::Create($Uri)
	$request.Headers["X-MC-MAC"] = $MAC
	$request.Headers["X-MC-Nonce"] = $Nonce
	$request.Timeout = 10800000

	$response = $request.GetResponse()
	$responseStream = $response.GetResponseStream()
	$responseStreamReader = new-object System.IO.StreamReader $responseStream
	
	while(-not $responseStreamReader.EndOfStream) {
		$line = $responseStreamReader.ReadLine()

		if($line.StartsWith('Error:')) {
			Write-Host $line.Substring(7) -ForegroundColor Red
		}
		elseif($line.StartsWith('Warning:')) {
			Write-Host $line.Substring(9) -ForegroundColor Yellow
		}
		elseif($line.StartsWith('Debug:')) {
			Write-Host $line.Substring(7) -ForegroundColor Gray
		}
		elseif($line.StartsWith('Info:')) {
			Write-Host $line.Substring(6) -ForegroundColor White
		}
		else {
			Write-Host $line -ForegroundColor White
		}
		[void]$responseText.AppendLine($line)
	}
	return $responseText.ToString()
}

. .\AuthenticateWithServicePrincipal.ps1
Authenticate
Set-AzureRMContext -SubscriptionId $subscriptionId

Download-FileFromWebApp $resourceGroup $OctopusParameters["Octopus.Action.Azure.WebAppName"] "" "bin/MicroCHAP.dll" "$($OctopusParameters["env:OctopusCalamariWorkingDirectory"])\MicroCHAP.dll"
Add-Type -Path "$($OctopusParameters["env:OctopusCalamariWorkingDirectory"])\MicroCHAP.dll"

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Sync-Unicorn -ControlPanelUrl "https://$($OctopusParameters["cmSiteHostName"])/unicorn.aspx" -SharedSecret $OctopusParameters["Unicorn.SharedSecret"]