Thursday, February 28, 2008 2:25 AM bart

Tiny IIS Manager - Layering MMC 3.0 snap-ins on top of Windows PowerShell

Back in manageability land. On TechEd EMEA Developers 2007 I delivered a talk on "Next-generation manageability: Windows PowerShell and MMC 3.0" covering the concept of layering graphical management tools (in this case MMC 3.0) on top of Windows PowerShell cmdlets (and providers). In this post, I'll cover this principle by means of a sample.

 

Introduction

It should be clear by now that Windows PowerShell is at the core of the next-generation manageability platform for Windows. First-class objects, ranging from .NET over COM to everything the Extended Type System can deal with (XML, ADSI, etc), together with scripting support allow people to automate complicated management tasks (combined with the v2.0 features on remoting and eventing this will only get better). Part of this vision is to layer management UIs on top of Windows PowerShell, which opens the door to broader discoverability: explore functionality in the UI, manage it over there and learn how the task would be done through the PowerShell CLI (command-line interface) directly, possibly wrapping it in a script for reuse in automation scenarios. On the development side this is also very appealing due to the fact the UI is a thin layer on top of the underlying cmdlet-based implementation which allows for better testing.

To lay the foundation for this post, please make sure to read the following tutorials:

We'll combine the two in a solution to create a sample layered management sample.

 

Step 0 - Solution plumbing

While thinking about this post I was wondering what to use as the running sample. Task managers layered on get-process are boring, similar for a Service Manager snap-in on top of get-service. Creating providers is too much to address in one post (my sample on TechEd created a provider to talk to a SQL database, allowing to cd to a table and dir it, exposing all of this to an MMC snap-in that hosted a Windows Forms DataGrid control). So I came up with the idea of writing a Tiny IIS Manager targeting IIS 7. This post assumes you've installed IIS 7 locally on your Windows Vista or Windows Server 2008 machine.

Before you start, make sure to run Visual Studio 2008 as administrator since we're going to launch Windows PowerShell loading a snap-in that requires administrative privileges.

Create a new solution called TinyIisManager:

image

Add two class library projects, one called TinyIisPS and another called TinyIisMMC. To configure the projects, follow my tutorials mentioned above:

Step 0.0 - Add the required references to the projects

This is how the end-result should look like:

image

Step 0.1 - Tweak the Debugger settings under the project properties

Again just the results (click to enlarge):

image image

Note: Make sure the paths to MMC and PS are set correctly on your machine. These settings won't work yet since we're missing the debug.* files (see below).

Step 0.2 - Add empty place-holders for the snap-ins (both PS and MMC)

Almost trivial to do if you've read the cookbook posts. Rename Class1.cs for the PS library into IisMgr.cs and add the following code:

image

Rename Class1.cs for the MMC library into IisMgr.cs and add the following code:

image

Step 0.3 - Build and register

Build both projects, open a Visual Studio 2008 Command Prompt running as administrator and cd into the bin\Debug folders for both projects to run installutil.exe against the created assemblies:

image

Step 0.4 - Creating debugging files

Open Windows PowerShell, add the registered snap-in and export the console file to debug.psc1 under the TinyIisPS project root folder:

image

Open MMC, add the registered snap-in (CTRL-M) and save the file as debug.msc under the TinyIisMMC project root folder:

image image

Don't worry about the empty node in the Selected snap-ins display - our constructor didn't set the node text (yet). Don't forget to close both MMC and Windows PowerShell.

Step 0.5 - Validate debugging

You should now be able to right-click any of the two projects and choose "Debug, Start new instance" to start a debugging session. Validate this is right: the MMC snap-in should load and the PS snap-in should be available:

image image

You're now all set to start the coding.

 

Step 1 - Building the Windows PowerShell layer

Let's start at the bottom of the design: the Windows PowerShell layer that will do all the real work. To keep things simple, we'll just provide a few cmdlets although bigger systems would benefit from providers too (so that you can navigate through a (optionally hierarchical) data store, e.g. to cd into virtual folder in an IIS website). We'll write just three cmdlets:

  • get-site - retrieves a list of sites on the local IIS 7 web server
  • start-site - starts a site
  • stop-site - stops a site

Feel free to envision other cmdlets of course :-). The API we'll use to talk to IIS is the new Microsoft.Web.Administration of IIS 7 which can be found under %windir%\system32\inetsrv, so let's import it (make sure you're under the right project: TinyIisPS):

image

Import the namespace Microsoft.Web.Administration to IisMgr.cs and add the following cmdlet classes (for simplicity I stick them in the same file - not recommended for manageability of your source tree :-)):

[Cmdlet(VerbsCommon.Get, "site")]
public class GetSiteCmdlet : Cmdlet
{
    protected override void ProcessRecord()
    {
        using (ServerManager mgr = new ServerManager())
        {
            WriteObject(mgr.Sites, true);
        }           
    }
}

public abstract class ManageSiteCmdlet : Cmdlet
{
    protected ServerManager _manager;

    [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)]
    public string Name { get; set; }

    protected override void BeginProcessing()
    {
        _manager = new ServerManager();
    }

    protected override void EndProcessing()
    {
        if (_manager != null)
            _manager.Dispose();
    }

    protected override void StopProcessing()
    {
        if (_manager != null)
            _manager.Dispose();
    }
}

[Cmdlet(VerbsLifecycle.Start, "site", SupportsShouldProcess = true)]
public class StartSiteCmdlet : ManageSiteCmdlet
{
    protected override void ProcessRecord()
    {
        Site site = _manager.Sites[ Name ];

        if (site == null)
        {
            WriteError(new ErrorRecord(new InvalidOperationException("Site not found."), "404", ErrorCategory.ObjectNotFound, null));
        }
        else if (site.State == ObjectState.Started || site.State == ObjectState.Starting)
        {
            WriteWarning("Can't start site.");
        }
        else if (ShouldProcess(site.Name, "Start"))
        {
            site.Start();
        }
    }
}

[Cmdlet(VerbsLifecycle.Stop, "site", SupportsShouldProcess = true)]
public class StopSiteCmdlet : ManageSiteCmdlet
{
    protected override void ProcessRecord()
    {
        Site site = _manager.Sites[ Name ];

        if (site == null)
        {
            WriteError(new ErrorRecord(new InvalidOperationException("Site not found."), "404", ErrorCategory.ObjectNotFound, null));
        }
        else if (site.State == ObjectState.Stopped || site.State == ObjectState.Stopping)
        {
            WriteWarning("Can't stop site.");
        }
        else if (ShouldProcess(site.Name, "Stop"))
        {
            site.Stop();
        }
    }
}

Just 80 lines of true power. Time for a quick check of the functionality. Run the TinyIisPS project under the debugger and play around a little with the cmdlets:

image

If you see messages like the one below, check you're running Visual Studio 2008 as an administrator which will fork the child Windows PowerShell debuggee process as administrator too:

image 

 

Step 2 - Building the graphical MMC layer on top of the cmdlets

Time to bump up our TinyIisMMC project. The first thing to do is to add a reference to the System.Management.Automation.dll assembly (the one used in the PS project to write the cmdlets) since we need to access the Runspace functionality in order to host Windows PowerShell in the context of our MMC snap-in:

image

Also add references to System.Windows.Forms (needed for some display) and Microsoft.Web.Administration (see instructions above - similar as in the PowerShell layer). We'll need this in order to use the objects returned by the PowerShell get-site cmdlet. Time to start coding again. Basically an MMC snap-in consists of:

  • The SnapIn class which acts as the root of the hierarchy; it adds nodes to its tree;
  • A tree of ScopeNode instances which get displayed in the tree-view;
  • Actions associated with the nodes;
  • View descriptions to render a node in the central pane.

We'll keep things simple and provide only the tree with a few actions and an HTML-based view on the item (which just loads the website - after tab-based browsing we now have tree-based browsing :-)). Let's start by the SnapIn class:

[SnapInSettings("{36D66A51-A9A4-4981-B338-B68D15068F5C}", DisplayName = "Tiny IIS Manager")]
public class IisMgr : SnapIn
{
    private Runspace _runspace;

    public IisMgr()
    {
        InitializeRunspace();

        this.RootNode = new SitesNode();
    }

    internal Runspace Runspace { get { return _runspace; } }

    private void InitializeRunspace()
    {
        RunspaceConfiguration config = RunspaceConfiguration.Create();

        PSSnapInException warning;
        config.AddPSSnapIn("IisMgr", out warning);

        // NOTE: needs appropriate error handling

        _runspace = RunspaceFactory.CreateRunspace(config);
        _runspace.Open();
    }

    protected override void OnShutdown(AsyncStatus status)
    {
        if (_runspace != null)
            _runspace.Dispose();
    }
}

In here, the core bridging with PowerShell takes place: we create a runspace (the space in which we run commands etc) based on some configuration object that has loaded the IisMgr PowerShell snap-in created in the previous paragraph. We also expose the runspace through an internal property so that we can reference it from the other classes used by the snap-in, such as SitesNode:

class SitesNode : ScopeNode
{
    public SitesNode()
    {
        this.DisplayName = "Web sites";
        this.EnabledStandardVerbs = StandardVerbs.Refresh;

        LoadSites();
    }

    protected override void OnRefresh(AsyncStatus status)
    {
        LoadSites();
        status.Complete("Loaded websites", true);
    }

    private void LoadSites()
    {
        this.Children.Clear();
        this.Children.AddRange(
            (from site in ((IisMgr)this.SnapIn).Runspace.CreatePipeline("get-site").Invoke()
             select new SiteNode((Site)site.BaseObject)).ToArray()
        );
    }
}

The constructor is easy: we add a display name to the node (no blankness anymore) and enable the "standard verb" Refresh (which will appear in the action pane). To handle Refresh, we overload Refresh. Notice MMC 3.0 support asynchronous loading (not to block the management console when an action is taking place) but let's not go there for now. In LoadSites the real stuff happens: we grab the Runspace through the internal property defined on the SnapIn, create a pipeline that simply invokes get-site and finally invoke it by calling Invoke. This produces a collection of PSObject objects, which are wrappers (used for the Extended Type System) around the original object (in our case a Microsoft.Web.Administration.Site object). Using a simple LINQ query we grab the results and wrap them in SiteNode objects (see below) which are added as the node's children.

class SiteNode : ScopeNode
{
    private Site _site;
    private Microsoft.ManagementConsole.Action _startAction;
    private Microsoft.ManagementConsole.Action _stopAction;
    private HtmlViewDescription _view;

    public SiteNode(Site site)
    {
        _site = site;

        this.DisplayName = site.Name;
        this.EnabledStandardVerbs = StandardVerbs.Properties | StandardVerbs.Refresh;

        _startAction = new Microsoft.ManagementConsole.Action() { Tag = "start", DisplayName = "Start" };
        this.ActionsPaneItems.Add(_startAction);
        _stopAction = new Microsoft.ManagementConsole.Action() { Tag = "stop", DisplayName = "Stop" };
        this.ActionsPaneItems.Add(_stopAction);

        Refresh();

        Microsoft.Web.Administration.Binding binding = _site.Bindings[0];
        _view = new HtmlViewDescription(new Uri(String.Format("{0}://{1}:{2}", binding.Protocol, binding.Host == "" ? "localhost" : binding.Host, binding.EndPoint.Port))) { DisplayName = "View site", Tag = "html" };

        this.ViewDescriptions.Add(_view);
    }

    protected override void OnAction(Microsoft.ManagementConsole.Action action, AsyncStatus status)
    {
        switch (action.Tag.ToString())
        {
            case "start":
                ((IisMgr)this.SnapIn).Runspace.CreatePipeline("start-site -name \"" + _site.Name + "\"").Invoke();
                break;
            case "stop":
                ((IisMgr)this.SnapIn).Runspace.CreatePipeline("stop-site -name \"" + _site.Name + "\"").Invoke();
                break;
        }

        Refresh();
    }

    protected override void OnAddPropertyPages(PropertyPageCollection propertyPageCollection)
    {
        propertyPageCollection.Add(new PropertyPage() {
            Title = "Website",
            Control = new PropertyGrid() {
                SelectedObject = _site,
                Dock = DockStyle.Fill
            }
        });
    }

    protected override void OnRefresh(AsyncStatus status)
    {
        Refresh();
    }

    private void Refresh()
    {
        _startAction.Enabled = _site.State == ObjectState.Stopped;
        _stopAction.Enabled = _site.State == ObjectState.Started;
    }
}

That's basically it. In the constructor we define a couple of custom actions for the "Stop" and "Start" actions. We enable the verbs for Properties and Refresh and provide some basic implementation for those (for properties we rely on the PropertyGrid control although in reality you'd want a much more customized view on the data that hides the real underlying object model). We also add an HTML view description that points at the URL of the website itself (normally you'd use different types of view descriptions in order to show items under that particular node, e.g. virtual folders for the website, or a bunch of 'control panel style' configuration options, as in the real inetmgr.exe). Again, the logic to invoke cmdlets is very similar, we just add some parameterization:

        switch (action.Tag.ToString())
        {
            case "start":
                ((IisMgr)this.SnapIn).Runspace.CreatePipeline("start-site -name \"" + _site.Name + "\"").Invoke();
                break;
            case "stop":
                ((IisMgr)this.SnapIn).Runspace.CreatePipeline("stop-site -name \"" + _site.Name + "\"").Invoke();
                break;
        }

and this time no data is returned (strictly speaking that's not true since errors will flow back through the runspace - feel free to play around with this).

 

Step 3 - The result

Time to admire the result. Launch the MMC snap-in project under the debugger:

image  image

The full code is available over here. Usual disclaimers apply - this is nothing more than sample code...

Enjoy!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under: , ,

Comments

# Tiny IIS Manager - Layering MMC 3.0 snap-ins on top of Windows PowerSh

Monday, March 03, 2008 1:40 AM by DotNetKicks.com

You've been kicked (a good thing) - Trackback from DotNetKicks.com

# Daily Bits - March 3, 2008 | Alvin Ashcraft's Daily Geek Bits

Pingback from  Daily Bits - March 3, 2008 | Alvin Ashcraft's Daily Geek Bits

# ui central 3.0

Wednesday, March 26, 2008 2:07 AM by ui central 3.0

Pingback from  ui central 3.0

# TechEd 2008 South Africa Demo Resources

Saturday, August 09, 2008 7:39 AM by B# .NET Blog

Last week, I had the honor to speak at TechEd 2008 South Africa on a variety of topics. In this post