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

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

  • Comments 5
  • Likes

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

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Thanks for the great blog Victor! we have written a custom provider and an issue i could not figure out was the Quota API in QuotaController.cs in HelloWorld. The API is
    [HttpGet]
    public List GetDefaultQuota()
    in the sample. I changed to
    [HttpGet]
    public ServiceQuotaSettingList GetDefaultQuota()
    {
    Logger.Writeline(" ");
    ServiceQuotaSettingList list = new ServiceQuotaSettingList();
    ServiceQuotaSetting setting = new ServiceQuotaSetting();
    setting.Key = "FOO";
    setting.Value = "BAR";
    list.Add(setting);
    return list;
    }

    But i dont see the API getting called in my logs after I navigate in admin portal to Plans, select a plan and click on our service that is part of the plan. It continues to say "extension does not have any quota configuration. So this page is left BLANK intentionally"

    What am i missing?

  • Hi Venkat,

    what I have found in the SQlAdmin Resource provider using a decompiler(Microsoft.WindowsAzure.Management.ResourceProvider.SqlServer.dll) the GetDefaultQuota method should probably look like this:

    [HttpGet]
    public ServiceQuotaSettingList GetDefaultQuota()
    {
    var defaultSettings = new ServiceQuotaSettingList();

    var configArray = new CloudSliderConfiguration[1];
    var config = new CloudSliderConfiguration
    {
    DisplayName = "Cloud Slider",
    InstanceCount = "1",
    Size = "Large"
    };

    configArray[0] = config;

    var quota = new ServiceQuotaSetting { Key = "CloudSliderConfiguration", Value = JsonConvert.SerializeObject(configArray) };

    defaultSettings.Add(quota);
    return defaultSettings;
    }
    }

    where CloudSliderConfiguration is my own configuration class:

    [DataContract(Namespace = Constants.DataContractNamespaces.Default)]
    public class CloudSliderConfiguration
    {
    ///
    /// displayName
    ///
    [DataMember(Order = 0)]
    public string DisplayName { get; set; }

    ///
    /// InstanceCount
    ///
    [DataMember(Order = 1)]
    public string InstanceCount { get; set; }

    ///
    /// Size
    ///
    [DataMember(Order = 2)]
    public string Size { get; set; }
    }


    Probably the configuration Array is only needed when you have multiple configuration entries for a service.

    To understand how the admin site interacts with your configuration you must read the comments in the following jscript file of the installed product
    "%INSTALLROOT%\MgmtSvc-AdminSite\Content\PlansAdmin\Scripts\Plans.ServiceQuotaConfigContainer.js"

    Hope this helps
    Regards
    Torsten

  • hello, I did it as you wrote, but there are a few porblems, adminsite have cloudSlider resource provider, but tenantsite have no cloudslider resource provider, I have given plan that has the service to the tenant

  • Hi feng,
    there a different reasons causing the tenant site not to display the CloudSlider extension to the logged on tenant:
    1. Did you change the access for the CloudSlider plan from private to public ?
    2. Did you create a CloudSlider subscription for the the tenant ?
    3. If one of the rename operations to the CloudSlider tenant resources failed the Extension will not load on startup and you will find an error log entry in the tenant site custom eventlog.
    4. If one of the changes in the CloudSlider javascript files is the root cause you must enable the development mode of the tenant site. In that case a special debug window will display the startup error. How to enable development mode you can find here http://blogs.technet.com/b/privatecloud/archive/2014/03/13/custom-resource-providers-in-windows-azure-pack-debugging-the-hello-world-sample.aspx

    Hope this helps
    Regards
    Torsten

  • thanks for your reply, i have more questions, there are many js files in windows azure pack, but i have not found the document about js files. i just find url http://msdn.microsoft.com/en-us/library/dn528478.aspx for it. for example, what does Exp.UI.Commands.Global.clear() mean, and so on. when i want to know their meaning, I must debug it? can you help me?