Custom Resource Providers in Windows Azure Pack - Extending the Hello World Sample calling a SMA Runbook

Today we have a guest blogger! Torsten Kuhn, Principal Consultant from Microsoft Consulting Services in Germany will walk us through the process of extending the Hello World Custom Resource Provider to call a SMA Runbook!

Hello readers I'm Torsten Kuhn a Principal Consultant from Microsoft Services and in this post I will walk you through the different steps required to programmatically extend the Hello World custom Resource Provider sample that is included in the Windows Azure Pack Developers Kit. This post is based on the Azure Pack example resource provider 'Hello World'. In this blog post I'll show all the necessary steps to extend the 'Hello World' example from the UI to the backend REST API with custom code.

Dear readers! – You should be familiar with: Visual Studio, REST API's, JavaScript and Compile, Compile, Compile… J

This blog post goes in line with the post from Victor Arzate that describes what Resource Providers are and how you can deploy the Hello World sample. You can learn more about the Hello World sample here and also, you can learn more about custom resource providers here.

Prerequisites

  • Visual Studio 2012 installed (in my case I had Update 4 applied as well). Note that Visual Studio 2012 does not need to be installed in the same machine where you have Windows Azure Pack (WAP) installed.
  • Windows Installer XML (WiX) toolset 3.7.1224.0 installed (direct download: https://wixtoolset.org/releases/feed/v3.7)
  • A Windows Azure Pack environment up & running.
  • Hello World Custom Resource Provider deployed as described in the blog post from Victor Arzate here.
  • Service Management Automation (SMA) from the System Center 2012 R2 Orchestrator DVD a trial version could be downloaded here. This blog uses the "Sample-Using-RunbookParameters" runbook that is included in the SMA extension of the WAP Admin Site.

This blog explains the following steps to extend the Hello World Sample with a call to a SMA Runbook:

Step 1 - Create the backend REST API

Step 2 - Implement the call to the SMA Runbook

Step 3 - Changes to the Hello World API client

Step 4 - Call into the REST API from the MVC controller of the tenant site

Step 5 - Implement the UI logic that triggers the call to the backend service

 

Step 1 – Create the backend REST API

Launch Visual Studio 2012 and open the Hello World Solution located in the folder where you extracted the sample archive (in my case C:\Projects\Microsoft.WAP.Samples.HelloWorld\ Microsoft.WAP.Samples.HelloWorld.sln). Navigate to the Microsoft.WAP.Samples.HelloWorld.Api project. Open the WebApiConfig.cs located in the App_Start folder and add the following lines:

config.Routes.MapHttpRoute(

name: "ExecuteRunbook",

routeTemplate: "subscriptions/{subscriptionId}/executerunbook",

defaults: new { controller = "FileShare" });

This will route the ExecuteRunbook call to the FileShareController. Go to the FileShareController.cs and add the following empty function stub into the FileShareController class (in step 2 we will implement the call to the REST API):

[HttpPost]

[System.Web.Http.ActionName("executerunbook")]

public void ExecuteRunbook(string subscriptionId, RunbookParameter runbookParameters)

{

 

}

We will pass the runbook parameters in a custom RunbookParameter class. In order to create this class navigate to the Microsoft.WAP.Samples.HelloWorld.ApiClient project select the DataContracts folder, add a new class named RunbookParameter and fill in the following code:

using System;

using System.Runtime.Serialization;

 

namespace Microsoft.WindowsAzurePack.Samples.HelloWorld.ApiClient.DataContracts

{

[DataContract(Namespace = Constants.DataContractNamespaces.Default)]

public class RunbookParameter

{

[DataMember(Order = 0)]

public string Name { get; set; }

[DataMember(Order = 1)]

public int Number { get; set; }

[DataMember(Order = 2)]

public string[] StringArray { get; set; }

[DataMember(Order = 3)]

public DateTime Date { get; set; }

[DataMember(Order = 4)]

public Boolean SayGoodbye { get; set; }

}

}

Make sure that the System.Runtime.Serialization assembly is referenced by the project.

 

Step 2 – Implement the call to the SMA Runbook

Now we will fill in the code to call the SMA Runbook. First we need to add a service reference to the SMA web service we installed in the prerequisites section. Make sure that the Microsoft.WAP.Samples.HelloWorld.Api project is selected and use the "Add Service Reference" command from the projects context menu. Enter the URL to the endpoint of the Orchestrator web site (on my machine https: \\theserver:9090) and add the empty GUID parameter 00000000-0000-0000-0000-000000000000 as displayed in the picture otherwise the call to the service endpoint will fail with a 404 HTTP error. Use SMAWebservice as the namespace:

Open the file FileShareController.cs file in the Controllers subfolder and add the following using statements:

using System;

using System.Collections.Generic;

using System.Globalization;

using System.Data.Services.Client;

using System.Net;

using System.Net.Security;

using System.Threading.Tasks;

using Microsoft.WindowsAzurePack.Samples.HelloWorld.Api.SMAWebService;

using IO = System.IO;

using System.Linq;

using System.Web.Http;

using Microsoft.WindowsAzurePack.Samples.HelloWorld.ApiClient.DataContracts;

Then navigate to the ExecuteRunbook empty function that we created in step 1 and replace it with the following code:

[HttpPost]

[System.Web.Http.ActionName("executerunbook")]

public void ExecuteRunbook(string subscriptionId, RunbookParameter rbParameter)

{

var api = new OrchestratorApi(new Uri("https://theserver:9090//00000000-0000-0000-0000-000000000000"));

((DataServiceContext)api).Credentials = CredentialCache.DefaultCredentials;

ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });

 

var runbook = api.Runbooks.Where(r => r.RunbookName == "Sample-Using-RunbookParameters").AsEnumerable().FirstOrDefault();

if (runbook == null) return;

 

var runbookParams = new List<NameValuePair>

{

new NameValuePair() {Name = "Name", Value = rbParameter.Name},

new NameValuePair() {Name = "Number", Value = rbParameter.Number.ToString(CultureInfo.InvariantCulture)},

new NameValuePair() {Name = "StringArray", Value = string.Join(",", rbParameter.StringArray)},

new NameValuePair() {Name = "Date", Value = rbParameter.Date.ToLongDateString()},

new NameValuePair() {Name = "SayGoodbye", Value = rbParameter.SayGoodbye.ToString(CultureInfo.InvariantCulture)}

};

 

OperationParameter operationParameters = new BodyOperationParameter("parameters", runbookParams);

var uriSma = new Uri(    string.Concat(api.Runbooks, string.Format("(guid'{0}')/{1}", runbook.RunbookID, "Start")),UriKind.Absolute);

var jobIdValue = api.Execute<Guid>(uriSma, "POST", true, operationParameters) as QueryOperationResponse<Guid>;

if (jobIdValue == null) return;

 

var jobId = jobIdValue.Single();

Task.Factory.StartNew(() => QueryJobCompletion(jobId));

}

 

private void QueryJobCompletion(Guid jobId)

{

 

}

Please note that in the last line in I started a new background task with a delegate to QueryJobCompletion. This function queries for job completion and handles the status update for the called runbook. In a future blog I will explain the different options we have to give feedback from long running backend operations back to the WAP portal user.

 

Step 3 – Changes to the HelloWorld API client

The HelloWorld API client (Microsoft.WAP.Samples.HelloWorld.ApiClient) hides all the complexity when calling the backend REST API's. The Tenant site MVC controller only makes simple function calls to the client API and then the client creates the target Uri's and makes the necessary HTTP PUT and GET calls to the backend, transforms the results in a way that makes it easy for the UI part of the portal to use them. Navigate to the Microsoft.WAP.Samples.HelloWorld.ApiClient project and open the HellworldClient.cs file. Add the following constant as the URI template for the new REST API at the top off HelloWorldClient class:

private const string TenantExecuteRunbook = "{0}/" + RegisteredPath + "/executerunbook";

Then add the following function to the same class:

public async Task ExecuteRunbook(string subscriptionId, RunbookParameter rb)

{    

var requestUrl =

this.CreateRequestUri(

string.Format(CultureInfo.InvariantCulture,

TenantExecuteRunbook, subscriptionId));

await this.PostAsync<RunbookParameter>(requestUrl, rb);

}

 

 

Step 4 – Call into the REST API from the MVC controller of the tenant site

Now we will add the new HelloWorldClient API call to the Tenant site's HelloWorldTenantController. To do so navigate to the Microsoft.WAP.Samples.HelloWorld.TenantExtension project and open the Models subfolder. Create a new RunbookParameterModel class that is used to pass the parameter values for the REST API from the UI to the HelloWorldTenantController and add the following code fragment:

using Microsoft.WindowsAzurePack.Samples.HelloWorld.ApiClient.DataContracts;

public class RunbookParameterModel

{

public string Name { get; set; }

public int Number { get; set; }

public string[] StringArray { get; set; }

public DateTime Date { get; set; }

public Boolean SayGoodbye { get; set; }

 

public RunbookParameter ToApiObject()

{

return new RunbookParameter()

{

Name = this.Name,

Number = this.Number,

StringArray = this.StringArray,

Date = this.Date,

SayGoodbye = this.SayGoodbye

};

}

}

 

Open the HelloWorldTenantController.cs and add the following method to the existing code:

[HttpPost]

[ActionName("ExecuteRunbook")]

public async Task<JsonResult> ExecuteRunbook(string subscriptionId,RunbookParameterModel rb)

{

await ClientFactory.HelloWorldClient.ExecuteRunbook(subscriptionId,rb.ToApiObject());

return this.Json("Success");

}

 

 

Step 5 – Implement the UI logic that triggers the call to the backend service

The last piece of the puzzle is the JavaScript code we need to trigger the call to the ExcecuteRunbook API from the user interface. For simplicity reasons we make the call from a button event handler, the parameter values are passed as constant values. In one of my next blogs I will show you how to collect the parameter values from a wizard based UI.

  • Open the HelloWorldTenant.Controller.js located in the TenantExtension\Content\Scripts subfolder and add the following function:

function executeRunbook(subscriptionId, name, number, stringArray, date, sayGoodbye) {

return Shell.Net.ajaxPost({

data: {

subscriptionId: subscriptionId,

Name: name,

Number: number,

StringArray: stringArray,

Date: date,

SayGoodbye: sayGoodbye

},

url: baseUrl + "/ExecuteRunbook"

});

}

 

  • In order to make the function accessible from the file share tab navigate to the bottom of the controller file and add the following line:

global.HelloWorldTenantExtension = global.HelloWorldTenantExtension || {};

global.HelloWorldTenantExtension.Controller = {

createFileShare: createFileShare,

listFileSharesUrl: listFileSharesUrl,

getFileShares: getFileShares,

getfileSharesData: getfileSharesData,

navigateToListView: navigateToListView,

executeRunbook: executeRunbook

};

 

  • Open the HellworldTenant.fileSharesTab.js file and navigate to the updateContextualCommands function, add the following line:

Exp.UI.Commands.Contextual.add(new Exp.UI.Command("Execute", "Execute",Exp.UI.CommandIconDescriptor.getWellKnown("Start"), true, null,onExecuteRunbook));

 

  • In the same file add the following command handler:

function onExecuteRunbook() {

var subscriptionId =

    global.Exp.Rdfe.getSubscriptionsRegisteredToService("helloworld")[0].id,

name = "test",

number = 100,

stringArray = new Array("value1", "value2", "value3"),

date = new Date(),

sayGoodbye = true;

 

var promise = HelloWorldTenantExtension.Controller.executeRunbook(subscriptionId, name,

            number, stringArray, date, sayGoodbye);

global.waz.interaction.showProgress(

promise,

{

initialText: "Executing runbook...",

successText: "Runbook launched successfully.",

failureText: "Failed to execute runbook."

}

);

promise.done(function () {

});

}

 

Now compile the solution. As a result you will get a new version of the MSI setup for the HelloWorld sample. If you followed all instruction in Victor Arzate's blog you have already an installed and registered version of HelloWorld. Make sure to uninstall the existing version in control panel and install the new one but don't touch the extension registration. Login to the Azure Pack Tenant Site and navigate to the HelloWorld extension:

You see your new Execute button and can now launch the runbook.

 

Until next time! Torsten