lundi 14 février 2011

Using PowerShell 2.0 from ASP.NET Part 1

In this post, you’ll find a step-by-step guide on PowerShell 2.0 integration in an ASP.NET project.

I want to show you here the most basic sample possible, we’ll extend this in the following post on this subject.

This sample targets PowerShell 2.0 with IIS 7.0, but it should work on IIS 6.0. also. You can use an Express version of visual studio.

Ok, let’s get started !

Project setup

Open Visual Studio, choose File> New > Project

tuto1_thumb1

Choose Web Project > ASP.NET Web Application. Target any framework from 2.0 to 4.0 (this example was build targeting .NET 4.0)

tuto2_thumb3

Then we’ll add a reference to the PowerShell assembly called “System.Management.Automation”. Right click on References, choose “Add reference..”

tuto3_thumb2

Go to :

C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0

or

C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0

For a x86 OS.

tuto4_thumb2

Now we can start building our GUI.

Create the GUI

our GUI will be straight-forward : a textbox for PowerShell code, an execute button and a result textbox :

tuto5

Here’s the ASP.NET code for our default.aspx page

   1: <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="PowerShellCall._Default" %>
   2:  
   3: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   4:  
   5: <html xmlns="http://www.w3.org/1999/xhtml">
   6: <head runat="server">
   7:     <title></title>
   8: </head>
   9: <body>
  10:  
  11:     <form id="form1" runat="server">
  12:     <center>
  13:         <div>
  14:         <table>
  15:             <tr><td><h1>PowerShell Test</h1></td></tr>
  16:             <tr><td><h3>PowerShell Code</h3></td></tr>
  17:  
  18:             <tr><td>
  19:                 <asp:TextBox ID="PowerShellCodeBox" runat="server" TextMode="MultiLine" Width=700 Height=100></asp:TextBox>
  20:             </td></tr>
  21:  
  22:             <tr><td>
  23:                 <asp:Button ID="ExecuteCode" runat="server" Text="Execute" Width=200 onclick="ExecuteCode_Click" 
  24:                      />
  25:             </td></tr>
  26:  
  27:             <tr><td><h3>Result</h3></td></tr>
  28:         
  29:             <tr><td>
  30:                 <asp:TextBox ID="ResultBox" TextMode="MultiLine" Width=700 Height=200 runat="server"></asp:TextBox>
  31:             </td></tr>
  32:         </table>
  33:         </div>
  34:     </center>
  35:     </form>
  36:  
  37: </body>
  38: </html>

Our goal: when the user click on “Execute”, we want to launch the PowerShell code in the upper textbox and display script result on the bottom textbox.

Let’s have a look to the code behind of our default.aspx page.

Calling PowerShell 2.0 from Code-behind

As you can see on the aspx page, we have an “onclick” method on our button. You’ll find an onclick method defined in the .cs file that will be called when we click on the button (ExecuteCode_Click)

   1: protected void ExecuteCode_Click(object sender, EventArgs e)
   2:         {
   3:             // Clean the Result TextBox
   4:             ResultBox.Text = string.Empty;
   5:  
   6:             // Initialize PowerShell engine
   7:             var shell = PowerShell.Create();
   8:  
   9:             // Add the script to the PowerShell object
  10:             shell.Commands.AddScript(PowerShellCodeBox.Text);
  11:             
  12:             // Execute the script
  13:             var results = shell.Invoke();
  14:  
  15:             // display results, with BaseObject converted to string
  16:             // Note : use |out-string for console-like output
  17:             if (results.Count > 0)
  18:             {
  19:                 // We use a string builder ton create our result text
  20:                 var builder = new StringBuilder();
  21:                 
  22:                 foreach (var psObject in results)
  23:                 {
  24:                     // Convert the Base Object to a string and append it to the string builder.
  25:                     // Add \r\n for line breaks
  26:                     builder.Append(psObject.BaseObject.ToString() + "\r\n");
  27:                 }
  28:                 
  29:                 // Encode the string in HTML (prevent security issue with 'dangerous' caracters like < >
  30:                 ResultBox.Text = Server.HtmlEncode(builder.ToString());
  31:             }
  32:  
  33:         }

Execution of PowerShell code is really simple in C#, we only need to follow these steps :

update : updated the code, we don't need runspace creation in this scenario, already available. (thanks to Oisin). Added string builder and html encoding to prevent security warning when displaying some characters.

1) Create a PowerShell Object

This object let us create pipeline, collect result /errors and so on.

2) Add the script

Here we add our script to our PowerShell object

3) Execute script

At last, we call the Invoke() Method to execute our command in a pipeline. This method returns a collection of PSObject.

4) Display result

Then, we browse this PSObject collection to display the result. Note that we call the “BaseObject” property of each PSObject, this property hold the original object that was decorated in the PSObject object.

tuto5

That’s it ! now if you build this project, you should be able to display the result of your script in the result TextBox.

Now we’ll see how to publish this website, and how to specify a specific account for execution.

Website publication

Now that we have a working sample, we’ll publish this website in IIS and specify a custom identity for code execution.

Go to Build > Publish

tuto6

Choose a target location (Inetpub/wwwroot is the default repository folder for IIS, but you can publish your website in any directory, as long as you set up access rights properly)

tuto7

After that, open IIS management console, right click on the default web site and select “Add Application”

Note : you can also create a new website on your IIS server or delete the default website to create your own. Creating an application let you access your Website by it’s name like this : http://localhost/MyApplication

tuto8

Gives a name to your application or website, then click “OK”

tuto9

Now your application is ready, we’ll now choose a custom identity for our website. This identity will be used to execute your PowerShell scripts. There are many ways to set identity for a website, we’ll cover in this first post the most basic ones.

Anonymous authentication

This method is the easiest. We’ll define an anonymous authentication to our website, and choose a custom identity to run our commands. This is easy, but with severe drawbacks :

Anyone can access our website and will execute commands with the identity provided (we can mitigate this with specific access rights on the website folder, but this is clearly not the most secured method, you’re warned !)

Select your application, then click on the “Authentication” icon

tuto10

Right click on “anonymous authentication” and select “Activate”, then Edit…

tuto11

Choose “Specific "User”, click Settuto12

Then fill your service account identity

tuto13

That’s it : your code will be executed with this identity, regardless of the identity of the user.

Application Pool Identity

In more evolved scenarii, we can disable anonymous authentication and still use a custom identity for our website execution. This identity is set in an “Application Pool”.

Application Pools can be seen as an execution context for a website/web application (I make it short here), we won’t go in the details, but there’s many interesting features behind the application pool concept. One is to be able to set windows integrated authentication to authenticate user and use the application pool identity to run our code.

Note: we can set an Application pool and still use anonymous authentication, in the previous screen you have the option to use the application pool identity for your anonymous connection.

We’ll now setup our dedicated application pool for our test project.

First, double click on “Application Pools” in the IIS console, and select “Add Application Pool”

tuto30

Name your application pool, choose the appropriate Framework and click “OK” (you can leave the managed pipeline mode as-is)

tuto31

Select your application pool, click on the “Advanced Settings” menu

tuto32

Select Identity and click on the “…” button

tuto21

Choose “Custom Account”, click on “Set…”
tuto22

Fill the form with your service account infos, then click ok.

tuto23

Click on your website and choose “Basic Settings”

tuto33

Click on “Select” and choose your new application pool

tuto34

If everything went fine, you should be able to display the current service account identity :

tuto25

source can be downloaded here :



In the next post, we’ll see how to authenticate our website users and use ASP.NET forms data to configure our PowerShell script.

29 commentaires:

Antoine Habert a dit…

Thanks Muhammad for your kind message. I'll post part 2 tomorrow or friday

Anonyme a dit…

Cool stuff!
For 2008R2 the reference can be found at C:\Windows\assembly\GAC_MSIL\System.Management.Automation\1.0.0.0__31bf3856ad364e35\System.Management.Automation.dll

How can I integrate Exchange cmdlets?

Antoine Habert a dit…

Hi,

if you've installed the exchange snapin on the server, you can use

add-pssnapin Microsoft.Exchange.Management.PowerShell.Admin

or use remoting on your exchange server

mviton a dit…

I run into error:
The name 'Powershell' does not exist in the current context

var shell = Powershell.Create();

Antoine Habert a dit…

did you referenced the Powershell dll properly ?

Jay S a dit…

Running into this:


'PowerShellCall._Default' is not allowed here because it does not extend class 'System.Web.UI.Page'

any help would be appreciated.

Antoine Habert a dit…

Jay_S : when do this happen, when you first load the project ? did you modified something ?

Jay S a dit…

I added the reference then cut and past the page code and then tried to run it.

Antoine Habert a dit…

Download the sample project, you're missing something here.

Jay S a dit…

error opening the file....the imported project c:\program files(x86)\msbuild\microsoft\visualstudio\v10.0\webapplications\Microsoft.webapplication.targets" was not found. Confirm that the path in the declaration is correct and that the file exists on disk

Antoine Habert a dit…

What is your operating system ? x86 right ? which version of visual studio ?

Antoine Habert a dit…

You can copy/paste, just take care that the default.aspx line has a proper inherit settings

PowerShellCall._Default

you should replace "PowerShellCall" with the name of your project (namespace)

Bill a dit…

I've got your sample code working. Thanks for the excellent instructions!

Do you have some sample code for remoting to an Exchange 2010 server? I have found some but am not sure how to integrate it into your code.

Thanks!

Jay S a dit…

Is there a way you know of to have the script run as the user that is requesting and not the service account?

William a dit…

How could I replace the powershell code box with a pre defined poweshell script that the user can not make changes to?

I need to let the user run a PS script when they click the button and receive the output of the script in the results box.

Inheritx Solutions a dit…

This is nice and informative post, Thanks to post.

Asp.net Development

JohnCornor a dit…

these linkss are vry useful i hope dat i get myself placed in da interview as well..bt thnxx ya once again good wrk.

Unknown a dit…

Excellent tutorial - this does exactly what I am looking for. I have one question, though. Where do I add extra PowerShell snapins? (i.e. add-pssnapin Microsoft.Exchange.Management.PowerShell.Admin)?

Antoine Habert a dit…

i Felix,

Short answer: just add the add-pssnapin or import-module in your PowerShell script :-)

Long Answer: you can also provide an initial session state to the PowerShell runspace. Read this article from Oisin Grehan here.

Dean132 a dit…

Many Many thanks for this. I'd like to know if you got an answer William as I would like to be able to do this too.

I'd lile to have the script embedded in the site and the users just adds a username in the field, hits execute and it runs the script with the parameter the user has added.

Antoine Habert a dit…

Hi Dean132,

Thank you. If you want to launch a script, simply invoke it from your button code.

Instead of launching a custom code from a textbox, you just have to launch this code:

Invoke-Expression c:\scripts\test.ps1

replace the script path with your own script path.

Dean132 a dit…

Thank you very much for the reply. Ithink I am missing something obvious if I were to do that, how would I specify in my script to use the parameter in the text field?

Antoine Habert a dit…

Yeah I think you Miss something pretty simple here :)

as you know, a script accepts parameters, why don t you simply provide one with your script and adapt your invole-expression accordingly ?

Just replace the parameter value with your textbox input and that s it.

Invoke-expression c:\myscript -username #replaceme#

Richard Byrdk a dit…
Ce commentaire a été supprimé par l'auteur.
tc490225 a dit…

Im hoping someone can shed some light on what im doing wrong..

Im using the code as is with no modifications. I can get many Exchange 2010 cmdlets to execute properly.. such as get-mailbox domain\user etc..

However when I execute get-mailboxsearch I get no results!?? on the web server im getting the following error..

The description for Event ID 6 from source MSExchange CmdletLogs cannot be found. Either the component that raises this event is not installed on your local computer or the installation is corrupted. You can install or repair the component on the local computer.

If the event originated on another computer, the display information had to be saved with the event.

The following information was included with the event:

Get-MailboxSearch
{}
MYDOMAIN/Active Directory Admins/Privileged Accounts/Domain Admins/MYADMINACCT
Default Host-Local
5740
21
00:00:00.0625008
View Entire Forest: 'True',
System.InvalidOperationException: Operation is not valid due to the current state of the object.
at Microsoft.Exchange.Data.Storage.ExchangePrincipal.get_ServerFullyQualifiedDomainName()
at Microsoft.Exchange.Data.Storage.MailboxSession.Initialize(MapiStore linkedStore, LogonType logonType, ExchangePrincipal owner, DelegateLogonUser delegateUser, Object identity, OpenMailboxSessionFlags flags, GenericIdentity auxiliaryIdentity)
at Microsoft.Exchange.Data.Storage.MailboxSession.<>c__DisplayClass12.b__10(MailboxSession mailboxSession)
at Microsoft.Exchange.Data.Storage.MailboxSession.InternalCreateMailboxSession(LogonType logonType, ExchangePrincipal owner, CultureInfo cultureInfo, String clientInfoString, IAccountingObject budget, Action`1 initializeMailboxSession, InitializeMailboxSessionFailure initializeMailboxSessionFailure)
at Microsoft.Exchange.Data.Storage.MailboxSession.CreateMailboxSession(LogonType logonType, ExchangePrincipal owner, DelegateLogonUser delegateUser, Object identity, OpenMailboxSessionFlags flags, CultureInfo cultureInfo, String clientInfoString, PropertyDefinition[] mailboxProperties, IList`1 foldersToInit, GenericIdentity auxiliaryIdentity, IAccountingObject budget)
at Microsoft.Exchange.Data.Storage.MailboxSession.ConfigurableOpen(ExchangePrincipal mailbox, MailboxAccessInfo accessInfo, CultureInfo cultureInfo, String clientInfoString, LogonType logonType, PropertyDefinition[] mailboxProperties, InitializationFlags initFlags, IList`1 foldersToInit, IAccountingObject budget)
at Microsoft.Exchange.Data.Storage.MailboxSession.OpenAsAdmin(ExchangePrincipal mailboxOwner, CultureInfo cultureInfo, String clientInfoString, GenericIdentity auxiliaryIdentity, Boolean nonInteractiveSession)
at Microsoft.Exchange.Data.Storage.Infoworker.MailboxSearch.MailboxDataStore..ctor(ADUser adUser)
at Microsoft.Exchange.Data.Storage.Infoworker.MailboxSearch.MailboxDataProvider.FindPaged[T](QueryFilter filter, ObjectId rootId, Boolean deepSearch, SortBy sortBy, Int32 pageSize)
at Microsoft.Exchange.Configuration.Tasks.GetTaskBase`1.GetPagedData()
at Microsoft.Exchange.Configuration.Tasks.GetTaskBase`1.InternalProcessRecord()
at Microsoft.Exchange.Configuration.Tasks.GetObjectWithIdentityTaskBase`2.InternalProcessRecord()
at Microsoft.Exchange.Management.Tasks.GetMailboxSearch.InternalProcessRecord()
at Microsoft.Exchange.Configuration.Tasks.Task.ProcessRecord()
at System.Management.Automation.CommandProcessor.ProcessRecord()
0

the message resource is present but the message is not found in the string/message table

Izzy77 a dit…

I am trying to execute powercli commands. It seems the first one I execute works find any subsequent requests gives me a nullReferenceException.

For Example

In PowerShellCodeBox:
Add-PSSnapin vmware.vimautomation.core
connect-viserver -server ServerName
$DCs = Get-datacenter
foreach($DC in $DCs){$DC.name}

This will work fine when I click execute the first time. If I click again or try a different Powercli script, I get the following error:
nullReferenceException was unhandled by user code
Object reference not set to an instance of an object.

Not sure why it is passing null values for the same script that worked on previous execute.

Antoine Habert a dit…

you should catch PowerShell error and redirect them to an output textbox.

John Croson a dit…

Hope someone can help me troubleshoot this. I created my own project using methods outlined by you in this article, and consistently recieve a Unable to access Windows PowerShell PowerShellEngine registry information. My project is developed on Win7 64bit and deployed to Windows 2008 r2.

I also tried your project files and got the same results.

Thanks!.


Antoine Habert a dit…

Hi,

you may have referenced the wrong dll, see here: http://stackoverflow.com/questions/10906529/an-error-occurred-when-loading-the-system-windows-powershell-snap-ins