Custom Resource Providers in Windows Azure Pack – Moving from Hello World to your own Resource Provider

Today we have again Torsten Kuhn as a guest blogger! Torsten is a Principal Consultant from Microsoft Consulting Services in Germany and he will walk us through the process of moving from the Hello World Custom Resource Provider to your own Resource Provider in Windows Azure Pack!

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 move the Hello World custom Resource Provider sample that is included in the Windows Azure Pack Developers Kit to your own resource provider. This post is based on the Windows Azure Pack example resource provider 'Hello World'.

Dear readers! – This blog is intended for developers who want to write their own custom resource provider based on the Hello World sample. The blog is very long but be patient as a result of implementing all steps you will get a good starting point for your own provider!

This blog post goes in line with the series of posts that Victor Arzate and I have been blogging about Resource Providers in Windows Azure Pack:

You can learn more about the Hello World sample here and also, you can learn more about custom resource providers here. "Hello World" is a good starting point to understand the design and components of a custom resource provider but if you want to create your own provider you'll have to do a lot of changes to get rid of the famous words "Hello World".

The idea to write a new custom resource provider came up when I read the blog Managing Windows Azure with SMA from Jim Britt and Victor Arzate. It would be great to have a resource provider where you can see the status of all virtual machines running in your Azure subscription and start or stop them all at once hitting a button. The name for the new provider is CloudSlider. I will not implement all the logic necessary to query an Azure subscription for running virtual machines and to start and stop them. Main goal of this blog is to show you how to move from HelloWorld to your own implementation.

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: http://wixtoolset.org/releases/feed/v3.7).
  • A Windows Azure Pack environment up & running.
  • Service Management Automation (SMA) from the System Center 2012 R2 Orchestrator DVD a trial version could be downloaded here.

     

This blog explains the following steps to move from Hello World Sample to CloudSlider:

Step 1 – Rename folders, classes, source files and the service name

Step 2 – Change the Rest API and the APIClient Project

Step 3 – Change the CloudSlider.AdminExtension

Step 4 – Change the CloudSlider.TenantExtension

Step 5 – Change the Setup

Step 6 – Change the registration script

 

Step 1 – Rename folder, classes, source files and the service name

  • Make a copy of the HelloWorld source directory and name the new directory to CloudSlider:

  • Make sure that all files and folders are not read only:

  • Go to the CloudSlider project directory, find the Microsoft.WindowsAzurePack.Samples.HelloWorld.sln solution file and rename it to CloudSlider.sln:

  • Open the CloudSlider.sln file in notepad and replace all occurrences of Microsoft.WindowsAzurePack.Samples.HelloWorld with CloudSlider:

  • Save the file and close notepad. Now navigate to every subfolder in the CloudSlider directory and rename the project files prefix from Microsoft.WindowsAzurePack.Samples.HelloWorld to CloudSlider:

  • Now it's time to launch Visual Studio. Open the CloudSlider Solution and navigate to the ApiClient folder and rename the HelloWorldClient.cs file to CloudSliderClient.cs, confirm that all references to the code element should also be renamed:

 

  • Go to the Controllers subfolder and rename HelloWorldTenantController.cs to CloudSliderTenantController.cs:
  • Do the same with the Controller source file in the AdminExtension/Controllers folder, rename the HelloWorldAdminController.cs to CloudSliderAdminController.
  • As the next action navigate to the Api/Controllers folder and delete the FileServersController.cs and ProductsController.cs.
  • Rename the FileShareController.cs to VirtualMachineController.cs.

    Make sure that to confirm the renaming of all references to the code elements with "Yes"!

  • Now open the Replace in Files Dialog (CTRL-Shift-H) and replace all occurrences of Microsoft.WindowsAzurePack.Samples.HelloWorld with Microsoft.WindowsAzurePack.Samples.CloudSlider:

  • Next global replace is from HelloWorld to CloudSlider, make sure that file type is a *.* wildcard:

 

  • The last global replace is from "Hello World" to "Cloud Slider", this will change all display strings:

  • Try to build the entire solution. Probably the build of the setup will fail because the assembly names are still the old ones. To fix this open the project properties of each project and change the assembly names to match CloudSlider make sure to change the default namespace in the same dialog:

 

Step 2 – Change the Rest API and the APIClient Project

  • It's time to get rid of project items we no longer need for Cloud Slider. Go to CloudSlider.ApiClient and delete the following files FileServer.cs, FileServerList.cs, Product.cs and Productlist.cs.

  • Rename FileShareList.cs to VirtualMachineList.cs and confirm the renaming of all references:


  • Add some new member to VirtualMachine class:

using System;

using System.Runtime.Serialization;

 

namespace Microsoft.WindowsAzurePack.Samples.CloudSlider.ApiClient.DataContracts

{


///
<summary>


/// This is a data contract class between extensions and resource provider


/// VirtualMachine contains data contract of data which shows up in "Virtual Machines" tab inside CloudSlider Tenant Extension


///
</summary>

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


public
class
VirtualMachine

{


///
<summary>


/// Id of the Virtual Machine


///
</summary>

[DataMember(Order = 1)]


public
string Id { get; set; }

 


///
<summary>


/// Name of the VirtualMachine


///
</summary>

[DataMember(Order = 2)]


public
string Name { get; set; }

 


///
<summary>


/// SubscriptionId


///
</summary>

[DataMember(Order = 3)]


public
string SubscriptionId { get; set; }

 


///
<summary>


/// Network name


///
</summary>

[DataMember(Order = 4)]


public
string NetworkName { get; set; }

 


///
<summary>


/// Status of the Virtual Machine


///
</summary>

[DataMember(Order = 5)]


public
string Status { get; set; }

 


///
<summary>


/// Scheduled start time of the Virtual Machine


///
</summary>

[DataMember(Order = 6)]


public
DateTime StartTime { get; set; }

 


///
<summary>


/// Scheduled stop time of the Virtual Machine


///
</summary>

[DataMember(Order = 7)]


public
DateTime StopTime { get; set; }

 


///
<summary>


/// Gets or sets the extension data.


///
</summary>


public
ExtensionDataObject ExtensionData { get; set; }

}

} }

  • VirtualMachineList.cs should look like this:

using System.Collections.Generic;

using System.Runtime.Serialization;

 

namespace Microsoft.WindowsAzurePack.Samples.CloudSlider.ApiClient.DataContracts

{

[CollectionDataContract(Name = "VirtualMachines", ItemName = "VirtualMachine", Namespace = Constants.DataContractNamespaces.Default)]


public
class
VirtualMachineList : List<VirtualMachine>, IExtensibleDataObject

{


///
<summary>


/// Gets or sets the structure that contains extra data.


///
</summary>


public
ExtensionDataObject ExtensionData { get; set; }

}

}

  • Change the Source Code of VirtualMachineController.cs to get a sample list of virtual machines and execute runbooks (based on my first blog post):


//————————————————————

// Copyright (c) Microsoft Corporation. All rights reserved.

//————————————————————

 

using System;

using System.Collections.Generic;

using System.Data.Services.Client;

using System.Net;

using System.Net.Security;

using System.Threading.Tasks;

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

using System.Linq;

using System.Web.Http;

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

using
RunbookParameter = Microsoft.WindowsAzurePack.Samples.CloudSlider.ApiClient.DataContracts.RunbookParameter;

 

namespace Microsoft.WindowsAzurePack.Samples.CloudSlider.Api.Controllers

{


public
class
VirtualMachineController : ApiController

{


public
static
List<VirtualMachine> VirtualMachines = new
List<VirtualMachine>();

 


///
<summary>


/// This function calls SMA runbooks used to


/// start or stop virtual machines.


///
</summary>


///
<param name="subscriptionId"></param>


///
<param name="rbParameter"></param>

[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 == rbParameter.RunbookName).AsEnumerable().FirstOrDefault();


if (runbook == null) return;

 


var runbookParams = new
List<NameValuePair>

{


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

};

 


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)

{

 

}

 


///
<summary>


/// This function returns only a list of sample VM's.


/// In a real world implementation we would query


/// the service management API or execute an


/// Azure powershell script to get all VM's and their status.


///
</summary>


///
<param name="subscriptionId"></param>


///
<returns></returns>

[HttpGet]

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


public
List<VirtualMachine> ListVirtualMachines(string subscriptionId)

{


if (string.IsNullOrWhiteSpace(subscriptionId))

{


throw
new
ArgumentNullException(subscriptionId);

}

 


if (VirtualMachines.Count == 0)

{

PopulateVirtualMachinesForSubscription(subscriptionId);

}

 


var vms = from vm in VirtualMachines


where
string.Equals(vm.SubscriptionId, subscriptionId, StringComparison.OrdinalIgnoreCase)


select vm;

 


return vms.ToList();

}

 


internal
static
void PopulateVirtualMachinesForSubscription(string subscriptionId)

{


// Create some sample data

VirtualMachines.Add(


new
VirtualMachine

{

Id = Guid.NewGuid().ToString("D"),

Name = "Contoso_DC",

NetworkName = "Contoso_NET",

SubscriptionId = subscriptionId,

Status = "Running",

StartTime = DateTime.Parse("08:00:00"),

StopTime = DateTime.Parse("22:00:00")

}

);

VirtualMachines.Add(


new
VirtualMachine

{

Id = Guid.NewGuid().ToString("D"),

Name = "Contoso_SQL",

NetworkName = "Contoso_NET",

SubscriptionId = subscriptionId,

Status = "Stopped",

StartTime = DateTime.Parse("08:00:00"),

StopTime = DateTime.Parse("22:00:00")

}

);

VirtualMachines.Add(


new
VirtualMachine

{

Id = Guid.NewGuid().ToString("D"),

Name = "Contoso_WTS",

NetworkName = "Contoso_NET",

SubscriptionId = subscriptionId,

Status = "Running",

StartTime = DateTime.Parse("08:00:00"),

StopTime = DateTime.Parse("22:00:00")

}

);

VirtualMachines.Add(


new
VirtualMachine

{

Id = Guid.NewGuid().ToString("D"),

Name = "Contoso_PRINT",

NetworkName = "Contoso_NET",

SubscriptionId = subscriptionId,

Status = "Stopped",

StartTime = DateTime.Parse("08:00:00"),

StopTime = DateTime.Parse("22:00:00")

}

);

VirtualMachines.Add(


new
VirtualMachine

{

Id = Guid.NewGuid().ToString("D"),

Name = "Contoso_FS",

NetworkName = "Contoso_NET",

SubscriptionId = subscriptionId,

Status = "Unknown",

StartTime = DateTime.Parse("08:00:00"),

StopTime = DateTime.Parse("22:00:00")

}

);

VirtualMachines.Add(


new
VirtualMachine

{

Id = Guid.NewGuid().ToString("D"),

Name = "Contoso_APPSVC",

NetworkName = "Contoso_NET",

SubscriptionId = subscriptionId,

Status = "Starting",

StartTime = DateTime.Parse("08:00:00"),

StopTime = DateTime.Parse("22:00:00")

}

);

 

}

}

}

  • Navigate to the App_Start folder in the same project and change the WebApiConfig.cs:


public
static
void Register(HttpConfiguration config)

{

config.Routes.MapHttpRoute(

name: "ExecuteRunbook",

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

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

 

config.Routes.MapHttpRoute(

name: "VirtualMachines",

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

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

 

config.Routes.MapHttpRoute(

name: "AdminSettings",

routeTemplate: "admin/settings",

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

 

config.Routes.MapHttpRoute(

name: "CloudSliderQuota",

routeTemplate: "admin/quota",

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

 

config.Routes.MapHttpRoute(

name: "CloudSliderDefaultQuota",

routeTemplate: "admin/defaultquota",

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

 

config.Routes.MapHttpRoute(

name: "Subscription",

routeTemplate: "admin/subscriptions",

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

}

  • Modify the CloudClient.cs in the CloudSlider.ApiClient project in the following way:


public
class
CloudSliderClient

{


public
const
string RegisteredServiceName = "CloudSlider";


public
const
string RegisteredPath = "services/" + RegisteredServiceName;


public
const
string AdminSettings = RegisteredPath + "/settings";


public
const
string TenantVirtualMachines = "{0}/" + RegisteredPath + "/virtualmachines";


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

 


public
Uri BaseEndpoint { get; set; }


public
HttpClient HttpClient;

 


///
<summary>


/// This constructor takes BearerMessageProcessingHandler which reads token as attach to each request


///
</summary>


///
<param name="baseEndpoint"></param>


///
<param name="handler"></param>


public CloudSliderClient(Uri baseEndpoint, MessageProcessingHandler handler)

{


if (baseEndpoint == null)

{


throw
new
ArgumentNullException("baseEndpoint");

}

 


this.BaseEndpoint = baseEndpoint;


this.HttpClient = new
HttpClient(handler);

}

 


public CloudSliderClient(Uri baseEndpoint, string bearerToken, TimeSpan? timeout = null)

{


if (baseEndpoint == null)

{


throw
new
ArgumentNullException("baseEndpoint");

}

 


this.BaseEndpoint = baseEndpoint;

 


this.HttpClient = new
HttpClient();

HttpClient.DefaultRequestHeaders.Authorization = new
AuthenticationHeaderValue("Bearer", bearerToken);

 


if (timeout.HasValue)

{


this.HttpClient.Timeout = timeout.Value;

}

}

 

#region Admin APIs


///
<summary>


/// GetAdminSettings returns Cloud Slider Resource Provider endpoint information if its registered with Admin API


///
</summary>


///
<returns></returns>


public
async
Task<AdminSettings> GetAdminSettingsAsync()

{


var requestUrl = this.CreateRequestUri(CloudSliderClient.AdminSettings);

 


var response = await
this.HttpClient.GetAsync(requestUrl, HttpCompletionOption.ResponseContentRead);

response.EnsureSuccessStatusCode();


return
await response.Content.ReadAsAsync<AdminSettings>();

}

 


///
<summary>


/// UpdateAdminSettings registers Cloud Slider Resource Provider endpoint information with Admin API


///
</summary>


///
<returns></returns>


public
async
Task UpdateAdminSettingsAsync(AdminSettings newSettings)

{


var requestUrl = this.CreateRequestUri(CloudSliderClient.AdminSettings);


var response = await
this.HttpClient.PutAsJsonAsync<AdminSettings>(requestUrl.ToString(), newSettings);

response.EnsureSuccessStatusCode();

}

#endregion

 

#region Tenant APIs


///
<summary>


///


///
</summary>


///
<param name="subscriptionId"></param>


///
<param name="rb"></param>


public
async
Task ExecuteRunbook(string subscriptionId, RunbookParameter rb)

{


var requestUrl =


this.CreateRequestUri(


string.Format(CultureInfo.InvariantCulture,

TenantExecuteRunbook, subscriptionId));


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

}

 


///
<summary>


/// ListVirtualMachinesAsync supposed to return list of virtual machines per subscription stored in CloudSlider Resource Provider


///
</summary>


///
<param name="subscriptionId"></param>


///
<returns></returns>


public
async
Task<List<VirtualMachine>> ListVirtualMachinesAsync(string subscriptionId = null)

{


var requestUrl = this.CreateRequestUri(string.Format(CultureInfo.InvariantCulture, TenantVirtualMachines, subscriptionId));


return
await
this.GetAsync<List<VirtualMachine>>(requestUrl);

}

#endregion

 

#region Private Methods


///
<summary>


/// Common method for making GET calls


///
</summary>


private
async
Task<T> GetAsync<T>(Uri requestUrl)

{


var response = await
this.HttpClient.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead);

response.EnsureSuccessStatusCode();


return
await response.Content.ReadAsAsync<T>();

}

 


///
<summary>


/// Common method for making POST calls


///
</summary>


private
async
Task PostAsync<T>(Uri requestUrl, T content)

{


var response = await
this.HttpClient.PostAsXmlAsync<T>(requestUrl.ToString(), content);

response.EnsureSuccessStatusCode();

}

 


///
<summary>


/// Common method for making PUT calls


///
</summary>


private
async
Task PutAsync<T>(Uri requestUrl, T content)

{


var response = await
this.HttpClient.PutAsJsonAsync<T>(requestUrl.ToString(), content);

response.EnsureSuccessStatusCode();

}

 


///
<summary>


/// Common method for making Request Uri's


///
</summary>


private
Uri CreateRequestUri(string relativePath, string queryString = "")

{


var endpoint = new
Uri(this.BaseEndpoint, relativePath);


var uriBuilder = new
UriBuilder(endpoint);

uriBuilder.Query = queryString;


return uriBuilder.Uri;

}

#endregion

}

Step 3 – Change the CloudSlider.AdminExtension

  • Navigate to the CloudSlider.AdminExtension project, we will remove all items not needed in the new CloudSlider provider:
    • Open the manifests folder and rename the HelloWorldAdminUIManifest.xml to CloudSliderAdminUIManifest.xml
    • Go to the models folder, remove the ProductModel.cs and FileServerModel.cs
    • Delete the entire Views folder
    • Rename the testteam.png to CloudSider.png
    • Open the Scripts folder and delete HelloWorld.ControlsTab.js, HelloWorld.FileServersTab.js and HelloWorld.ProductsTab.js. Change the prefix of the Controller.js and the SettingsTab.js to match the new CloudSlider name.
    • Go to the Styles folder and delete the HelloWorldControls.css file, rename HelloWorldAdmin.css to CloudSliderAdmin.css
    • In the Templates/tab folder delete FileServersTab.html, FileServerTabEmpty.html, ProductsTab.html, ProductsTabEmpty.html and the ControlsTab.html
    • Remove all code from the CloudSliderAdminController.cs related to File Servers and Products.

Open the CloudSliderAdminUIManfest.xml and remove all outdated items:

<?xml
version="1.0"
encoding="utf-8"?>

<uiManifest>

<!–CloudSlider Management–>

<extension
name="CloudSliderAdminExtension"


baseUri="~/Content/CloudSliderAdmin">

<scripts>

<script
src="~/Scripts/CloudSlider.Controller.js" />

<script
src="~/Scripts/CloudSlider.QuickStartTab.js" />

<script
src="~/Scripts/CloudSlider.SettingsTab.js" />

<script
src="~/CloudSliderAdminExtension.js" />

 

<!–Self registration–>

<script
src="~/extensions.data.js" />

</scripts>

<stylesheets>

<stylesheet
src="~/Styles/CloudSliderAdmin.css"/>

</stylesheets>

<templates>

<template
name="quickStartTab"


src="~/Templates/Tabs/QuickStartTab.html" />

<template
name="quickStartTabContent"


src="~/Templates/Tabs/QuickStartTabContent.html" />

<template
name="settingsTab"


src="~/Templates/Tabs/SettingsTab.html" />

<template
name="registerEndpoint"


src="~/Templates/Dialogs/RegisterEndpoint.html" />

</templates>

</extension>

</uiManifest>

  • In the CloudSlider.SettingsTab.js fix some wrong control id's – replace all occurrences of "#dm-" with "#hw-".
  • Now we have left only three script files – extensions.data.js

(function (global, undefined) {


"use strict";

 


var extensions = [{

name: "CloudSliderAdminExtension",

displayName: "Cloud Slider",

iconUri: "/Content/CloudSliderAdmin/CloudSliderAdmin.png",

iconShowCount: false,

iconTextOffset: 11,

iconInvertTextColor: true,

displayOrderHint: 51

}];

 

global.Shell.Internal.ExtensionProviders.addLocal(extensions);

})(this);

  • the CloudSlider.AdminExtension.js

/*globals window,jQuery,Shell,Exp,waz*/

 

(function (global, $, undefined) {


"use strict";

 


var resources = [],

CloudSliderExtensionActivationInit,

navigation;

 


function clearCommandBar() {

Exp.UI.Commands.Contextual.clear();

Exp.UI.Commands.Global.clear();

Exp.UI.Commands.update();

}

 


function onApplicationStart() {

Exp.UserSettings.getGlobalUserSetting("Admin-skipQuickStart").then(function (results) {


var setting = results ? results[0] : null;


if (setting && setting.Value) {

global.CloudSliderAdminExtension.settings.skipQuickStart = JSON.parse(setting.Value);

}

});

 

global.CloudSliderAdminExtension.settings.skipQuickStart = false;

}

 


function loadQuickStart(extension, renderArea, renderData) {

clearCommandBar();

global.CloudSliderAdminExtension.QuickStartTab.loadTab(renderData, renderArea);

}

 


function loadSettingsTab(extension, renderArea, renderData) {

global.CloudSliderAdminExtension.SettingsTab.loadTab(renderData, renderArea);

}

 

global.CloudSliderExtension = global.CloudSliderAdminExtension || {};

 

navigation = {

tabs: [

{

id: "quickStart",

displayName: "quickStart",

template: "quickStartTab",

activated: loadQuickStart

},

{

id: "settings",

displayName: "settings",

template: "settingsTab",

activated: loadSettingsTab

}

],

types: [

]

};

 

CloudSliderExtensionActivationInit = function () {


var CloudSliderExtension = $.extend(this, global.CloudSliderAdminExtension);

 

$.extend(CloudSliderExtension, {

displayName: "Cloud Slider",

viewModelUris: [

global.CloudSliderAdminExtension.Controller.adminSettingsUrl

],

menuItems: [],

settings: {

skipQuickStart: true

},

getResources: function () {


return resources;

}

});

 

CloudSliderExtension.onApplicationStart = onApplicationStart;

CloudSliderExtension.setCommands = clearCommandBar();

 

Shell.UI.Pivots.registerExtension(CloudSliderExtension, function () {

Exp.Navigation.initializePivots(this, navigation);

});

 


// Finally activate CloudSliderExtension

$.extend(global.CloudSliderAdminExtension, Shell.Extensions.activate(CloudSliderExtension));

};

 

Shell.Namespace.define("CloudSliderAdminExtension", {

init: CloudSliderExtensionActivationInit

});

 

})(this, jQuery, Shell, Exp);

  • and from the CloudSlider.Controller.js we only remove some outdated url's

/*globals window,jQuery,cdm, CloudSliderAdminExtension*/

(function ($, global, undefined) {


"use strict";

 


var baseUrl = "/CloudSliderAdmin",

adminSettingsUrl = baseUrl + "/AdminSettings";

 


function makeAjaxCall(url, data) {


return Shell.Net.ajaxPost({

url: url,

data: data

});

}

 


function updateAdminSettings(newSettings) {


return makeAjaxCall(baseUrl + "/UpdateAdminSettings", newSettings);

}

 


function invalidateAdminSettingsCache() {


return global.Exp.Data.getData({

url: global.CloudSliderAdminExtension.Controller.adminSettingsUrl,

dataSetName: CloudSliderAdminExtension.Controller.adminSettingsUrl,

forceCacheRefresh: true

});

}

 


function getCurrentAdminSettings() {


return makeAjaxCall(global.CloudSliderAdminExtension.Controller.adminSettingsUrl);

}

 


function isResourceProviderRegistered() {

global.Shell.UI.Spinner.show();

global.CloudSliderAdminExtension.Controller.getCurrentAdminSettings()

.done(function (response) {


if (response && response.data.EndpointAddress) {


return
true;

}


else {


return
false;

}

})

.always(function () {

global.Shell.UI.Spinner.hide();

});

}

 


// Public

global.CloudSliderAdminExtension = global.CloudSliderAdminExtension || {};

global.CloudSliderAdminExtension.Controller = {

adminSettingsUrl: adminSettingsUrl,

updateAdminSettings: updateAdminSettings,

getCurrentAdminSettings: getCurrentAdminSettings,

invalidateAdminSettingsCache: invalidateAdminSettingsCache,

isResourceProviderRegistered: isResourceProviderRegistered

};

})(jQuery, this);

Step 4 – Change the CloudSlider.TenantExtension

  • Go to the Models folder and rename the FileShareModel.cs to VirtualMachineModel.cs. Open the VirtualMachineModel.cs file and change the code as follows:


public
class
VirtualMachineModel

{


///
<summary>


/// Initializes a new instance of the <see cref="VirtualMachineModel" /> class.


///
</summary>


public VirtualMachineModel()

{

}


///
<summary>


/// Initializes a new instance of the <see cref="VirtualMachineModel" /> class.


///
</summary>


///
<param name="virtualmachineFromApi">virtual machine from API.</param>


public VirtualMachineModel(VirtualMachine virtualmachineFromApi)

{


this.Name = virtualmachineFromApi.Name;


this.SubscriptionId = virtualmachineFromApi.SubscriptionId;


this.Id = virtualmachineFromApi.Id;


this.NetworkName = virtualmachineFromApi.NetworkName;


this.Status = virtualmachineFromApi.Status;


this.StartTime = virtualmachineFromApi.StartTime;


this.StopTime = virtualmachineFromApi.StopTime;

}


///
<summary>


/// Convert to the API object.


///
</summary>


///
<returns>The API VirtualMachine data contract.</returns>


public
VirtualMachine ToApiObject()

{


return
new
VirtualMachine()

{

Name = this.Name,

NetworkName = this.NetworkName,

Id = this.Id,

SubscriptionId = this.SubscriptionId,

Status = this.Status,

StartTime = this.StartTime,

StopTime = this.StopTime

};

}


///
<summary>


/// Gets or sets the name.


///
</summary>


public
string Name { get; set; }


///
<summary>


/// Gets or sets the value of the network name


///
</summary>


public
string NetworkName { get; set; }


///
<summary>


/// Status of the virtual machine


///
</summary>


public
string Status { get; set; }


///
<summary>


/// Gets or sets the value of the subscription id


///
</summary>


public
string SubscriptionId { get; set; }


///
<summary>


/// id of the virtual machine


///
</summary>


public
string Id { get; set; }


///
<summary>


/// start time of the VM


///
</summary>


public
DateTime StartTime { get; set; }


///
<summary>


/// stop time of the VM


///
</summary>


public
DateTime StopTime { get; set; }

}

  • Modify the CloudSliderTenantController.cs located in the Controllers folder:

[RequireHttps]

[OutputCache(Location = OutputCacheLocation.None)]

[PortalExceptionHandler]


public
sealed
class
CloudSliderTenantController : ExtensionController

{

[HttpPost]

[ActionName("ExecuteRunbook")]


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

{


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


return
this.Json("Success");

}

 


///
<summary>


/// List virtual machines that belong to a subscription


///
</summary>


///
<param name="subscriptionId">subscription id</param>


///
<returns></returns>

[HttpPost]

[ActionName("VirtualMachines")]


public
async
Task<JsonResult> ListVirtualMachines(string subscriptionId)

{


// Make the requests sequentially for simplicity


var vms = new
List<VirtualMachineModel>();

 


if (string.IsNullOrEmpty(subscriptionId))

{


throw
new
HttpException("Subscription Id not supplied.");

}

 


var vmsFromApi = await
ClientFactory.CloudSliderClient.ListVirtualMachinesAsync(subscriptionId);

vms.AddRange(vmsFromApi.Select(d => new
VirtualMachineModel(d)));

 


return
this.JsonDataSet(vms, namePropertyName: "Name");

}

}

  • Rename all items in the project prefixed with HelloWorld to CloudSlider:

  • Change the CloudSliderTenantExtension.js as follows:

     

///
<reference path="scripts/CloudSliderTenant.controller.js" />

/*globals window,jQuery,Shell, CloudSliderTenantExtension, Exp*/

 

(function ($, global, undefined) {


"use strict";

 


var resources = [],

CloudSliderTenantExtensionActivationInit,

navigation,

serviceName = "CloudSlider";

 


function onNavigateAway() {

Exp.UI.Commands.Contextual.clear();

Exp.UI.Commands.Global.clear();

Exp.UI.Commands.update();

}

 


function loadSettingsTab(extension, renderArea, renderData) {

global.CloudSliderTenantExtension.SettingsTab.loadTab(renderData, renderArea);

}

 


function loadVirtualMachinesTab(extension, renderArea, renderData) {

global.CloudSliderTenantExtension.VirtualMachinesTab.loadTab(renderData, renderArea);

}

 

global.CloudSliderTenantExtension = global.CloudSliderTenantExtension || {};

 

navigation = {

tabs: [

{

id: "virtualMachines",

displayName: "Virtual Machines",

template: "virtualMachinesTab",

activated: loadVirtualMachinesTab

},

{

id: "settings",

displayName: "Settings",

template: "settingsTab",

activated: loadSettingsTab

}

],

types: [

]

};

 

CloudSliderTenantExtensionActivationInit = function () {


var subscriptionRegisteredToService = global.Exp.Rdfe.getSubscriptionsRegisteredToService("CloudSlider"),

CloudSliderExtension = $.extend(this, global.CloudSliderTenantExtension);

 


// Don't activate the extension if user doesn't have a plan that includes the service.


if (subscriptionRegisteredToService.length === 0) {


return
false; // Don't want to activate? Just bail

}

 

$.extend(CloudSliderExtension, {

viewModelUris: [CloudSliderExtension.Controller.listVirtualMachinesUrl],

displayName: "Cloud Slider",

navigationalViewModelUri: {

uri: CloudSliderExtension.Controller.listVirtualMachinesUrl,

ajaxData: function () {


return global.Exp.Rdfe.getSubscriptionIdsRegisteredToService(serviceName)[0].id;

}

},

displayStatus: global.waz.interaction.statusIconHelper(global.CloudSliderTenantExtension.VirtualMachinesTab.statusIcons, "Status"),

menuItems: [

],

getResources: function () {


return resources;

}

});

 

CloudSliderExtension.onNavigateAway = onNavigateAway;

CloudSliderExtension.navigation = navigation;

 

Shell.UI.Pivots.registerExtension(CloudSliderExtension, function () {

Exp.Navigation.initializePivots(this, this.navigation);

});

 


// Finally activate and give "the" CloudSliderExtension the activated extension since a good bit of code depends on it

$.extend(global.CloudSliderTenantExtension, Shell.Extensions.activate(CloudSliderExtension));

};

 

Shell.Namespace.define("CloudSliderTenantExtension", {

serviceName: serviceName,

init: CloudSliderTenantExtensionActivationInit

});

})(jQuery, this);

  • The RunbookParameter class and the RunbookParameterModel class are used to pass parameters to the SMA runbook called from the REST API backend:


public
class
RunbookParameterModel

{


public
string RunbookName { get; set; }


public
string MachineName { get; set; }

 


public
RunbookParameter ToApiObject()

{


return
new
RunbookParameter()

{

RunbookName = this.RunbookName,

MachineName = this.MachineName,

};

}

}

 

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


public
class
RunbookParameter

{

[DataMember(Order = 0)]


public
string RunbookName { get; set; }

[DataMember(Order = 1)]


public
string MachineName { get; set; }

}

  • Change the CloudSliderTenantUIManifest.xml to reflect the rename and delete actions we made in the CloudSlider.TenantExtension project:

<?xml
version="1.0"
encoding="utf-8"?>

<uiManifest>

<!–Hello World–>

<extension
name="CloudSliderTenantExtension"


baseUri="~/Content/CloudSliderTenant">

<scripts>

<script
src="~/Scripts/CloudSliderTenant.Controller.js" />

<script
src="~/Scripts/CloudSliderTenant.VirtualMachinesTab.js" />

<script
src="~/Scripts/CloudSliderTenant.SettingsTab.js" />

<script
src="~/CloudSliderTenantExtension.js" />

 

<!–Self registration–>

<script
src="~/extensions.data.js" />

</scripts>

<stylesheets>

<stylesheet
src="~/Styles/CloudSliderTenant.css"/>

</stylesheets>

<templates>

<template
name="virtualMachinesTab"


src="~/Templates/Tabs/VirtualMachinesTab.html" />

<template
name="virtualMachinesTabEmpty"


src="~/Templates/Tabs/VirtualMachinesTabEmpty.html" />

<template
name="settingsTab"


src="~/Templates/Tabs/SettingsTab.html" />

</templates>

</extension>

</uiManifest>

  • Modify the HTML template for the Virtual Machines View located in the Content/Template/Tabs folder as follows:

<div
class="gridContainer"></div>

<div
id="hs-empty"
class="hs-environment"></div>

  • and the empty template VirtualMachinesTabEmpty.html:

<div
class="hs-empty">

    <div
class="hs-empty-header">


<p
id="msg-nothing">No virtual machines available.</p>

    </div>

</div>

 

Step 5 – Change the setup

The last piece of the puzzle is the setup code. Change the name of the generated MSI file in the project properties dialog:

As a first step we should fix the broken project references. Remove the outdated references and add the new ones:

Now open the product.wxs and replace the guid's for product code and upgrade code. To do so launch guidgen from a Developers Command Prompt and generate new guid's:

Of course you can change the product name and manufacturer to whatever you want J.

In the AdminSite.wxi change the following lines:

<?ifndef
AdminExtensionTargetDir ?>

<?define
AdminExtensionTargetDir="$(var.CloudSlider.AdminExtension.TargetDir)" ?>

<?endif?>

Do the same in the TenantSite.wxi and WebSitecontent.wxi:

<?ifndef
TenantExtensionTargetDir ?>

<?define
TenantExtensionTargetDir="$(var.CloudSlider.TenantExtension.TargetDir)" ?>

<?endif?>

<?ifndef
WebSiteRootTargetDir ?>

<?define
WebSiteRootTargetDir="$(var.CloudSlider.Api.TargetDir)" ?>

<?endif?>

Now the project should compile and you will get a bunch of errors caused by removed items:

Navigate to the errors by double clicking on them and remove the outdated components from the setup. In the TenantSite.wxi make sure to replace FileSharesTab with VirtualMachinesTab:

Step 6 – Change the registration script

Navigate to the powershell folder in the setup and open the register-resourceprovider.ps1 file and change it as follows:

# PowerShell script to register Windows Azure Pack resource provider.

# Copyright (c) Microsoft Corporation. All rights reserved.

 

# NOTE: This script is designed to run on a machine where MgmtSvc-AdminAPI is installed.

# The *-MgmtSvcResourceProviderConfiguration cmdlets resolve the connection string and encryption key parameters from the web.config of the MgmtSvc-AdminAPI web site.

 

$rpName = 'CloudSlider'

 

Write-Host -ForegroundColor Green "Get existing resource provider '$rpName'…"

$rp = Get-MgmtSvcResourceProviderConfiguration -Name $rpName

if ($rp -ne $null)

{

Write-Host -ForegroundColor Green "Remove existing resource provider '$rpName' $($rp.InstanceId)…"

$rp = Remove-MgmtSvcResourceProviderConfiguration -Name $rpName -InstanceId $rp.InstanceId

}

else

{

Write-Host -ForegroundColor Green "Resource provider '$rpName' not found."

}

 

$hostName = "$env:ComputerName" + ":30037"

$userName = "username"

$password = "pass@word1"

 

$rpSettings = @{

'Name' = $rpName;

'DisplayName' = 'Cloud Slider';

'InstanceDisplayName' = 'Cloud Slider';

'AdminForwardingAddress' = "http://$hostName/admin";

'AdminAuthenticationMode' = 'Basic';

'AdminAuthenticationUserName' = $userName;

'AdminAuthenticationPassword' = $password;

'TenantForwardingAddress' = "http://$hostName/";

'TenantAuthenticationMode' = 'Basic';

'TenantAuthenticationUserName' = $userName;

'TenantAuthenticationPassword' = $password;

'TenantSourceUriTemplate' = '{subid}/services/CloudSlider/{*path}';

'TenantTargetUriTemplate' = 'subscriptions/{subid}/{*path}';

'NotificationForwardingAddress' = "http://$hostName/admin";

'NotificationAuthenticationMode' = 'Basic';

'NotificationAuthenticationUserName' = $userName;

'NotificationAuthenticationPassword' = $password;

}

 

Write-Host -ForegroundColor Green "Create new resource provider '$rpName'…"

$rp = New-MgmtSvcResourceProviderConfiguration @rpSettings

Write-Host -ForegroundColor Green "Created new resource provider '$rpName'."

 

Write-Host -ForegroundColor Green "Add new resource provider '$rpName'…"

$rp = Add-MgmtSvcResourceProviderConfiguration -ResourceProvider $rp

Write-Host -ForegroundColor Green "Added new resource provider '$rpName'."

 

Write-Host -ForegroundColor Green "Get existing resource provider '$rpName' as Xml…"

Get-MgmtSvcResourceProviderConfiguration -Name $rpName -as XmlString

  • Make sure that the HTTP_PORT in Website.wxi matches the port in the $hostName variable:

<Property
Id="WebAppPoolName"


Value="MgmtSvc-CloudSlider"/>

<Property
Id="WebSiteName"


Value="MgmtSvc-CloudSlider"/>

<Property
Id="HTTP_PORT"


Secure="yes"


Value="
30037" />

Now you can build the final version of the setup, and if you've followed the previous series of posts on Custom Resource Providers described at the beginning of this blog post, now you should know how to install and register the Resource Provider.

This properly designed resource provider does not conflict anymore with SMA runbooks:

Until next time! Torsten