Creating a generic ping proxy

In the previous post we walked through the steps required to implement a service monitoring ‘Ping’ operation as a WCF endpoint behavior. This allows us to add the ping functionality to an endpoint using WCF configuration alone. Following on from this, here’s a generic implementation of a Http proxy class to call Ping on any service.

Last time we left off having created a proxy to a test service using the WcfTestClient application.

Ping operation from metadata in the WCF Test Client

Switching to the XML view lets us see the SOAP request and reply message for a ping.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://aderant.com/expert/contract/TestServiceContract/IService1/Ping</Action>
  </s:Header>
  <s:Body>
    <Ping xmlns="http://aderant.com/expert/contract/TestServiceContract" />
  </s:Body>
</s:Envelope>

and the response:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header />
  <s:Body>
    <PingResponse xmlns="http://aderant.com/expert/contract/TestServiceContract">
      <PingResult>2011-10-18T01:28:57.0222683Z</PingResult>
    </PingResponse>
  </s:Body>
</s:Envelope>

The namespace highlighted in green is the namespace we gave to our service as part of its ServiceContract attribution:

[ServiceContract(Namespace="http://aderant.com/expert/contract/TestServiceContract")]
public interface IService1 {

The SOAP message varies from service to service according to the namespace of the contract and the type of the contract. If we know these two values then we can construct the appropriate SOAP message for a generic client.

private static void CallWcfServiceUsingGenericProxy() {
    const string address = @"http://localhost/TestService/Service1.svc";
    const string serviceContractType = "IService1";
    const string serviceContractNamespace = @"http://aderant.com/expert/contract/TestServiceContract";
    Console.WriteLine("Pinging WCF service using the generic proxy...");
    DateTime utcStart = DateTime.UtcNow;
    string response = PingService(serviceContractType, serviceContractNamespace, address);
    DateTime utcFinished = DateTime.UtcNow;
    DateTime serverPingTimeUtc = ProcessPingResponse(response, serviceContractNamespace);
    WriteTimingMessage(utcStart, serverPingTimeUtc, utcFinished);
}

The calling of the generic proxy is separated from the processing of the response so that a timing can be made around just the communication.

private static string PingService(string serviceContractType, string contractNamespace, string address) {
    string pingSoapMessage = string.Format(@"", contractNamespace);
    if (!contractNamespace.EndsWith("/")) { contractNamespace = contractNamespace + "/";}
    WebClient pingClient = new WebClient();
    pingClient.Headers.Add("Content-Type", "text/xml; charset=utf-8");
    pingClient.Headers.Add("SOAPAction", string.Format(@"""{0}{1}/Ping""", contractNamespace, serviceContractType));
    string response = pingClient.UploadString(address, pingSoapMessage);
    return response;
}

To Ping the service, we construct the SOAP message and use a WebClient to make the call. The web client requires headers to be added for the content type and the SOAPAction which tells the WCF Dispatcher which method we want to call.

To process the SOAP message returned from the Ping message we use:

private static DateTime ProcessPingResponse(string response, string contractNamespace) {
    XDocument responseXml = XDocument.Parse(response);
    XElement pingTime = responseXml.Descendants(XName.Get("PingResult", contractNamespace)).Single();
    DateTime serverPingTimeUtc = DateTime.Parse(pingTime.Value).ToUniversalTime();
    return serverPingTimeUtc;
}

Now we have the UTC DateTime from the server when it processed the response.

All good, if we know the contract namespace and interface type for the service we can ping it. As we saw, this information is attributed on the service contract class. To simplify the proxy code a little, we can use reflection to determine this information given just the contract type.

public class WcfServicePinger<TServiceContract> where TServiceContract : class {
    public DateTime Ping(string addressUri) {
        string @namespace = string.Empty;
        ServiceContractAttribute attribute = typeof(TServiceContract)
            .GetCustomAttributes(typeof(ServiceContractAttribute), true)
            .FirstOrDefault() as ServiceContractAttribute;
        if(attribute == null) {
            throw new ArgumentException("The specified type {0} is not a WCF service contract.", typeof(TServiceContract).Name);
        }
        if(string.IsNullOrWhiteSpace(attribute.Namespace)) {
            @namespace = "http://tempuri.org";
        } else {
            @namespace = attribute.Namespace;
        }
        return new WcfServicePinger().Ping(addressUri, typeof(TServiceContract).Name, @namespace);
    }
}

The non-generic WcfServicePinger calls our previous code, as above:

public class WcfServicePinger {
    public DateTime Ping(string addressUri, string serviceContractTypename, string serviceContractNamespace) {
        string response = PingService(serviceContractTypename, serviceContractNamespace, addressUri);
        DateTime serverPingTimeUtc = ProcessPingResponse(response, serviceContractNamespace);
        return serverPingTimeUtc;
    }

So in the end we would use:

DateTime serverTime = new WcfServicePinger<IService1>().Ping(“http://localhost/TestService/Service1.svc”);

Note that we have constructed a generic proxy that calls an Http endpoint. I did try to construct a net.tcp generic proxy class too but it broke my time box. The Ping method can be called via net.tcp using a proxy generated from the endpoint metadata.

Checking WCF Service Availability using an Endpoint Behavior

A common operational requirement for an SOA is the ability to determine if a service is available. Just as it is common to ping a machine, we also want to be able to ping an individual service to determine that it is running and able to respond to messages.

Our first pass at solving this issue was to introduce an IPingable interface that each of our service facades would implement. The code was pushed into our service base class and the software factory updated as appropriate. However, this didn’t feel quite right. Other service extensions such as the metadata endpoint were not so invasive, it can be established by a simple addition to the service configuration file. We felt we wanted the same configurable nature for a ping mechanism and so set out to figure out how to create a custom WCF behavior.

This post walks through the creation of a WCF Endpoint Behavior which will add a Ping() method to a service. The method returns the DateTime, in UTC, stating when the server processed the request. This can be used to establish how long a basic roundtrip to the service is taking for both the request and the reply. The use of an endpoint behavior allows us to add this functionality to any service, therefore we can add it retrospectively to services that we have already created without needing to change them.

The source code for this post is available from my DropBox.

Let’s start by looking at how we would configure such an endpoint behavior. For an existing service, we would copy the assembly containing the endpoint behavior into the bin folder for the service and then add some entries into the web.config:

<configuration>
    <system.serviceModel>
        <extensions>
            <behaviorExtensions>
                <add name="Ping" type="CustomWcfBehaviors.PingEndpointBehavior, CustomWcfBehaviors" />
            </behaviorExtensions>
        </extensions>
        <behaviors>
            <endpointBehaviors>
                <behavior>
                    <Ping>
                </behavior>
            </endpointBehaviors>
        </behaviors>

We register the type [CustomWcfBehaviors.PingEndpointBehavior in assembly CustomWcfBehaviors] that is our custom behavior as a behavior extension and then declare that we want to use the extension. The configuration above sets up a default behavior that will be applied to all endpoints since we haven’t explicitly named it. That’s it, fetch the metadata for the service it will now contain a Ping() method.

The configuration of the behavior is reasonably straight forward, let’s look at the implementation…

namespace CustomWcfBehaviors {
    public class PingEndpointBehavior: BehaviorExtensionElement, IEndpointBehavior {
    private const string PingOperationName = "Ping";
    private const string PingResponse = "PingResponse";

The behavior needs to derive from BehaviorExtensionElement which allows it to be declared in the service configuration as an extension element. We need to introduce two overrides:

// factory method to construct an instance of the behavior
protected override object CreateBehavior(){
    return new PingEndpointBehavior();
}
// property used to determine the type of the behavior
public override Type BehaviorType{
    get { return typeof(PingEndpointBehavior); }
}

Now onto the real work, the IEndpointBehavior implementation:

public void Validate(ServiceEndpoint endpoint) {}
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) {}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) {}

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) {
    if(PingOperationNotDeclaredInContract(endpoint.Contract)) {
        AddPingToContractDescription(endpoint.Contract);
    }
    UpdateContractFilter(endpointDispatcher, endpoint.Contract);
    AddPingToDispatcher(endpointDispatcher, endpoint.Contract);
}

There are four methods that we need to implement but only one that we do any work in. The ApplyDispatchBehavior is where we add our code to manipulate how the endpoint invokes the service operations.

Endpoints map to the ABC of WCF [address, binding and contract]. What we want to do is extend the contract with a new service operation. A service host may allow multiple endpoints to expose the service contract on different protocols. For example, we may choose to host our service using both http and net.tcp. If we use IIS as our service host, this is configured via the Advanced Settings… of the Manage Application… context menu option.

The ‘Enabled Protocols’ lists the protocols we want to use. If we have more than one protocol listed then we have to take care that we don’t attempt to add the new Ping operation multiple times to our contract – once per binding type. To check that we haven’t already added the operation we call a small Linq query…

private bool PingOperationNotDeclaredInContract(ContractDescription contract) {
    return ! contract
        .Operations
        .Where(operationDescription =>
            OperationDescription.Name.Equals(PingOperationName,
                StringComparison.InvariantCultureIgnoreCase))
        .Any();
}

If the Ping operation is not found then we need to add it:

private void AddPingToContractDescription(ContractDescription contractDescription) {
    OperationDescription pingOperationDescription = new
    OperationDescription(PingOperationName, contractDescription);
    MessageDescription inputMessageDescription = new MessageDescription(
        GetAction(contractDescription, PingOperationName), MessageDirection.Input);
    MessageDescription outputMessageDescription = new MessageDescription(
        GetAction(contractDescription, PingResponse), MessageDirection.Output);
    MessagePartDescription returnValue = new MessagePartDescription("PingResult",
        contractDescription.Namespace);
    returnValue.Type = typeof(DateTime);
    outputMessageDescription.Body.ReturnValue = returnValue;
    inputMessageDescription.Body.WrapperName = PingOperationName;
    inputMessageDescription.Body.WrapperNamespace = contractDescription.Namespace;
    outputMessageDescription.Body.WrapperName = PingResponse;
    outputMessageDescription.Body.WrapperNamespace = contractDescription.Namespace;
    pingOperationDescription.Messages.Add(inputMessageDescription);
    pingOperationDescription.Messages.Add(outputMessageDescription);
    pingOperationDescription.Behaviors.Add(new DataContractSerializerOperationBehavior(pingOperationDescription));
    pingOperationDescription.Behaviors.Add(new PingOperationBehavior());
    contractDescription.Operations.Add(pingOperationDescription);
}

Here we are creating an OperationDescription to add to our ContractDescription, this adds the operation specification for our Ping operation to the existing contract. The code to execute is encapsulated by the PingOperationBehavior().

public class PingOperationBehavior : IOperationBehavior {
    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) {
        dispatchOperation.Invoker = new PingInvoker();
    }
    public void Validate(OperationDescription operationDescription) {}
    public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters) {}
    public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) {}
 }

Similar to the endpoint behavior, we need to implement an interface in this case the IOperationBehavior. The method signatures are similar and we need to fill out the ApplyDispatchBehavior method to call an IOperationInvoker to execute our Ping implementation:

internal class PingInvoker : IOperationInvoker {
    public object[] AllocateInputs() {
        return new object[0];
    }
    public object Invoke(object instance, object[] inputs, out object[] outputs) {
        outputs = new object[0];
        return DateTime.UtcNow;
    }
    public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state) {
        throw new NotImplementedException();
    }
    public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result) {
        throw new NotImplementedException();
    }
    public bool IsSynchronous {
        get { return true; }
    }
 }

The ping operation is so simple that there is no asynchronous implementation. All we do is return the current date time on the server in UTC.

So where are we? Well, we’ve added the Ping method to our existing service contract and mapped it to an IOperationBehavior which will dispatch an IOperationInvoker to call the code.

Next up, we have to update the endpoint dispatcher so that it knows what to do if it receives a Ping request message. The endpoint dispatcher maintains a list of actions that it knows how to action. We need to update this list so that it includes our new ping action. To do this we just refresh the action list from the contract description operations:

private void UpdateContractFilter(EndpointDispatcher endpointDispatcher, ContractDescription contractDescription) {
    string[] actions = (from operationDescription in contractDescription.Operations
                        select GetAction(contractDescription, operationDescription.Name)
                       ).ToArray();
    endpointDispatcher.ContractFilter = new ActionMessageFilter(actions);
}

Finally we need to add a new dispatch operation to the endpoint dispatcher so that it calls our PingOperation when the Ping action is received.

private void AddPingToDispatcher(EndpointDispatcher endpointDispatcher, ContractDescription contractDescription) {
    DispatchOperation pingDispatchOperation = new DispatchOperation(endpointDispatcher.DispatchRuntime,
        PingOperationName,
        GetAction(contractDescription, PingOperationName),
        GetAction(contractDescription, PingResponse));

    pingDispatchOperation.Invoker = new PingInvoker();
    endpointDispatcher.DispatchRuntime.Operations.Add(pingDispatchOperation);
}

private string GetAction(ContractDescription contractDescription, string name) {
    string @namespace = contractDescription.Namespace;
    if(!@namespace.EndsWith("/")) { @namespace = @namespace + "/"; }
    string action = string.Format("{0}{1}/{2}", @namespace, contractDescription.Name, name);
    return action;
}

Well, we are now done on the server side. We’ve created a couple of new classes:
• PingEndpointBehavior
• PingOperationBehavior
• PingInvoker

These three classes allow us to add a Ping method to a service by adding the Ping behavior via the service configuration file.

To test this, you can use the WcfTestClient application. The sample code demonstrates this by calling Ping on a standard WCF and WF service created by Visual Studio, see my DropBox.

In the next post I’ll discuss how we create a generic service proxy to call Ping.

UPDATE: the code works for the basicHttpBinding but not for wsHttpBinding.

UPDATE: Code now available from github.com/stefsewell/WCFPing

Securing WF & WCF Services using Windows Authentication

To finish off the DEV404 session Pete and I presented at TechEd NZ, I gave a brief run through of the steps required to get Windows Authentication working in a load balanced environment using kerberos. Given the number of camera phones that appeared for snaps I’m going to assume this is a common problem with a non-intuitive solution…

The product I work on is an on-premise enterprise solution that uses the Windows Identity to provide an authenticated credential against which to authorize user requests. We host our services in IIS/Windows Server AppFabric and take advantage of the Windows Authentication provided by IIS. This allows one of two protocols to be used: kerberos and NTLM, which have quite separate characteristics.

Why Use Kerberos?
There are two main reasons we want to use kerberos over NTLM:

1. Performance: NTLM uses a challenge response pattern for authentication which leads to a high network utilization. During performance testing we saw a high volume of NTLM challenges which ultimately throttled our ability to serve requests. Kerberos uses tickets which can be cached permitted a better performing protocol.

1. Double hops: NTLM does not flow credentials – the canonical example is a user requesting serviceA on server1 to access a secured resource on server2. Server1 cannot flow the users identity to server2.

Kerberos and Load Balancing
We want to run our services within a load balanced cluster to avoid single points of failure and to be able to grow resources to meet demand as required, without having to adopt bigger tin. The default configuration of IIS does not encourage this… the Application Pools run as a local machine account. This is a significant issue for Kerberos because of the manner in which the protocol encrypts the tickets passed between client, TGS and target server. The password of the account running the service is used to encrypt tickets so that only a process running under that account can decrypt the message. The default use of a machine specific account prevents a ticket granting access to serviceX on server A also being used to access serviceX on server B.

The following steps are required to fix this:

1. Use a common domain account for the applications pools.

We use a DOMAIN\service.expert account to run our services. This domain account is granted log on as a service and log on as a batch job rights on each of the application servers.

2. Register an SPN mapping the service class to the account.

We run our services on HTTP and so register the load balancer address with the domain account used to run the services:

>setspn -a HTTP/clusteraddress serviceAccount

We are using the WCF BasicHttpBinding which does not require the client to ensure the service is running as a particular user (to prevent man in the middle attacks). If you are using any other type of binding then the client needs to state who it expects the service to be running as.

3. Configure IIS to use the application pool account rather than a machine account

system.webServer/security/authentication/windowsAuthentication useAppPoolCredentials must be set to true.

4. Configure IIS to allow kerberos authentication tokens to be cached

system.webServer/security/authentication/windowsAuthentication authPersistNonNTLM must be set to true.

See also http://support.microsoft.com/kb/954873

5. Ensure the cluster address is considered to be in the Local Intranet zone


Kerberos tokens are not supported in the Internet zone, therefore the URL for your services must be considered to be trusted. The standard way to implement this is to roll out a group policy that adds your domain to the local intranet zone settings.

The slide deck for the talk is available from http://public.me.com/stefsewell/

DEV404 – Hardcore Workflow 4

Thanks to everyone who attended the DEV404 session at TechEd NZ. We wanted to cover off some new material that we haven’t seen else where and so Pete concentrated on the extensibility of the WorkflowServiceHost and the WorkflowServiceHostFactory. Before we got there I felt we needed a lead in and so I gave a brief overview of the workflow runtime, much of the material was covered in depth at PDC09 in the session Workflow 4 Inside Out.

The key point was the single threaded nature of the workflow scheduler. There is a single thread responsible for scheduling the execution of the activities in the activity tree, you really do not want to block this thread. This is the thread that runs the Execute method of synchronous activities, to show this in action I built the following workflow:

There’s a collection of strings populated with URLs of a few well known websites.

Then, there is a ParallelForEach that iterates over the collection and fetches the contents of the web page. The FetchUrl activity was written as follows:

using System.Activities;
using System.IO;
using System.Net;

namespace WorkflowRuntime.Activities {
    /// <summary>
    /// Fetch HTTP resource synchronously 
    /// </summary>
    public sealed class FetchUrlSync: CodeActivity {
        public InArgument Address { get; set; }
        protected override string Execute(CodeActivityContext context) {
            string address = context.GetValue(Address);
            string content = string.Empty;
            WebRequest request = HttpWebRequest.Create(address); 

            using(HttpWebResponse response = request.GetResponse() as HttpWebResponse) {
                if(response != null) {
                    using (Stream stream = response.GetResponseStream()) {
                        if (stream != null) {
                            StreamReader reader = new StreamReader(stream);
                            content = reader.ReadToEnd();
                        }
                    }
                }
            }
            return content;
        }
    }
}

The HttpWebRequest class is used to fetch the page contents. Running the workflow gives the following results:

The Urls are fetched one at a time, the same behavior that you would see if the activities were scheduled in a sequence rather than in a parallel. Why? This is the single threaded scheduler, it must wait for the Execute() method of the activity to complete before it can schedule the next activity.

What we want to see is:

So how can we achieve this? Well, we have to rewrite the FetchUrl activity to perform its work asynchronously. The HttpWebRequest already has async support via the BeginGetResponse and EndGetResponse method pairs; this is a standard pattern in .NET for async programming. The FetchUrl activity becomes:

using System;
using System.Activities;
using System.IO;
using System.Net;

namespace WorkflowRuntime.Activities {
    /// <summary>
    /// Fetch HTTP resource asynchronously
    /// <summary>
    public sealed class FetchUrlAsync: AsyncCodeActivity {
        public InArgument Address { get; set; }

        protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state) {
            string address = context.GetValue(Address);
            WebRequest request = HttpWebRequest.Create(address);
            context.UserState = request;
            return request.BeginGetResponse(callback, state);
        }

        protected override string EndExecute(AsyncCodeActivityContext context, IAsyncResult result) {
            string content = string.Empty; WebRequest request = (WebRequest) context.UserState;
            using (HttpWebResponse response = request.EndGetResponse(result) as HttpWebResponse) {
                if (response != null) {
                    using (Stream stream = response.GetResponseStream()) {
                        if (stream != null) {
                            StreamReader reader = new StreamReader(stream);
                            content = reader.ReadToEnd();
                        }
                    }
                }
            }
            return content;
        }
    }
}

We call the BeginGetResponse method passing in the callback and state object given to us by the workflow runtime as part of the AsyncCodeActivity.BeginExecute method. When the fetch is completed, by a separate worker thread, the workflow runtime is called back and the EndExecute method is invoked. In this method we take the resultant stream and read the contents into a string that we return. The workflow scheduler thread is no longer responsible for fetching the content, therefore it can schedule the fetch of the next Url and we get the parallel behavior we expect. All fetches are scheduled and then the workflow runtime waits to be called back by each worker thread when complete.

The time taken for the synchronous fetches to complete is the sum total of all fetches. For the asynchronous fetches, it is the time of the longest fetch plus a little overhead.

A basic rule of workflow is to perform I/O asynchronously and not to block the scheduler thread.

The sample code and PPT deck is available from https://public.me.com/stefsewell

Co-ordinating deployments using the Parallel class in .NET 4.0

It’s been a long time since the last entry, the new year brings with it a fresh post based on some of the deployment work I’ve been looking at recently. This work has opened my eyes to the support for parallel co-ordination of work within .NET 4…

Recently I’ve been looking at the deployment approach we have for our services with an eye to reducing the time it takes for a full deployment. There are two simple concepts that leapt out: the first is to use a pull rather than a push model; the second is to deploy to all of the servers in parallel. This second point becomes increasing important as more servers get involved in hosting the services.

Pull versus Push
One of the most basic operations performed by the deployment engine is the copying of files to the application servers that host the various services within our product. The file copying was originally implemented as a push: the deployment agent performs the copy to the target server using an administration share, e.g. \\appserver01.domain.com\d$\AderantExpert\Live\ . This requires the deployment engine to run with administrator privilege on the remote machines which is not ideal.

An alternative is to send a script to the target server containing the copy the commands, the target server is then responsible for pulling the file to its local storage from a network share (which can be secured appropriately). The deployment engine is responsible for creating the script from the deployment model and co-ordinating the execution of the scripts across the various application servers.

PowerShell remoting is a great option for the remote execution of scripts and it’s quite straight forward to transform an object model into a PowerShell script using LINQ. I created a small script library class that provides common functions, for example:

internal class PowerShellScriptLibrary {
    internal static void ImportModules(StringBuilder script) {
    script.AppendLine("import-module WebAdministration");
    script.AppendLine("import-module ApplicationServer");
}

internal static void StopWindowsServices(string filter, StringBuilder script) {
    script.AppendLine("# Stop Windows Services");
    script.AppendLine(string.Format("Stop-Service {0}", filter));
}

internal static void CreateTargetDirectories(string rootPath, IEnumerable fileSpecifications, StringBuilder script) {
    script.AppendLine("# Create the required folder structure");
    fileSpecifications
        .Where(spec => !string.IsNullOrWhiteSpace(spec.TargetFile.TargetRelativePath))
        .Select(x => x.TargetFile)
        .Distinct()
        .ToList()
        .ForEach(targetFile => {
            string path = Path.Combine(rootPath, targetFile.TargetRelativePath);
            script.AppendLine(string.Format("if(-not(Test-Path '{0}'))", path));
            script.AppendLine("{");
            script.AppendLine(string.Format("\tNew-Item '{0}' -ItemType directory", path));
            script.AppendLine("}");
        });
 }


The library is then used to create the required script by calling the various functions, the examples below are for the patching approach that allows updates to be installed without requiring a full remove and redeploy:

private string GenerateInstallScriptForPatch(Server server, IEnumerable filesToDeploy, Environment environment, string patchFolder) {
    StringBuilder powershellScript = new StringBuilder();

    PowerShellScriptLibrary.ImportModules(powershellScript);
    PowerShellScriptLibrary.StopWindowsServices("ADERANT*", powershellScript);
    PowerShellScriptLibrary.StopAppFabricServices(environment, powershellScript);
    PowerShellScriptLibrary.CreateTargetDirectories(server.ExpertPath, filesToDeploy, powershellScript);
    PowerShellScriptLibrary.CreatePatchRollback(server, patchFolder, filesToDeploy, powershellScript);
    PowerShellScriptLibrary.CopyFilesFromSourceToServer(environment, server, filesToDeploy, powershellScript);
    PowerShellScriptLibrary.UpdateFactoryBinFromExpertShare(server, environment.NetworkSharePath, powershellScript);
    PowerShellScriptLibrary.StartAppFabricServices(environment, powershellScript);
    PowerShellScriptLibrary.StartWindowsServices("ADERANT*", powershellScript);

    return powershellScript.ToString();
}

Though it is possible to treat NTFS as a transactional system (see http://msdn.microsoft.com/en-us/library/bb968806(v=VS.85).aspx ), and therefore have it participate in atomic actions, I didn’t walk this path. Instead I chose the compensation route and so when the model is transformed into a script I create both an install script and a compensate script which is executed in the event of anything going wrong.

private string GenerateRollbackScriptForPatch(Server server, IEnumerable filesToDeploy, Environment environment, string patchFolder) {
    StringBuilder powershellScript = new StringBuilder();

    PowerShellScriptLibrary.ImportModules(powershellScript);
    PowerShellScriptLibrary.StopWindowsServices("ADERANT*", powershellScript);
    PowerShellScriptLibrary.StopAppFabricServices(environment, powershellScript);
    PowerShellScriptLibrary.RollbackPatchedFiles(server, patchFolder, filesToDeploy, powershellScript);
    PowerShellScriptLibrary.StartAppFabricServices(environment, powershellScript);
    PowerShellScriptLibrary.StartWindowsServices("ADERANT*", powershellScript);

    return powershellScript.ToString();
}

The scripts simply take a copy of the existing files that will be replaced before replacing them with the new versions. If anything goes wrong during the patch install, the compensating script is executed to restore the previous files.

Given that a server specific script is now generated per application server, because different servers host different roles and therefore require different files, the deployment engine has the opportunity to pass the script to the server; ask it to execute it and then wait for the OK from each server. If one server has an error then all can have the compensation script executed as required.

Parallelizing a deployment
Before looking at ome co-ordination code for the deployment engine, I want to explicitly note that there are two different and often confused concepts:
• Asynchronous execution
• Parallel execution

An asynchronous execution involves a call to begin a method and then a callback from that method when the work is complete. IO operations are natural candidates for asynchronous calls to ensure that the calling thread is not blocked waiting on the IO to complete. Single threaded frameworks such as UI are the most common place to see a push for asynchronous programming. In .NET 3, the Windows Workflow Foundation provided an excellent asynchronous programming model where asynchronous activities are co-ordinated by a single scheduler thread. It is bad practice to have this scheduler thread block or perform long running operations as it stalls the workflow progress when in a parallel activity. It is better to schedule multiple asynchronous activities in parallel when possible and have these execute on separate worker threads.

Parallel execution involves breaking a problem into small parts that can be executed in parallel due to the multi-core nature of todays CPUs. Rather than having a single core work towards an answer, many cores can participate in the calculation. To reduce the elapsed time, the time experienced by the end user, of a calculation, it may be possible to execute a LINQ query over all available cores (typically 2, 4 or 8). Linq now has the .AsParallel() extension method which can be applied to queries to enable parallel execution of the query. Of course, profiling is required to determine if the query performs better in parallel for typical data sets.

.NET 4 added the Task Parallel Library into the core runtime. This library adds numerous classes to the BCL to make parallel programming and the writing of co-ordination logic much simpler. In particular the Parallel class can be used to easily schedule multiple threads of work. For example:

Parallel.Invoke(
    () => Parallel.ForEach(updateMap, server =>
        serverInstallationScripts.Add(server.Key, GenerateInstallScriptForPatch(server.Key, server.Value, environment, patchFolder))),
    () => Parallel.ForEach(updateMap, server =>
        serverRollbackScripts.Add(server.Key, GenerateRollbackScriptForPatch(server.Key, server.Value, environment, patchFolder)))
);

The above code is responsible for creating the install and compensate PowerShell scripts from the deployment model discussed above. There are two levels of parallelism going on here. First the generation of the install and compensate scripts are scheduled at the same time using a Parallel.Invoke() call. Then a Parallel.ForEach() is used to generate the required script for each application server defined in the environment in parallel. The runtime is responsible for figuring out how best to achieve this, as a programmer we simply declare what we want to happen. In the above code the updateMap is an IDictionary<server, IList>, this is a list of files to deploy to each server keyed on the server.

I was simply blown away by how simple and yet how powerful this programming model is.

Accessing the LSA from managed code

This blog entry would be filed under the ‘it should not be this hard’ category if I had one. A reasonably common requirement is to determine the rights a user has and then to add additional rights as necessary. After much searching I could not find a ‘managed’ way to do this so I ended up with the following…

This post very much stands on the shoulders of others and so here are the links to the original articles I used:

LSA .NET from Code Project
“RE: Unmarshalling LsaEnumerateAccountRights() list of privileges”

When installing a new service, it is often necessary to add additional rights to the user that the service runs as, for example ‘Log on a service’. To do so from either managed code or PowerShell would seem like a reasonably obvious ask but I could not find any type that allowed me to. The security information is managed by the Local Security Authority (LSA) which has an unmanaged API available from advapi32.dll, to access this from C# requires P/Invoke and a reasonable amount of code to marshall the types. I’m not a C++ programmer and so I first looked for an alternative.

The Windows Server 2003 Resource Kit includes a utility NTRights.exe which allows rights to be added and removed from a user via the command line. Unfortunately this tool no longer ships in the Windows Server 2008 Resource Kit but the 2003 version still works on both Windows 7 and Server 2008 (R2). The tool provided part of the solution but I also wanted to be able to find out the rights that have already been assigned to the user as well as add and remove.

No matter which way I turned, I was always led back to the advapi32 and writing a wrapper to allow the functions to be called from C#. Thankfully most of the hard work had already been done and documented by Corinna John, with a sample project posted on Code Project. The original article comes from 2003, so I was a little surprised that it still hasn’t made it into a managed library. The sample by Corinna showed show to add rights to a user but unfortunately did not include listing the rights. For that I have to thank Seng who lists sample code here.

By combining the efforts of both together and cleaning up the code a little I ended up with the wrapper class given at the end of the posting (there is plenty of room for improvement in my code). This was compiled in VS2010, the API ended up as:

public IList GetRights(string accountName)
public void SetRight(string accountName, string privilegeName)
public void SetRights(string accountName, IList rights)

I had to compile for .NET 2.0 so that I could call it from PowerShell…

[void][Reflection.Assembly]::LoadFile('C:\Samples\LSAController.dll') # void suppresses the output of the message text
$LsaController = New-Object -TypeName 'LSAController.LocalSecurityAuthorityController'
$LsaRights = New-Object -TypeName 'LSAController.LocalSecurityAuthorityRights' # a convenience class containing common rights
$LsaController.SetRight('ADERANT_AP\stefan.sewell', [LSAController.LocalSecurityAuthorityRights]::LogonAsBatchJob)
$LsaController.GetRights('ADERANT_AP\stefan.sewell')

The code for the wrapper follows, I hope this saves someone the 2 days I spend on this.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
//
// This code has been adapted from http://www.codeproject.com/KB/cs/lsadotnet.aspx
// The rights enumeration code came from http://www.tech-archive.net/Archive/DotNet/microsoft.public.dotnet.framework.interop/2004-11/0394.html
//
// Windows Security via .NET is covered on by Pluralsight:http://alt.pluralsight.com/wiki/default.aspx/Keith.GuideBook/HomePage.html
//

namespace LSAController {
    //
    // Provides methods the local security authority which controls user rights. Managed via secpol.msc normally.
    //
    public class LocalSecurityAuthorityController {
        private const int Access = (int)(
            LSA_AccessPolicy.POLICY_AUDIT_LOG_ADMIN |
            LSA_AccessPolicy.POLICY_CREATE_ACCOUNT |
            LSA_AccessPolicy.POLICY_CREATE_PRIVILEGE |
            LSA_AccessPolicy.POLICY_CREATE_SECRET |
            LSA_AccessPolicy.POLICY_GET_PRIVATE_INFORMATION |
            LSA_AccessPolicy.POLICY_LOOKUP_NAMES |
            LSA_AccessPolicy.POLICY_NOTIFICATION |
            LSA_AccessPolicy.POLICY_SERVER_ADMIN |
            LSA_AccessPolicy.POLICY_SET_AUDIT_REQUIREMENTS |
            LSA_AccessPolicy.POLICY_SET_DEFAULT_QUOTA_LIMITS |
            LSA_AccessPolicy.POLICY_TRUST_ADMIN |
            LSA_AccessPolicy.POLICY_VIEW_AUDIT_INFORMATION |
            LSA_AccessPolicy.POLICY_VIEW_LOCAL_INFORMATION
            );

        [DllImport("advapi32.dll", PreserveSig = true)]
        private static extern UInt32 LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, Int32 DesiredAccess, out IntPtr PolicyHandle);

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        private static extern int LsaAddAccountRights(IntPtr PolicyHandle, IntPtr AccountSid, LSA_UNICODE_STRING[] UserRights, int CountOfRights);

        [DllImport("advapi32")]
        public static extern void FreeSid(IntPtr pSid);

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true, PreserveSig = true)]
        private static extern bool LookupAccountName(string lpSystemName, string lpAccountName, IntPtr psid, ref int cbsid, StringBuilder domainName, ref int cbdomainLength, ref int use);

        [DllImport("advapi32.dll")]
        private static extern bool IsValidSid(IntPtr pSid);

        [DllImport("advapi32.dll")]
        private static extern int LsaClose(IntPtr ObjectHandle);

        [DllImport("kernel32.dll")]
        private static extern int GetLastError();

        [DllImport("advapi32.dll")]
        private static extern int LsaNtStatusToWinError(int status);

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        private static extern int LsaEnumerateAccountRights(IntPtr PolicyHandle, IntPtr AccountSid, out IntPtr UserRightsPtr, out int CountOfRights);

        [StructLayout(LayoutKind.Sequential)]
        private struct LSA_UNICODE_STRING {
            public UInt16 Length;
            public UInt16 MaximumLength;
            public IntPtr Buffer;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct LSA_OBJECT_ATTRIBUTES {
            public int Length;
            public IntPtr RootDirectory;
            public LSA_UNICODE_STRING ObjectName;
            public UInt32 Attributes;
            public IntPtr SecurityDescriptor;
            public IntPtr SecurityQualityOfService;
        }

        [Flags]
        private enum LSA_AccessPolicy : long {
            POLICY_VIEW_LOCAL_INFORMATION = 0x00000001L,
            POLICY_VIEW_AUDIT_INFORMATION = 0x00000002L,
            POLICY_GET_PRIVATE_INFORMATION = 0x00000004L,
            POLICY_TRUST_ADMIN = 0x00000008L,
            POLICY_CREATE_ACCOUNT = 0x00000010L,
            POLICY_CREATE_SECRET = 0x00000020L,
            POLICY_CREATE_PRIVILEGE = 0x00000040L,
            POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080L,
            POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100L,
            POLICY_AUDIT_LOG_ADMIN = 0x00000200L,
            POLICY_SERVER_ADMIN = 0x00000400L,
            POLICY_LOOKUP_NAMES = 0x00000800L,
            POLICY_NOTIFICATION = 0x00001000L
        }

        // Returns the Local Security Authority rights granted to the account
        public IList<string> GetRights(string accountName) {
            IList<string> rights = new List<string>();
            string errorMessage = string.Empty;

            long winErrorCode = 0;
            IntPtr sid = IntPtr.Zero;
            int sidSize = 0;
            StringBuilder domainName = new StringBuilder();
            int nameSize = 0;
            int accountType = 0;

            LookupAccountName(string.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType);

            domainName = new StringBuilder(nameSize);
            sid = Marshal.AllocHGlobal(sidSize);

            if (!LookupAccountName(string.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType)) {
                winErrorCode = GetLastError();
                errorMessage = ("LookupAccountName failed: " + winErrorCode);
            } else {
                LSA_UNICODE_STRING systemName = new LSA_UNICODE_STRING();

                IntPtr policyHandle = IntPtr.Zero;
                IntPtr userRightsPtr = IntPtr.Zero;
                int countOfRights = 0;

                LSA_OBJECT_ATTRIBUTES objectAttributes = CreateLSAObject();

                uint policyStatus = LsaOpenPolicy(ref systemName, ref objectAttributes, Access, out policyHandle);
                winErrorCode = LsaNtStatusToWinError(Convert.ToInt32(policyStatus));

                if (winErrorCode != 0) {
                    errorMessage = string.Format("OpenPolicy failed: {0}.", winErrorCode);
                } else {
                    int result = LsaEnumerateAccountRights(policyHandle, sid, out userRightsPtr, out countOfRights);
                    winErrorCode = LsaNtStatusToWinError(result);
                    if (winErrorCode != 0) {
                        errorMessage = string.Format("LsaAddAccountRights failed: {0}", winErrorCode);
                    }

                    Int32 ptr = userRightsPtr.ToInt32();
                    LSA_UNICODE_STRING userRight;

                    for (int i = 0; i < countOfRights; i++) {
                        userRight = (LSA_UNICODE_STRING)Marshal.PtrToStructure(new IntPtr(ptr), typeof(LSA_UNICODE_STRING));
                        string userRightStr = Marshal.PtrToStringAuto(userRight.Buffer);
                        rights.Add(userRightStr);
                        ptr += Marshal.SizeOf(userRight);
                    }
                    LsaClose(policyHandle);
                }
                FreeSid(sid);
            }
            if (winErrorCode > 0) {
                throw new ApplicationException(string.Format("Error occured in LSA, error code {0}, detail: {1}", winErrorCode, errorMessage));
            }
            return rights;
        }

        // Adds a privilege to an account
        public void SetRight(string accountName, string privilegeName) {
            long winErrorCode = 0;
            string errorMessage = string.Empty;

            IntPtr sid = IntPtr.Zero;
            int sidSize = 0;
            StringBuilder domainName = new StringBuilder();
            int nameSize = 0;
            int accountType = 0;

            LookupAccountName(String.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType);

            domainName = new StringBuilder(nameSize);
            sid = Marshal.AllocHGlobal(sidSize);

            if (!LookupAccountName(string.Empty, accountName, sid, ref sidSize, domainName, ref nameSize, ref accountType)) {
                winErrorCode = GetLastError();
                errorMessage = string.Format("LookupAccountName failed: {0}", winErrorCode);
            } else {
                LSA_UNICODE_STRING systemName = new LSA_UNICODE_STRING();
                IntPtr policyHandle = IntPtr.Zero;
                LSA_OBJECT_ATTRIBUTES objectAttributes = CreateLSAObject();

                uint resultPolicy = LsaOpenPolicy(ref systemName, ref objectAttributes, Access, out policyHandle);
                winErrorCode = LsaNtStatusToWinError(Convert.ToInt32(resultPolicy));

                if (winErrorCode != 0) {
                    errorMessage = string.Format("OpenPolicy failed: {0} ", winErrorCode);
                } else {
                    LSA_UNICODE_STRING[] userRights = new LSA_UNICODE_STRING[1];
                    userRights[0] = new LSA_UNICODE_STRING();
                    userRights[0].Buffer = Marshal.StringToHGlobalUni(privilegeName);
                    userRights[0].Length = (UInt16)(privilegeName.Length * UnicodeEncoding.CharSize);
                    userRights[0].MaximumLength = (UInt16)((privilegeName.Length + 1) * UnicodeEncoding.CharSize);

                    int res = LsaAddAccountRights(policyHandle, sid, userRights, 1);
                    winErrorCode = LsaNtStatusToWinError(Convert.ToInt32(res));
                    if (winErrorCode != 0) {
                        errorMessage = string.Format("LsaAddAccountRights failed: {0}", winErrorCode);
                    }

                    LsaClose(policyHandle);
                }
                FreeSid(sid);
            }

            if (winErrorCode > 0) {
                throw new ApplicationException(string.Format("Failed to add right {0} to {1}. Error detail:{2}", accountName, privilegeName, errorMessage));
            }
        }

        public void SetRights(string accountName, IList<string> rights) {
            rights.ToList().ForEach(right => SetRight(accountName, right));
        }

        private static LSA_OBJECT_ATTRIBUTES CreateLSAObject() {
            LSA_OBJECT_ATTRIBUTES newInstance = new LSA_OBJECT_ATTRIBUTES();

            newInstance.Length = 0;
            newInstance.RootDirectory = IntPtr.Zero;
            newInstance.Attributes = 0;
            newInstance.SecurityDescriptor = IntPtr.Zero;
            newInstance.SecurityQualityOfService = IntPtr.Zero;

            return newInstance;
        }
    }

    // Local security rights managed by the Local Security Authority
    public class LocalSecurityAuthorityRights {
        // Log on as a service right
        public const string LogonAsService = "SeServiceLogonRight";
        // Log on as a batch job right
        public const string LogonAsBatchJob = "SeBatchLogonRight";
        // Interactive log on right
        public const string InteractiveLogon = "SeInteractiveLogonRight";
        // Network log on right
        public const string NetworkLogon = "SeNetworkLogonRight";
        // Generate security audit logs right
        public const string GenerateSecurityAudits = "SeAuditPrivilege";
    }
}

PowerShell Part 2 – Installing a new service

Following on from the brief introduction to PowerShell, let’s walk through the installation script…

The script installs a simple Magic Eight Ball service that will return a pseudo-random answer to any question it’s given. The service is written as a WCF service in C#, the files to deploy are available from http://public.me.com/stefsewell/ , have a look in TechEd2010/DEV306-WindowsServerAppFabric/InstallationSource. The folder contains a web.config to set up the service activation and a bin folder with the service implementation. The PowerShell scripts are also available from the file share, look in Powershell folder in DEV306…

Pre-requisite Checking

The script begins by checking a couple of pre-requisites. If any of these checks fail then we do not attempt to install the service, instead the installing admin is told of the failed checks. There are a number of different checks we can make, in this script we check the OS version, that dependent services are installed and that the correct version of the .NET framework is available.

First we need a variable to hold whether or not we have a failure:

$failedPrereqs = $false

Next we move on to our first check: that the correct version of Windows being used:

$OSVersion = Get-WmiObject Win32_OperatingSystem
if(-not $OSVersion.Version.StartsWith('6.1')) {
    Write-Host "The operating system version is not supported, Windows 7 or Windows Server 2008 required."
    $failedPrereqs = $true
    # See http://msdn.microsoft.com/en-us/library/aa394239(v=VS.85).aspx for other properties of Win32_OperatingSystem
    # See http://msdn.microsoft.com/en-us/library/aa394084(VS.85).aspx for additional WMI classes
}

The script fetches the Win32_OperatingSystem WMI object for interrogation using Get-WmiObject. This object contains a good deal of useful information, links are provided above to let you drill down into other properties. The script checks the Version to ensure that we are working with either Windows 7 or Windows Server 2008, in which case the version starts with “6.1”.

Next we look for a couple of installed services:

# IIS is installed
$IISService = Get-Service -Name 'W3SVC' -ErrorAction SilentlyContinue
if(-not $IISService) {
    Write-Host "IIS is not installed on" $env:computername
    $FailedPrereqs = $true
}

# AppFabric is installed
$AppFabricMonitoringService = Get-Service -Name 'AppFabricEventCollectionService' -ErrorAction SilentlyContinue
if(-not $AppFabricMonitoringService) {
    Write-Host "AppFabric Monitoring Service is not installed on" $env:computername
    $FailedPrereqs = $true
}

$AppFabricMonitoringService = Get-Service -Name 'AppFabricWorkflowManagementService' -ErrorAction SilentlyContinue
if(-not $AppFabricMonitoringService) {
    Write-Host "AppFabric Workflow Management Service is not installed on" $env:computername
    $FailedPrereqs = $true
}

A basic pattern is repeated here using the Get-Service command to determine if a particular Windows Service is installed on the machine.

With the service requirements checked, we look to see if we have the correct version of the .NET framework installed. In our case we want the RTM of version 4 and go to the registry to validate this.

$frameworkVersion = get-itemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -ErrorAction SilentlyContinue
if(-not($frameworkVersion) -or (-not($frameworkVersion.Version -eq '4.0.30319'))){
    Write-Host "The RTM version of the full .NET 4 framework is not installed."
    $FailedPrereqs = $true
}

The registry provider is used, HKLM: [HKEY_LOCAL_MACHINE], to look up a path in the registry that should contain the version. If the key is not found or the value is incorrect we fail the test.

Those are all the checks made in the original script from the DEV306 session, however there is great feature in Windows Server 2008 R2 that allows very simple querying of the installed Windows features. I found this by accident:

>Get-Module -ListAvailable

This command lists all of the available modules on a system, the ServerManager module looked interesting:

>Get-Command -Module ServerManager

CommandType Name Definition
----------- ---- ----------
Cmdlet Add-WindowsFeature Add-WindowsFeature [-Name] [-IncludeAllSubFeature] [-LogPath ] [-...
Cmdlet Get-WindowsFeature Get-WindowsFeature [[-Name] ] [-LogPath ] [-Verbose] [-Debug] [-Err...
Cmdlet Remove-WindowsFeature Remove-WindowsFeature [-Name] [-LogPath ] [-Concurrent] [-Restart...

A simple add/remove/get interface which allows you to easily determine which Windows roles and features are installed – then add or remove as required. This is ideal for pre-requisite checking as we can now explicitly check to see if the WinRM IIS Extensions are installed for example:

import-module ServerManager

if(-not (Get-WindowsFeature ‘WinRM-IIS-Ext’).Installed) {
    Write-Host "The WinRM IIS Extension is not installed"
}

Simply calling Get-WindowsFeature lists all features and marks-up those that are installed with [X]:

PS>C:\Windows\system32> Get-WindowsFeature

Display Name Name
------------ ----
[ ] Active Directory Certificate Services AD-Certificate
[ ] Certification Authority ADCS-Cert-Authority
[ ] Certification Authority Web Enrollment ADCS-Web-Enrollment
[ ] Certificate Enrollment Web Service ADCS-Enroll-Web-Svc
[ ] Certificate Enrollment Policy Web Service ADCS-Enroll-Web-Pol
[ ] Active Directory Domain Services AD-Domain-Services
[ ] Active Directory Domain Controller ADDS-Domain-Controller
[ ] Identity Management for UNIX ADDS-Identity-Mgmt
[ ] Server for Network Information Services ADDS-NIS
[ ] Password Synchronization ADDS-Password-Sync
[ ] Administration Tools ADDS-IDMU-Tools
[ ] Active Directory Federation Services AD-Federation-Services
[ ] Federation Service ADFS-Federation
[ ] Federation Service Proxy ADFS-Proxy
[ ] AD FS Web Agents ADFS-Web-Agents
[ ] Claims-aware Agent ADFS-Claims
[ ] Windows Token-based Agent ADFS-Windows-Token
[ ] Active Directory Lightweight Directory Services ADLDS
[ ] Active Directory Rights Management Services ADRMS
[ ] Active Directory Rights Management Server ADRMS-Server
[ ] Identity Federation Support ADRMS-Identity
[X] Application Server Application-Server
[X] .NET Framework 3.5.1 AS-NET-Framework
[X] AppFabric AS-AppServer-Ext
[X] Web Server (IIS) Support AS-Web-Support
[X] COM+ Network Access AS-Ent-Services
[X] TCP Port Sharing AS-TCP-Port-Sharing
[X] Windows Process Activation Service Support AS-WAS-Support
[X] HTTP Activation AS-HTTP-Activation
[X] Message Queuing Activation AS-MSMQ-Activation
[X] TCP Activation AS-TCP-Activation
...

The right hand column contains the name of the feature to use via the command.

I ended up writing a simple function to check for a list of features:

<#
.SYNOPSIS
Checks to see if a given set of Windows features are installed.    

.DESCRIPTION
Checks to see if a given set of Windows features are installed.

.PARAMETER featureSetArray
An array of strings containing the Windows features to check for.

.PARAMETER featuresName
A description of the feature set being tested for.

.EXAMPLE
Check that a couple of web server features are installed.

Check-FeatureSet -featureSetArray @('Web-Server','Web-WebServer','Web-Common-Http') -featuresName 'Required Web Features'

#>
function Check-FeatureSet{
    param(
        [Parameter(Mandatory=$true)]
        [array] $featureSetArray,
        [Parameter(Mandatory=$true)]
        [string]$featuresName
    )
    Write-Host "Checking $featuresName for missing features..."

    foreach($feature in $featureSetArray){
        if(-not (Get-WindowsFeature $feature).Installed){
            Write-Host "The feature $feature is not installed"
        }
    }
}

The function introduces a number of PowerShell features such as comment documentation, functions, parameters and parameter attributes. I don’t intend to dwell on any as I hope the code is quite readable.

Then to use this:

# array of strings containing .NET related features
$dotNetFeatureSet = @('NET-Framework','NET-Framework-Core','NET-Win-CFAC','NET-HTTP-Activation','NET-Non-HTTP-Activ')

# array of string containing MSMQ related features
$messageQueueFeatureSet = @('MSMQ','MSMQ-Services','MSMQ-Server')

Check-FeatureSet $dotNetFeatureSet '.NET'
Check-FeatureSet $messageQueueFeatureSet 'Message Queuing'

To complete the pre-requisite check, after making each individual test the failure variable is evaluated. If true then the script ends with a suitable message, otherwise we go ahead with the install.

Installing the Service

The first step in the installation is to copy the required files from a known location. This is a pull model – the target server pulls the files across the network, rather than having the files pushed on to the server via an administration share or such like [e.g. \\myMachine\c$\Services\].

$sourcePath = '\\SomeMachine\MagicEightBallInstaller\'
$installPath = 'C:\Services\MagicEightBall'

if(-not (Test-Path $sourcePath)) {
Write-Host 'Cannot find the source path ' $sourcePath
Throw (New-Object System.IO.FileNotFoundException)
}

if(-not (Test-Path $installPath)) {
New-Item -type directory -path $installPath
Write-Host 'Created service directory at ' $installPath
}

Copy-Item -Path (Join-Path $sourcePath "*") -Destination $installPath -Recurse

Write-Host 'Copied the required service files to ' $installPath

The file structure is copied from a network share onto the machine the script is running on. The Test-Path command determines whether a path exists an allows appropriate action to be taken. To perform a recursive copy the Copy-Item command is called, using the Join-Path command to establish the source path. These path commands can be used with any provider, not just the file system.

With the files and directories in place, we now need to host the service in IIS. To do this we need to use the PowerShell module for IIS:

import-module WebAdministration # require admin-level privileges

Next…

$found = Get-ChildItem IIS:\AppPools | Where-Object {$_.Name -eq "NewAppPool"}
if(-not $found){
    New-WebAppPool 'NewAppPool'
}

We want to isolate our service into its own pool so we check to see if NewAppPool exists and if not we create it. We are using the IIS: provider to treat the web server as if it was a file system, again we just use standard commands to query the path.

Set-ItemProperty IIS:\AppPools\NewAppPool -Name ProcessModel -Value @{IdentityType=3;Username="MyServer\Service.EightBall";Password="p@ssw0rd"} # 3 = Custom

Set-ItemProperty IIS:\AppPools\NewAppPool -Name ManagedRuntimeVersion -Value v4.0

Write-Host 'Created application pool NewAppPool'

Having created the application pool we set some properties. In particular we ensure that .NET v4 is used and that a custom identity is used. The @{} syntax allows us to construct new object instances – in this case a new process model object.

New-WebApplication -Site 'Default Web Site' -Name 'MagicEightBall' -PhysicalPath $installPath -ApplicationPool 'NewAppPool' -Force

With the application pool in place and configured, we next set-up the web application itself. The New-WebApplication command is all we need, giving it the site, application name, physical file system path and application pool.

Set-ItemProperty 'IIS:/Sites/Default Web Site/MagicEightBall' -Name EnabledProtocols 'http,net.tcp' # do not include spaces in the list!

Write-Host 'Created web application MagicEightBall'

To enable both HTTP and net.tcp endpoints, we simply update the EnabledProtocols property of the web application. Thanks to default endpoints in WCF4, this is all we need to do get both protocols supported. Note: do not put spaces into the list of protocols.

Configuring AppFabric Monitoring

We now have enough script to create the service host, but we want to add AppFabric monitoring. Windows Server AppFabric has a rich PowerShell API, to access it we need to import the module:

import-module ApplicationServer

Next we need to create our monitoring database:

[Reflection.Assembly]::LoadWithPartialName("System.Data")

$monitoringDatabase = 'MagicEightBallMonitoring'
$monitoringConnection = New-Object System.Data.SqlClient.SqlConnectionStringBuilder -argumentList "Server=localhost;Database=$monitoringDatabase;Integrated Security=true"
$monitoringConnection.Pooling = $true

We need a couple of variables: a database name and a connection string. We use the SqlConnectionStringBuilder out of the System.Data assembly to get our connection string. This demonstrates the deep integration between PowerShell and .NET.

Add-WebConfiguration -Filter connectionStrings -PSPath "MACHINE/WEBROOT/APPHOST/Default Web Site/MagicEightBall" -Value @{name="MagicEightBallMonitoringConnection"; connectionString=$monitoringConnection.ToString()}

We add the connection string to our web application configuration.

Initialize-ASMonitoringSqlDatabase -Admins 'Domain\AS_Admins' -Readers 'DOMAIN\AS_Observers' -Writers 'DOMAIN\AS_MonitoringWriters' -ConnectionString $monitoringConnection.ToString() -Force

And then we create the actual database, passing in the security groups. While local machine groups can be used, in this case I’m mocking a domain group which is more appropriate for load balanced scenarios.

Set-ASAppMonitoring -SiteName 'Default Web Site' -VirtualPath 'MagicEightBall' -MonitoringLevel 'HealthMonitoring' -ConnectionStringName 'MagicEightBallMonitoringConnection'

The last step is to enable monitoring for the web application, above we are setting a ‘health monitoring’ level which is enough to populate the AppFabric dashboard inside the IIS manager.

Set-ASAppServiceMetadata -SiteName 'Default Web Site' -VirtualPath 'MagicEightBall' -HttpGetEnabled $True

Last of all we ensure that meta data publishing is available for our service. This allows us to test the service using the WCFTestClient application.