Thursday, December 27, 2012

Create an ASP.NET Server Control that uses Powershell

So here’s the task, we want to create some admin tools that will perform a function, return the result in a formatted table, and then display that in a table format – encapsulated in a server control.  You might want to ask why? Well if we use this tool on multiple reporting sites, we will want to reuse the code, So why not just package this up in a nice server control?

A Note About Powershell And ASP.NET:
You have to remember that the security features for ASP.Net do not let you access to the client (the user), so everything that is run through Powershell in the context of ASP.Net will be run from THE SERVER’S PERSPECTIVE.  Thus, in our example we will be retrieving a list of the logical drives, jumble it up with some formatting, apply some HTML to it, and then spit it out to a Literal.  All the drives listed will be in relation to the server (if you write the code and debug it, it will give you the list of drives on the machine you are debugging in I.E. it will always use localhost as the base address).

Before we even start:
Microsoft is no dummy, and thus they automagically set you to not be able to run powershell scripts on your machine.  Unless they are signed.  You can create your own signature for scripts, however it is an extremely convoluted process that will have you up for the rest of the night (just Google “Sign Powershell Script”.  There are plenty of examples).
However, in our case we are going to remove the restriction.
So Left-Click your Start button.  In the Search field at the bottom, type “Powershell”.  You want the one named “Windows Powershell”.  The ISE is used to create modules and functions, we don’t need to do that so Windows Powershell will work for our purpose.  It should take you to a prompt like PS \> (it might also have a drive reference in there – we don’t care, all we’re doing here is changing the execution policy).
The short and skinny of Powershell commands are formed like so <verb-singularNoun>
Why singular nouns?  Well in the English language we have different ways to form plural nouns.  For instance, the plural of home is homes – the plural of policy is policies – and the plural of mouse is mice.  Even worse, the plural of virus is virii.  So to keep it simple, it’s singular.
Now that we have our Powershell prompt open, type get-executionpolicy
It will reply with what your current executionpolicy is.  If it is not unrestricted we need to set it as so, so predictably our command is: set-executionpolicy unrestricted
You can reissue the get-executionpolicy to ensure it has definitely been set.  Now that we have that out of the way, you can close the Powershell shell.

Fire up your Visual Studio.  Left-Click File | New Project.. | In the New Project modal window ensure you select Visual Basic Web | Also ensure you are on the .Net Framework 4 and select ASP.NET Server Control.  Name and store your control appropriately (Mine is named TDN.Web – TDN = TheDotNetters).  Click OK and you will be sent to the code editor.

First things first, go into your solution explorer and rename the .vb file to ServerDriveInfo.vb
You will be prompted to name your class the samething – obviously, choose YES.  Thus, our solution explorer should look like -
image

Now that we have the basics down.  We need to be able to utilize Powershell.  We are able to accomplish this using a Reference.  So in the Solution Explorer | Right-Click your solution name | Add Reference…
Right next to the File Name field, there is Browse button | Click the Browse button.  The assembly we need is located in the %drive%\Program Files\Reference Assemblies\Microsoft\WindowsPowershell\v1.0
Here you will see a list of Powershell assemblies.  The particular one we need is: System.Management.Automation.  Highlight the assembly and then click Open.  Click Add | Close.

You should automagically have the ServerDriveInfo.vb file open from when you created the Server Control, but if you do not, double-click it in the Server Explorer.  Here you will see the template for a Server Control.
In our declarations, we have the following code:

Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls


We need to add our reference to the list:



Imports System.Management.Automation


So our entire declarations statement should look like:



Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Management.Automation


Ok, so next in our template is the Class definition and inheritance.  Prior to that, we have some funny code that are in brackets < >.  In Visual Basic.Net, anywhere you see these tags in the code behind (or coding sections) these are compiler tags.  That is, they are commands for the compiler.  They define things like the default property of the control and how the toolbox forms the drag and drop commands (when you drag and drop the control from the toolbox to the aspx).  Here we need to change the name of the default property.  Instead of using the templated “Text” we are going to change it to “ServerName”:



<DefaultProperty("ServerName"), ToolboxData("<{0}:ServerDriveInfo 
runat=server></{0}:ServerDriveInfo>"
)>


We need to create several global variables that will hold key information.  To be exact, we need to create a single variable that will hold our current server name, and also a variable that represents a literal object.  So after our property declaration, you have your Class declaration and inheritance:



Public Class ServerDriveInfo
Inherits WebControl




Lets create the global objects:



Dim CurrServer As String = String.Empty
Private lit As Literal




Here we are declaring a variable to hold the current server name.  We are also initializing it with a default value of String.Empty (The same as saying CurrServer = “”).  Next we are creating a Literal object.  This will be used as the results come back from PowerShell to display the information in a table format (reasoning and discussion below).



Since we have already set the default property of the control to ServerName, we need to create the actual property itself.  Remember, because we put the default property in brackets <> that it draws attention to the compiler, thus, We are not actually creating the property in that statement.  We create the property by adding the following code:



<Bindable(True), Category("Appearance"), DefaultValue(""), Localizable(True)>
Property ServerName() As String
Get
Dim
s As String = CStr(ViewState("ServerName"))
If s Is Nothing Then
Return
CurrServer
Else
Return
s
End If
End Get

Set
(ByVal Value As String)
ViewState("ServerName") = Value
End Set
End Property




So here we start with the brackets <> again.  Notice the Bindable and Localizable properties.  These are instructions for the compiler that allow you to bind to this particular property.  Thus, if you place the control inside of a Repeater or like control, you can Bind to it.  Category is merely for housekeeping and the DefaultValue is… Obviously, the default value for the property (this means if you omit the property from the control’s HTML statement, it will utilize this value).  For instance, if you create a Width property and assign it a default value of 220px – it will use 220px unless you specify a different value for the property in the HTML declaration.



Since we have decided to put our return results in a Literal, we will need to create that Literal as a child control (to the server control).  That is, our server control is giving birth to our literal and it will exist inside our server control.  We don’t want anything outside of the current class to have access to the Literal so our Sub declaration will be Protected.  Secondly, due to inheritance (from WebControl) we’ll need to override the CreateChildControls() Sub.  Our code will look like:



Protected Overrides Sub CreateChildControls()
End Sub




Now that we have our Sub all setup, we need to initialize our Literal.  In code splice, it would look like:




Protected Overrides Sub CreateChildControls()
lit = New Literal()




Now that we have initialized the Literal, we can set its properties like any other Control.  For general booking keeping and S &G’s lets set its ID, in code splice:




Protected Overrides Sub CreateChildControls()
lit = New Literal()
lit.ID = "litReturnResults"




Now we need to create an object to house our PowerShell operations. In code splice, I am going to create an object called shell and initialize it to the PowerShell.Create() method that we have referenced from System.Management.Automation:




Protected Overrides Sub CreateChildControls()
lit = New Literal()
lit.ID = "litReturnResults"
Dim shell = PowerShell.Create()




Now, we can create the actual PowerShell command -

Due to the fact of limited space, this code splice is going to contain carriage returns that should not be present in the code.  The shell.Commands.AddScript commands should all be present on the same line –OR- should contain a line sepeartor (_).



If CurrServer = String.Empty Then
shell.Commands.AddScript("Get-WmiObject -Class Win32_LogicalDisk
| Where-Object {$_.DriveType -ne 5} | Sort-Object -Property Name |
Select-Object Name, VolumeName, FileSystem, Description, VolumeDirty,
@{""Label""=""DiskSize(GB)"";""Expression""={""{0:N}"" -f ($_.Size/1GB)
-as [float]}}, @{""Label""=""FreeSpace(GB)"";""Expression""={""{0:N}""
-f ($_.FreeSpace/1GB) -as [float]}},
@{""Label""=""%Free"";""Expression""={""{0:N}"" –f
($_.FreeSpace/$_.Size*100) -as [float]}} | convertto-html -fragment –as
list | ForEach {$_ -replace ""<table>"", ""<table width='100%'>""} |
ForEach {$_ -replace ""<td>"", ""<td style='padding-left:15px;'>""}"
)
Else
shell.Commands.AddScript("Get-WmiObject -Class Win32_LogicalDisk
-Computer "
& CurrServer & " | Where-Object {$_.DriveType -ne 5} |
Sort-Object -Property Name | Select-Object Name, VolumeName, FileSystem,
Description, VolumeDirty,
@{""Label""=""DiskSize(GB)"";""Expression""={""{0:N}"" -f ($_.Size/1GB) –as
[float]}}, @{""Label""=""FreeSpace(GB)"";""Expression""={""{0:N}"" –f
($_.FreeSpace/1GB) -as [float]}},
@{""Label""=""%Free"";""Expression""={""{0:N}"" –f
($_.FreeSpace/$_.Size*100) -as [float]}} | convertto-html -fragment -as list
| ForEach {$_ -replace ""<table>"", ""<table width='100%'>""} |
ForEach {$_ -replace ""<td>"", ""<td style='padding-left:15px;'>""}"
)
End If





As stated above, the shell.Commands.AddScript command all needs to be on one line with the carriage returns removed.  Ok, this one is going to be a little bit of a breather, but this is the central core of the code.  We are creating a WMIObject referencing the Win32_LogicalDisk class.  We are then creating a condition WHERE the drive type is of 5 (a Logical Drive).  Then we are going to sort it by Drive Name (Thus, drive C: comes before drive Z: ).  After that, we are only going to select the values for Name, VolumeName, FileSystem, Description, VolumeDirty, and we’ll perform some math to get a DiskSize (in GB), FreeSpace, and %Free.

We are going to take that result set and convert it to HTML as a fragment (leaves off the header, body, and footer tags – gives us the result in a raw HTML table).  Then, as an added bonus, we’ll replace the table tag with one with some style (here we are setting it to 100% width) and then do the same for the columns.



Now why the IF statement?  If you look at the script there is a very minute difference between the two.  If you notice in the ELSE statement after our Win32_LogicalDisk statement, we are defining a parameter to the Computer (or computer name).  We then add our CurrServer which is assigned by the ServerName property.  Thus, if we specify a ServerName property in our HTML statement, it will populate the ELSE statement, running against that specific server instead of the current server as the IF statement specifies. 
NOTE: If you run against another computer name (Specify a value for ServerName, that  servername needs to be accessible TO THE SERVER, and secondly it needs to have its executionpolicy set to unrestricted (see above).



In the .Net Framework, we need to Invoke objects that perform an action (in this instance, run our script), after our if statement in code splice:




Dim results = shell.Invoke()





Here we are creating a results object to hold the results of our script, that is run by initializing results to the shell.Invoke() method.  If your debug and bring up the command window on results, you will notice that the results are an (string) array.  Lines that are written from PowerShell in the Invoke method are placed into a string array (one element for each line of the result).  Thus, to use the results we will need to create a StringBuilder and piece together the array so that it will format the returning HTML result into something we can display, in code splice:




Dim strbuilder As StringBuilder = New StringBuilder
For Each result In results
strbuilder.Append(result.BaseObject.ToString())
Next





So here, we are declaring a StringBuilder object and initializing it to New.  Then we are looping through the results array and building the HTML into something we can use.  However, doing it this way, it is taking the individual elements of the results HTML and forming a long HTML string.  Since we do not care if it is human readable, this suits our purpose.  Now because we have formatted it into HTML, we can not simply display it as text – we need to use our Literal, in code splice:




lit.Text = strbuilder.ToString()





This is simply taking our StringBuilder value, converting it to a complete string and populating the Literal control.  Finally, we need to add our Literal control the Server control (and close the Sub), in code splice:




Me.Controls.Add(lit)
End Sub





Now, we need a way to display the Literal control.  We can use the Render Sub.  Due to the fact that we are inheriting it from the WebControl class, we will need to override it:



    Protected Overrides Sub Render(writer As System.Web.UI.HtmlTextWriter)
lit.RenderControl(writer)
End Sub





Finally, we need to close the class:



End Class





When you compile the code and use it in a website, you will get a result like so (It is assumed that you know how to add assemblies to the ToolBox and drag/drop them on a webpage):

image



Now, if you notice, when you add the Assembly to your webpage it will have the TagPrefix assigned as ca.  There is a single line of code that you can add that will change this.  However, it comes with the need of an explanation.

This is ONLY true for VisualBasic.  When creating the TagPrefix, VisualBasic starts at the Root Namespace.  This is extremely important (and this DOES NOT apply to C#).  So, to figure out what your Root Namespace is, right-click your solution name of your server control in the solution explorer | Properties:


image



As you can see here, MY Root Namespace is TDN.Web (TheDotNetters.Web).  Due to the fact that in my Server Control, I did not declare a namespace, this is my entire namespace for this control.  Thus, my TagPrefix is built as so – TagPrefix(assembly namespace, TagPrefix), in code splice:




<Assembly: TagPrefix("TDN.Web", "DotNetters")>





Here is the entire code in its entirety (again, there are carriage returns that need to be removed when placed in the VS2010 DE):



Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.Management.Automation

<Assembly: TagPrefix("TDN.Web", "DotNetters")>
<DefaultProperty("ServerName"), ToolboxData("<{0}:ServerDriveInfo
runat=server></{0}:ServerDriveInfo>"
)>
Public Class ServerDriveInfo
Inherits WebControl

Dim CurrServer As String = String.Empty
Private lit As Literal

<Bindable(True), Category("Appearance"), DefaultValue(""),
Localizable(True)>
Property ServerName() As String
Get
Dim
s As String = CStr(ViewState("ServerName"))
If s Is Nothing Then
Return
CurrServer
Else
Return
s
End If
End Get

Set
(ByVal Value As String)
ViewState("ServerName") = Value
End Set
End Property

Protected Overrides Sub
CreateChildControls()
lit = New Literal()
lit.ID = "litReturnResults"
Dim shell = PowerShell.Create()
If CurrServer = String.Empty Then
shell.Commands.AddScript("Get-WmiObject -Class Win32_LogicalDisk
| Where-Object {$_.DriveType -ne 5} | Sort-Object -Property Name |
Select-Object Name, VolumeName, FileSystem, Description, VolumeDirty,
@{""Label""=""DiskSize(GB)"";""Expression""={""{0:N}"" -f ($_.Size/1GB)
-as [float]}}, @{""Label""=""FreeSpace(GB)"";""Expression""={""{0:N}""
-f ($_.FreeSpace/1GB) -as [float]}},
@{""Label""=""%Free"";""Expression""={""{0:N}"" -f ($_.FreeSpace/$_.Size*100)
-as [float]}} | convertto-html -fragment -as list |
ForEach {$_ -replace ""<table>"", ""<table width='100%'>""} |
ForEach {$_ -replace ""<td>"", ""<td style='padding-left:15px;'>""}"
)
Else
shell.Commands.AddScript("Get-WmiObject -Class Win32_LogicalDisk
-Computer "
& CurrServer & " | Where-Object {$_.DriveType -ne 5} |
Sort-Object -Property Name | Select-Object Name, VolumeName, FileSystem,
Description, VolumeDirty,
@{""Label""=""DiskSize(GB)"";""Expression""={""{0:N}"" -f ($_.Size/1GB)
-as [float]}}, @{""Label""=""FreeSpace(GB)"";""Expression""={""{0:N}"" –f
($_.FreeSpace/1GB) -as [float]}},
@{""Label""=""%Free"";""Expression""={""{0:N}"" -f ($_.FreeSpace/$_.Size*100)
-as [float]}} | convertto-html -fragment -as list | ForEach {$_
-replace ""<table>"", ""<table width='100%'>""} |
ForEach {$_ -replace ""<td>"", ""<td style='padding-left:15px;'>""}"
)
End If

Dim
results = shell.Invoke()
Dim strbuilder As StringBuilder = New StringBuilder
For Each result In results
strbuilder.Append(result.BaseObject.ToString())
Next
lit.Text = strbuilder.ToString()
'MyBase.CreateChildControls()
Me.Controls.Add(lit)
End Sub
Protected Overrides Sub
Render(writer As System.Web.UI.HtmlTextWriter)
lit.RenderControl(writer)
End Sub

End Class






And our code on our webpage (if you do not drag/drop from the toolbox), you will need to reference the assembly and then add the following code:



<%@ Register Assembly="TDN.Web" Namespace="TDN.Web" TagPrefix="DotNetters" %>





The above line obviously goes below your Page Declaration and for our control:



<DotNetters:ServerDriveInfo ID="ServerDriveInfo1" runat="server" />





Ahhh… Good Tymes!!! And that’s all there is to it!

We have created a Server Control and it implements PowerShell.  You can see how it is easy to manipulate the script and use other PowerShell commands.


You can also see how easy it would be to use in code behind if you only wanted to implement PowerShell in a single web page.



Håþþ¥ .ñꆆïñg…


No comments:

Post a Comment