XenCenter - How Access XenCenter's Inventory

This is a common enquiry. ISVs often want their application/XenCenter Plug-in to be aware of the inventory information already added to XenCenter.

Although there isn’t an API to access their inventory – if you have suitable directory access permissions you can access the text based configuration file XenCenter uses.

It’s in the roaming user.config file, and its location is OS-dependent. On Windows 7, it will be something like: C:\Users\$USERNAME\AppData\Roaming\Citrix\XenCenterMain.exe_Url_$HASH\$VERSION

MessageBoard Example - JavaScript

Introduction

This is an advanced example of using JavaScript to make XenServer API calls on a XenCenter TabPage plugin.

By using some modified jQuery libraries it is possible to run JavaScript from a local HTML file and pass XML-RPC requests through XenCenter onto XenServer. This enables you to create Javascript based UI additions to XenCenter.

Objective: Add a Message Board for each of your server objects, where people can post messages and status updates which are visible to other XenCenter users with the plugin.

Note: This is a simple example designed to show what is possible using Javascript in a XenCenter TabPage plugin, and as such is not supported or tested as a feature of XenCenter

Requirements

In order to run this sample, you will need to ensure your system has PowerShell 2.0 (or higher) and WiX 3.7 installed.  The code for this example can be obtained from GitHub at: https://github.com/xenserver/xencenter-samples/tree/master/JavaScript.  If you are creating an installer, you will also need the Plugin Installer; available from GitHub at: https://github.com/xenserver/xencenter-samples/tree/master/PluginInstaller.  The full PlugIn specification is available from: https://github.com/xenserver/xencenter-samples/tree/master/docs

XCPlugin file

We define a tab page which uses the relative attribute to reference a local HTML file, and the credentials attribute to tell XenCenter to pass session information to the HTML DOM:


<XenCenterPlugin xmlns="http://www.citrix.com/XenCenter/Plugins/schema" version="1" plugin_version="1.0.0.0">
	<TabPage
	name="Message_Board"
	url="Plugins/Citrix/MessageBoard/MessageBoard.html"
	relative="true"
	credentials="true"
	search="70ecbca9-20e7-4f57-b079-fe3aab968cbc"/>
	<Search
		uuid="70ecbca9-20e7-4f57-b079-fe3aab968cbc"
		name="MessageBoardObjects"
		major_version="2"
		minor_version="0"
		show_expanded="yes">
        <Query>
            <QueryScope>
                <Pool />
                <Server />
                <VM />
                <Snapshot />
                <UserTemplate />
                <DefaultTemplate />
                <RemoteSR />
                <LocalSR />
                <VDI />
                <Network />
            </QueryScope>
        </Query>
    </Search>
</XenCenterPlugin>

Resources

Here is the resources table for this plugin.

Name
Value
MessageBoard.description XenServer JavaScript plugin example.
MessageBoard.copyright © Citrix Systems Inc. 2010
MessageBoard.link http://community.citrix.com/xencenter
Message_Board.label Message Board

HTML File

Here's the header where we reference the jQuery core library as well as our modified RPC library and our own JavaScript file for the plugin:


<script type="text/javascript" src="jquery-1.3.2.js"></script>
  <script type="text/javascript" src="jquery.rpc.js"></script>
  <script type="text/javascript" src="MessageBoard.js"></script>
  <link type="text/css" href="MessageBoard.css" rel="stylesheet"></link>

We want to have a current status message shown quite prominently, so we define:

  • A heading
  • A span statusText to update with this message
  • Some text that the user can click on to edit this value.
<div class="mainHeading" id="statusHeading">
	Current Status:
</div>
<div class="regularBodyText">
	<span id="statusText">None</span>
	<span class="linkLabel" onClick="EditStatus()">(edit)</span>
</div>

It will also be nice to show some other values from the server, such as the description and tags on the object. We will have these as read only.


<div class="mainHeading" id="descriptionHeading">
	Description:
</div>
<div class="regularBodyText" id="descriptionText">
	None
</div>
<div class="mainHeading" id="tagsHeading">
	Tags:
</div>
<div class="regularBodyText" id="tagsText">
	None
</div>

Then we want a space for all the messages people can leave. We define a blank div which the JavaScript will clear and populate each time we want to refresh.


<div class="mainHeading" id="messagesHeading">
	Messages:
	<div id="messagesDiv">
	</div>
</div>

At the bottom we want some text the user can click to add a new message, or to clear the plugin data from the server object.


<div class="bottomButtons">
	<span class="linkLabel" onClick="AddMessage()">Add message</span>
	<span class="linkLabel" onClick="ClearAll()">Clear plugin data</span>
</div>

And finally an error div which we can show/hide when we encounter any problems.


<div class="borderWrapperBottom" id="errorContent">
	There has been an unexpected error with the plugin: <span id="errorMessage">Unknown Error</span>
</div>

CSS

Note for the curious: Rather than anchor links, we are using spans and css (with underlining and hand cursors) for our linkLabels. The main reason for this is that the document.ready event in JavaScript didn't enjoy firing when the .net browser control navigated to a url with an anchor link on the end. As they don't actually navigate anywhere, it seemed more appropriate to use spans and styles in any case.

JavaScript

Now we are going to create the MessageBoard.js script file that will populate the blank bits of our HTML and read/write values to the server.

RefreshPage and local variables

First of all we define our RefreshPage function as mentioned in the TabPage Feature section of the plugin specification. This function should be responsible for tearing down and rebuilding the page, and is called by XenCenter whenever there is a change of state which might need a rebuild (for example, to provide updated session information).


$(document).ready(RefreshPage);
function RefreshPage()
{// hide the error div and show the main content div
	$("#content").css({"display" : ""});
	$("#errorContent").css({"display" : "none"});
	$("#errorMessage").html("");
	RefreshMessagesAndStatus();
	RefreshDescription();
	RefreshTags();
}

It clears out all of our content divs, hides the error feedback div and then starts refreshing all the content.

We also define a few variables for use in the script. Our messages and status text are stored on the other config maps of the server objects, so we need a key which we will use to identify our data in these maps. We also have a local array of messages to simplify interaction between our functions.

// LOCAL VARIABLES SECTION
// Each message is a post on the message board and is stored on the other config map of the server object (a string, string map) under the key defined below.
// The value of this entry is a json encoding of an array of these messages, and each message has its string fields escaped so as not to break the xmlrpc.
// These fields are only escaped when they are put on the server, and are unescaped when saved to the local array (Messages).
var StatusString;var StatusStringOtherConfigKey = "XCServerMessagesPluginStatus";var Messages = new Array();var MessageOtherConfigKey = "XCServerMessagesPlugin";

Drawing, adding and encoding messages

Each message that a user can add to our message board is going to have a title, name, date and a body. We use the title as a unique identifier for the message to keep things simple:

// MESSAGE OBJECT SECTION
// We force uniqueness on the titles and use it as a key for deleting and editing
function Message(t, n, d, b)
{this.title = t;this.name = n;this.date = d;this.body = b;
}

When we write these messages to the server we are going to use JSON encoding, so we define a helper function to escape and encode a message.


function MessageToJson(message)
{return '{"title" : "' + escape(message.title) + '", "name" : "' + escape(message.name) + '", "date" : "' + escape(message.date) + '", "body" : "' + escape(message.body) + '"}';
}

This is the HTML we want to inject to draw each message:


function DrawMessage(Title, Name, Date, Body)
{var newThread = '<div class="messageBox" id="message' + Title + '">'
		+ '<div class="messageTitleBar">'
		+ 	'<span class="messageTitle">' + Title + '</span>'
		+ 	'<span class="messageAuthor">' + Name + '</span>'
		+ 	'<span class="messageDate">' + Date + '</span>'
		+ '</div>'
		+ '<div class="messageBody">' + Body + '</div>'
		+ '<div class="messageFooter">'
		+	'<span class="linkLabel" onClick="EditMessage(\'' + Title + '\')">Edit</span>'
		+	'<span class="linkLabel" onClick="DeleteMessage(\'' + Title + '\')">Delete</span>'
		+ '</div></div>';
	$("#messagesDiv").html($("#messagesDiv").html() + newThread);
}

Then we have the functions which are called by clicking on our linkLabels in the HTML file. These update our local array of messages and then save it all to the server.

  • The edit and delete functions use the Title of the message as a unique identifier before deleting/recreating the message object
  • The add and edit functions use simple JavaScript prompts to collect data

The add function is shown below as an example, if you would like to see the other functions refer to the full MessageBoard.js.


function AddMessage()
{var title = prompt("Enter a title for your message", "");if (title == null || title == "")
	{
		alert("Cannot create a message without a title. Try again, choosing a non blank, unique title.");return;
	}var message;for (m in Messages)
	{if (Messages[m] != null && Messages[m].title == title)
		{
			message = Messages[m];
		}
	}if (message != null)
	{
		alert("A message already exists with that title. Try again, choosing a non blank, unique title.");return;
	}var name = prompt("Enter the authors name.", "");var body = prompt("Enter the body text of the message", "");var time = new Date();var m = new Message(title, name, time.toString(), body);
	Messages.push(m);
	SaveDataToServer();
}

The misc. functions

Here is our first example of an XML-RPC call. It is a utility function used by other bits of the script to retrieve the other config map of the selected object.

// MISC FUNCTIONS SECTION
//  Retrieves the other config map for the currently selected XenCenter object and passes it on to the callback function
function GetOtherConfig(Callback)
{var tmprpc;
	function GetCurrentOtherConfig()
	{var toExec = "tmprpc." + window.external.SelectedObjectType + ".get_other_config(Callback, window.external.SessionUuid, window.external.SelectedObjectRef);";
		eval(toExec);
	}
	tmprpc= new $.rpc("xml",
		GetCurrentOtherConfig,null,
		[window.external.SelectedObjectType + ".get_other_config"]
	);
}

We create and RPC object, telling it what code to evaluate once it has loaded and the different server calls we will need to use in that code. Notice the use of the window.external variable provided by XenCenter, and the callback that the result will be passed to being defined as an additional first parameter to the API call.

The parameters for the RPC constructor are as follows:

  • Indicate that we want to use XML encoding rather than JSON
  • The function that should be evaluated once the RPC object has loaded
  • The XML version to use, default is 1
  • A list of the different API calls we would like to make

It is useful then to have another utility function to parse the result that we get back from the server. The one we need here should update and hide/show the error div with any relevant information.

// The result object of any xmlrpc call to the server contains:
// - a result field which indicates whether it was succesfull or not,
// - a value field containing any returned data in json
// - an error description field containing any error information
// This function checks for success, displays any relevant errors, and returns a json object that corresponds to the value field
function CheckResult(Result)
{var myResult=Result.result;if(myResult.Status=="Failure")
	{var message=myResult.ErrorDescription[0];for(var i=1; i<myResult.ErrorDescription.length; i++)
		{
			message+=","+myResult.ErrorDescription[i];
		}
		$("#content").css({"display" : "none"});
		$("#errorContent").css({"display" : ""});
		$("#errorMessage").html(message);return;
	}if (myResult.Value == "")
	{return;
	}
	myResult = eval("("+myResult.Value+")");return myResult;
}

The result comes back as a JSON object for ease of evaluation.

Retrieving data from the server

Leaving the other config to one side for a moment, a decent example of retrieving a value from the server and populating our HTML with it is this chain of two methods which refresh the tags div.

// TAGS UPDATE SECTION
// This pair of methods chain to retrieve the server objects tags from the server and display them. There is no writing to the tags field on the server.
function RefreshTags()
{var tmprpc;
	function RetrieveTags()
	{var toExec = "tmprpc." + window.external.SelectedObjectType + ".get_tags(ShowTags, window.external.SessionUuid, window.external.SelectedObjectRef);";
		eval(toExec);
	}
	tmprpc= new $.rpc("xml",
		RetrieveTags,null,
		[window.external.SelectedObjectType + ".get_tags"])
}
function ShowTags(TagsResult)
{var result = CheckResult(TagsResult);if (result == null)
	{return;
	}var tags = "";for (t in result)
	{
		tags = tags + result[t] + ", ";
	}
	tags = tags.substring(0, tags.length - 2);if (tags == "")
	{
		tags = "None";
	}
	$("#tagsText").html(tags);
}

We also need a similar pair of functions to refresh the description div, which you can see in the full MessageBoard.js.

Writing data to the server

Finally, we have a chain of functions which:

  1. Remove all the messages from the objects other config map
  2. Write the local message array to the other config map
  3. Clear the status message from the other config map
  4. Write the local status message to the other config map
  5. Populate our HTML page with values from the server

Here are the first pair which clear and write the message data:


function ClearMessages()
{var tmprpc;
	function RemoveMessagesFromOtherConfig()
	{var toExec = "tmprpc." + window.external.SelectedObjectType + ".remove_from_other_config(WriteMessages, window.external.SessionUuid, window.external.SelectedObjectRef, '" + MessageOtherConfigKey + "');";
		eval(toExec);
	}
	tmprpc= new $.rpc("xml",
		RemoveMessagesFromOtherConfig,null,
		[window.external.SelectedObjectType + ".remove_from_other_config"]
	);
}
function WriteMessages(RemoveMessagesResult)
{var result = CheckResult(RemoveMessagesResult);var tmprpc;
	function WriteMessagesToOtherConfig()
	{var toExec = "tmprpc." + window.external.SelectedObjectType + ".add_to_other_config(ClearStatus, window.external.SessionUuid, window.external.SelectedObjectRef, '" + MessageOtherConfigKey + "', escape(output));";
		eval(toExec);
	}var output = "[";if (Messages.length > 0)
	{for (m in Messages)
		{
			output = output + MessageToJson(Messages[m]) + ",";
		}
		output = output.substring(0, output.length-1);
	}
	output = output + "]";
	tmprpc= new $.rpc("xml",
		WriteMessagesToOtherConfig,null,
		[window.external.SelectedObjectType + ".add_to_other_config"]
	);
}

Summary

Hopefully this quick run through gives you an idea of the sort of things that can be done using JavaScript with your TabPage plugin to create dynamic pieces of user interface.

Screenshots

MessageBoardTabPageScreen

XenCenter Plugins - HelloWorld Example - PowerShell

Introduction

Here is a short example which will guide you through the creation of a XenCenter plugin.

Objective: We would like a new menu item in XenCenter which says "hello from ..." followed by the names of whichever objects are selected in the XenCenter resource list (treeview).

In this example we shall be using the XenServerPSSnapIn PowerShell bindings to communicate with the servers listed in XenCenter.

Requirements

In order to run this sample, you will need to ensure your system has PowerShell 2.0 (or higher) and WiX 3.7 installed.  The code for this example can be obtained from GitHub at: https://github.com/xenserver/xencenter-samples/tree/master/PowerShell.  If you are creating an installer, you will also need the Plugin Installer; available from GitHub at: https://github.com/xenserver/xencenter-samples/tree/master/PluginInstaller.  The full PlugIn specification is available from: https://github.com/xenserver/xencenter-samples/tree/master/docs

Running the sample code will require you to install the XenServer PowerShell SnapIn located in the XenServerPSSnapIn folder in the SDK.

XCPlugin file

To add a menu item into XenCenter and call our PowerShell script we need to create a plugin configuration file.

We start with the XenCenterPlugin node with the following attributes:

  • xmlns - Specifies the schema of the XML file.
  • version - The XenCenter Plugins version, we are using version 1.

<XenCenterPlugin xmlns=http://www.citrix.com/XenCenter/Plugins/schema" version="1">

To create a menu item we add a MenuItem node under the XenCenterPlugin node. We give this three attributes:

  • name - For identifying the menu item.
  • menu - The name of the menu under which the menu item should appear. Let's use the 'View' menu.
  • serialized - Decides if copies of the plugin running simultaneously are allowed for the same object. Here we do not limit this so we set to "none".
<MenuItem name="hello-menu-item" menu="view" serialized="none">

Next we add the XenServerPowerShell tag which points to a PowerShell script to run:

  • filename - The filepath of our target PowerShell script, relative to the install directory of the XenCenter executable.
  • window - Whether to show the console window which runs the script. We don't want this, lets turn it off.
<XenServerPowerShell filename="Plugins\xenserver.org\HelloWorld\HelloWorld.ps1" window="false" />

Close off all the tags and save as HelloWorld.xcplugin.xml. The name of this file must be the same name as the name of the plugin directory in which it resides.

Resources

The next step is to add some resources which provide strings and image paths for the plugin. These are stored in DLLs so that the correct language (if other cultures are provided) can be loaded at run-time.

First we need to create the resx file. This can be done using Visual Studio. Add strings for the menu-item labels, copyright statements, filepaths to icons etc. The names of the resources strings should be <name>.<property>, where <name> is the name given to the tag and <property> is one of the properties found in the specification. Here is the resources table for this plugin.

Name
Value
HelloWorld.description XenServer PowerShell plugin example.
HelloWorld.copyright © Citrix Systems Inc. 2009
HelloWorld.link http://community.citrix.com/xencenter
hello-menu-item.label Hello World!
hello-menu-item.description Displays 'Hello World' from the selected object.
hello-menu-item.icon Plugins\Citrix\HelloWorld\HelloWorld.png

 The final step is to convert this .resx into a DLL. We do this with two tools:

  1. ResGen.exe - Creates a .resources file from the resx. This can be found in the .NET framework SDK.
  2. Al.exe - Embeds the .resources file into a DLL. We specify we want to create a library, the file we wish to embed, that we need the invariant culture and the name of the DLL file. Al.exe is in the .NET framework directory.
C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\ResGen.exe HelloWorld.resx
C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\al.exe /t:lib /embed:HelloWorld.resources
 /culture:0x007F /out:HelloWorld.resources.dll

The PowerShell script

We now need to write the script, which will compile a list of names from the selected objects in the resource list and show a message box with our "Hello from ..." in it.

XenCenter will pass in parameter sets to your plugin to let you know what is selected in the resource list, and to enable you to communicate with your connected servers. PowerShell plugins can access these parameter sets from the $ObjInfoArray variable which XenCenter populates before running your script.

$ObjInfoArray

This handy variable is an array of hashmaps. Each map represents a parameter set with the following keys - for each $map in $ObjInfoArray:

  • $map["url"] - The URL of the server which owns the object selected in the tree view
  • $map["sessionRef"] - An authenticated session reference for the server (allows the script to log into the server)
  • $map["class"] - The type of the object selected: Server, VM etc
  • $map["objUuid"] - The UUID for the selected object.

You will get:

  • One parameter set per object selected in the resource list.
  • One parameter set per object in a folder if a folder is selected.
  • One parameter set per connected server if the user has the XenCenter node selected. These are provided to allow you to communicate with any server if launched from the XenCenter node, however the "class" and "objUuid" keys will be marked as blank.

$SelectedObjectNames=@();
$XenCenterNodeSelected = 0;
#the object info array contains hashmaps, each of which represent a parameter set and describe a target in the XenCenter resource list
foreach($parameterSet in $ObjInfoArray)
{if ($parameterSet["class"] -eq "blank")
	{
		#When the XenCenter node is selected a parameter set is created for each of your connected servers with the class and objUuid keys marked as blankif ($XenCenterNodeSelected)
		{continue
		}
		$XenCenterNodeSelected = 1;
		$SelectedObjectNames += "XenCenter"
	}
	elseif ($parameterSet["sessionRef"] -eq "null")
	{
		#When a disconnected server is selected there is no session information, we get null for everything except class
		$SelectedObjectNames += "a disconnected server"
	}else
	{
		Connect-XenServer -url $parameterSet["url"] -opaqueref $parameterSet["sessionRef"]
		#Use $class to determine which server objects to get
		#-properties allows us to filter the results to just include the selected object
		$exp = "Get-XenServer:{0} -properties @{{uuid='{1}'}}" -f $parameterSet["class"], $parameterSet["objUuid"]
		$obj = Invoke-Expression $exp
		$SelectedObjectNames += $obj.name_label;
	}
}

Looking at that final else block you can see that before any server commands can be used we need to connect to the server. We use the sessionRef from the existing connection in XenCenter which means we don't need to provide any new credentials.


Connect-XenServer -url $parameterSet["url"] -opaqueref $parameterSet["sessionRef"]

Then when we use XenServer commands they automatically use the session from the last Connect-XenServer call. We construct an expression using the class and uuid information we were given which will retrieve the object selected in the resource list.


$exp = "Get-XenServer:{0} -properties @{{uuid='{1}'}}" -f $parameterSet["class"], $parameterSet["objUuid"]
$obj = Invoke-Expression $exp

Because being pretty is important the following bit of code constructs a nice sentence out of our list of names, but equally you could just string join your name array at this point.


$NameString = "Hello from {0}" -f $SelectedObjectNames[0];if ($SelectedObjectNames.length -gt 1)
{
	#we are aiming for "name_1, name_2, name_3...name_n-1 and name_n"for ($i=1; $i -lt $SelectedObjectNames.length - 1; $i++)
	{
		$NameString += ", {0}" -f $SelectedObjectNames[$i]
	}
	$NameString += " and {0}" -f $SelectedObjectNames[$SelectedObjectNames.length - 1]
}

Finally we use .NET Windows Forms to create an alert box by loading the correct DLL.


Reflection.Assembly]::loadwithpartialname('system.windows.forms')
[system.Windows.Forms.MessageBox]::show($NameString, "Hello World")

Deployment

The best way to distribute your XenCenter plugin is to package your plugin into a single MSI (Windows Installer) file.

Using a Windows Installer allows you to make sure the plugin is being installed into the correct place (by checking the XenCenter InstallDir registry key) and it gives versioning. A newer version will automatically uninstall the old version and then install the new one.

The entire build process, including creation of an installation MSI is accomplished by running make.cmd from a Visual Studio Command Prompt.

Summary

This exampled covered all the basics of producing a MenuItem plugin for XenCenter using PowerShell. It also introduced how to resource and deploy your plugin.

Screenshots


 

MenuItemScreen


 

HelloPromptScreen


 

XenCenter Plugins - WebUI Tab Example

Introduction

This article is a guide through the steps to create a XenCenter TabPage plugin. It assumes you are familiar with the Hello World Example.

This type of plugin add a new tab page for particular objects in the tree view in XenCenter. The url can be context sensitive making the contents of the tab relevent to the current server, for example.

In this example we will create a plugin called NetAppWebUI which creates a tab displaying the web interface for a Network Appliance storage repository. 

Requirements

In order to run this sample, you will need to ensure your system has PowerShell 2.0 (or higher) and WiX 3.7 installed.  The code for this example can be obtained from GitHub at: https://github.com/xenserver/xencenter-samples/tree/master/WebUI.  If you are creating an installer, you will also need the Plugin Installer; available from GitHub at: https://github.com/xenserver/xencenter-samples/tree/master/PluginInstaller.  The full PlugIn specification is available from: https://github.com/xenserver/xencenter-samples/tree/master/docs


You will also need a NetApp SAN to use the plugin. 

Plugin XML

The plugin is described by a .xcplugin.xml file. The tab page is described by a TabPage xml node. We need to give it a url and a search to determine which tree view items make the tab page appear.

The URL can contain context sensitive variables. The have the form {$variable}. They get replaced each time the web page is loaded. In this example the URL is the web address of the NetApp web interface. This is at http://<storage IP address>/na_admin. We also need to specify the UUID of a search which we are going to add the plugin descriptor.

 <TabPage name="webui-tab" url="http://{$ip_address}/na_admin/" search="4f40121b-cc67-4b38-a93b-e72959bf0904" />

The search is more tricky. We can get a rough skeleton from a search exported from XenCenter. The search used is 'Search for All Storage Repositories'. We cannot get the entire search because you cannot filter by SR type in XenCenter. We need to add a Query to the XML manually. The property sr_type is an Enum so we need an EnumPropertyQuery.

<Search uuid="4f40121b-cc67-4b38-a93b-e72959bf0904" name="NetApp SRs" major_version="2" minor_version="0" show_expanded="yes">
  <Query>
    <QueryScope>
      <RemoteSR />
      <LocalSR />
    </QueryScope>
    <EnumPropertyQuery property="sr_type" equals="yes" query="netapp" />
  </Query></Search>

The whole plugin descriptor is contained in NetAppWebUI.xcplugin.xml. 

Resources File

Here is a list of labels used in the resources file.

NameLabel
NetAppWebUI.description Show NetApp web interface in XenCenter.
NetAppWebUI.copyright © Citrix Systems Inc. 2009
NetAppWebUI.link http://community.citrix.com/xencenter
webui-tab.label NetApp Configuration

Deployment

The entire build process, including creation of an installation MSI is accomplished by running make.cmd from a Visual Studio Command Prompt.

Screenshots

web na-plug-in-1

 

 

 

 

XenCenter Plugins - Working with Parameters

XenCenter provides parameter sets to your plugin to describe the current selection in the XenCenter resource list. For the full details of the data in a parameter set, please see the full PlugIn specification is available from: https://github.com/xenserver/xencenter-samples/tree/master/docs.

This page details how to deal with this information using a Shell command, which requires some thought as there are no handy global variables separating everything out as is the case with the PowerShell commands.

Shell commands and parameters


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE XenCenterPlugin PUBLIC "-//XENCENTERPLUGIN//DTD XENCENTERPLUGIN1//EN" "xencenter-1.dtd">
<XenCenterPlugin
	xmlns="http://www.citrix.com/XenCenter/Plugins/schema"
	version="1">
	<MenuItem
		name="hello-menu-item"
		menu="view"
		serialized="none">
		<Shell
			filename="Plugins\xenserver.org\HelloWorld\HelloWorld.exe"
			window="true"
			param="1, goodbye"
		/>
	</MenuItem>
</XenCenterPlugin>

When using a Shell command, your parameters are passed directly as arguments to the plugin executable. In addition to the parameter sets, you will also receive any parameters you define in the param attribute on your XML. Outlined here are several approaches you might take to parse these arguaments, with examples in C#.

Option 1 - No extra params


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE XenCenterPlugin PUBLIC "-//XENCENTERPLUGIN//DTD XENCENTERPLUGIN1//EN" "xencenter-1.dtd">
<XenCenterPlugin
	xmlns="http://www.citrix.com/XenCenter/Plugins/schema"
	version="1">
	<MenuItem
		name="hello-menu-item"
		menu="view"
		serialized="none">
		<Shell
			filename="Plugins\xenserver.org\HelloWorld\HelloWorld.exe"
			window="true"
		/>
	</MenuItem>
</XenCenterPlugin>

This is the simple option. We know that parameter sets come in groups of four arguments so if none of the menu item commands which call that executable define anything in the param attribute we can just read them off in groups.


private static void SeparateParams(string[] args, out List<ParameterSet> ParamSets)
{
     ParamSets = new List<ParameterSet>();
     for (int i = 0; i < args.Length; i += 4)
     {
           ParamSets.Add(new ParameterSet(args[i], args[i + 1], args[i + 2], args[i + 3]));
     }
}

Option 2 - Fixed number of extra parameters

If you want to use extra parameters in your plugin commands, one option is to define a fixed number of extra parameters that each command must pass.


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE XenCenterPlugin PUBLIC "-//XENCENTERPLUGIN//DTD XENCENTERPLUGIN1//EN" "xencenter-1.dtd">
<XenCenterPlugin
	xmlns="http://www.citrix.com/XenCenter/Plugins/schema"
	version="1">
	<MenuItem
		name="goodbye-menu-item"
		menu="view"
		serialized="none">
		<Shell
			filename="Plugins\xenserver.org\HelloWorld\HelloWorld.exe"
			window="true"
			param="goodbye"
		/>
	</MenuItem>
	<MenuItem
		name="hello-menu-item"
		menu="view"
		serialized="none">
		<Shell
			filename="Plugins\xenserver.org\HelloWorld\HelloWorld.exe"
			window="true"
			param="hello"
		/>
	</MenuItem>
	<MenuItem
		name="quiet-menu-item"
		menu="view"
		serialized="none">
		<Shell
			filename="Plugins\xenserver.org\HelloWorld\HelloWorld.exe"
			window="true"
			param="BLANK_PARAM"
		/>
	</MenuItem>
</XenCenterPlugin>

In this example you can see each command passes exactly one extra parameter, so we can read that off before moving onto the parameter sets. Commands that don't want to use all the parameters you are expecting can use a dummy keyword to keep the number of parameters consistent.


private static int numberOfExtraParams = 1;
private static void SeparateParams(string[] args, out List<ParameterSet> ParamSets, out List<string> ExtraParams)
        {
            ExtraParams = new List<string>();
            for (int i = 0; i < numberOfExtraParams; i++)
            {
                ExtraParams.Add(args[i]);
            }
            ParamSets = new List<ParameterSet>();
            for (int i = numberOfExtraParams; i < args.Length; i += 4)
            {
                ParamSets.Add(new ParameterSet(args[i], args[i + 1], args[i + 2], args[i + 3]));
            }
        }

Option 3 - Variable number of extra parameters

Here we are going to use some sort of indicator to help us know how many extra parameters there are going to be before the parameter sets start in our list of arguments. There are several ways to do this, but here we are going to use the first extra parameter as an indicator for how many further extra parameters we should expect.


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE XenCenterPlugin PUBLIC "-//XENCENTERPLUGIN//DTD XENCENTERPLUGIN1//EN" "xencenter-1.dtd">
<XenCenterPlugin
	xmlns="http://www.citrix.com/XenCenter/Plugins/schema"
	version="1">
	<MenuItem
		name="hello-menu-item"
		menu="view"
		serialized="none">
		<Shell
			filename="Plugins\xenserver.org\HelloWorld\HelloWorld.exe"
			window="true"
			param="1, goodbye"
		/>
	</MenuItem>
	<MenuItem
		name="busy-menu-item"
		menu="view"
		serialized="none">
		<Shell
			filename="Plugins\xenserver.org\HelloWorld\HelloWorld.exe"
			window="true"
			param="2, hello, goodbye"
		/>
	</MenuItem>
</XenCenterPlugin>

private static void SeparateParams(string[] args, out List<ParameterSet> ParamSets, out List<string> ExtraParams)
        {
            int numberOfExtraParams = 0;
            numberOfExtraParams = int.Parse(args[0]);
            ExtraParams = new List<string>();
            for (int i = 0; i < numberOfExtraParams; i++)
            {
                ExtraParams.Add(args[i + 1]);
            }
            ParamSets = new List<ParameterSet>();
            for (int i = numberOfExtraParams + 1; i < args.Length; i += 4)
            {
                ParamSets.Add(new ParameterSet(args[i], args[i + 1], args[i + 2], args[i + 3]));
            }
        }

This is a good technique as it only requires one extra parameter to encode the information and allows flexibility. Another alternative would be to have a marker which signifies that the extra parameters have finished:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE XenCenterPlugin PUBLIC "-//XENCENTERPLUGIN//DTD XENCENTERPLUGIN1//EN" "xencenter-1.dtd">
<XenCenterPlugin
	xmlns="http://www.citrix.com/XenCenter/Plugins/schema"
	version="1">
	<MenuItem
		name="hello-menu-item"
		menu="view"
		serialized="none">
		<Shell
			filename="Plugins\xenserver.org\HelloWorld\HelloWorld.exe"
			window="true"
			param="goodbye, END_EXTRA_PARAMS"
		/>
	</MenuItem>
	<MenuItem
		name="busy-menu-item"
		menu="view"
		serialized="none">
		<Shell
			filename="Plugins\xenserver.org\HelloWorld\HelloWorld.exe"
			window="true"
			param="hello, goodbye, END_EXTRA_PARAMS"
		/>
	</MenuItem>
</XenCenterPlugin>

About XenServer

XenServer is the leading open source virtualization platform, powered by the Xen Project hypervisor and the XAPI toolstack. It is used in the world's largest clouds and enterprises.
 
Commercial support for XenServer is available from Citrix.