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: https://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' = "https://$hostName/admin";

'AdminAuthenticationMode' = 'Basic';

'AdminAuthenticationUserName' = $userName;

'AdminAuthenticationPassword' = $password;

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

'TenantAuthenticationMode' = 'Basic';

'TenantAuthenticationUserName' = $userName;

'TenantAuthenticationPassword' = $password;

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

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

'NotificationForwardingAddress' = "https://$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