Monday, September 29, 2014

Continuous Deployment - Remote execution of PowerShell scripts from your build process


Including Windows PowerShell script as part of your build and deployment process, brings you the flexibility of easily and effectively customize your packaging and deployment process. With the proper combination of environment configuration files (XML) and PowerShell scripts you can achieve the impossible. This post will show you how to run Windows PowerShell scripts remotely from a TFS build process.
Using CredSSP for second-hop remoting

One common issue with PowerShell remoting is the “double hop” problem. When the scripts are executed remotely on a Server A and then it tries to connect from Server A to Server B, the second connection fails to send the credentials to that server. As a result the second server fails to authenticate the request and rejects the connection. To get around this issue you need to use the CredSSP authentication mechanism in PowerShell.
Credential Security Service Provider (CredSSP) is a new security service provider that is available through the Security Support Provider Interface (SSPI) in Windows. CredSSP enables an application to delegate the user’s credentials from the client (by using the client-side SSP) to the target server (through the server-side SSP).
To setup the machines, for CredSSP you can follow the below given steps:

On the local computer that needs to connect to a remote server, execute the command Enable-WSManCredSSP -Role client -DelegateComputer *. This will  make the local computer role as client since so that it can connect to remote computer which acts as server.
On the remote server run the command Enable-WSManCredSSP -Role server.

Remote execution of the PowerShell script from TFS build

For executing PowerShell scripts from the C# code you need to make use of the API’s in the System.Management.Automation.dll assembly. You can use the PowerShell instance directly but a better way is to create a Runspace instance. Every PowerShell instance works in a Runspace. You can have multiple Runspaces to connect to different remote hosts at the same time.
To get the remote execution working properly you need to first construct an instance of WSManConnectionInfo and pass that on to the RunspaceFactory.CreateRunspace(..) method. You can use the following code snippet to construct the connection object.

private PSCredential BuildCredentials(string username, string domain, string password)
{
    PSCredential credential = null;
    if (String.IsNullOrWhiteSpace(username))
    {
        return credential;
    }
    if (!String.IsNullOrWhiteSpace(domain))
    {
        username = domain + "\\" + username;
    }
    var securePassword = new SecureString();
    if (!String.IsNullOrEmpty(password))
    {
        foreach (char c in password)
        {
            securePassword.AppendChar(c);
        }
    }
    securePassword.MakeReadOnly();
    credential = new PSCredential(username, securePassword);
    return credential;
}

private WSManConnectionInfo ConstructConnectionInfo()
{
    //The connection info values are created after looking into the ouput of the command in the target machine

    /*
        * dir WSMan:\localhost\Listener\Listener_*\*
        * /
    //Output           
    /* 
        * Name                      Value                                                             Type

        * ----                      -----                                                             ----

        * Address                   *                                                                 System.String

        * Transport                 HTTP                                                              System.String

        * Port                      5985                                                              System.String

        * URLPrefix                 wsman                                                             System.String

        * 

        */
   const string SHELL_URI = http://schemas.microsoft.com/powershell/Microsoft.PowerShell;
    var credentials = BuildCredentials("username""domain""password");
    var connectionInfo = new WSManConnectionInfo(false"remoteserver", 5985, "/wsman", SHELL_URI, credentials);
    connectionInfo.AuthenticationMechanism = AuthenticationMechanism.Credssp;
    return connectionInfo;
}
Next you can use this connection object to pass it to the RunspaceFactory.CreateRunspace method and invoke the PowerShell script as given below.
var scriptOutput = new ScriptOutput();
var tfsBuildHost = new TFSBuildHost(scriptOutput);
var connection = ConstructConnectionInfo();
 
using (var runspace = RunspaceFactory.CreateRunspace(tfsBuildHost, connection))
{
    runspace.Open();
    InvokePSScript(runspace, scriptOutput);
    runspace.Close();
}
 
 
private void InvokePSScript(Runspace runspace, ScriptOutput scriptOutput)
{
    using (var ps = PowerShell.Create())
    {
        ps.Runspace = runspace;
        var commandToExecute = ConstructScriptExecutionCommand("path to script file""parameter1""parameter2");
        ps.AddScript(commandToExecute);
        try
        {
            var results = ps.Invoke();
            foreach (var result in results)
            {
                if (result.BaseObject != null)
                {
                    scriptOutput.WriteResults(result.BaseObject.ToString());
                }
            }
        }
        catch(Exception)
        {
            if (ps.InvocationStateInfo.State != PSInvocationState.Failed)
            {
                return;
            }
            scriptOutput.WriteError(ps.InvocationStateInfo.Reason.Message); 
        }
    }
}

You can also notice the TFSBuildHost object in the code. This class provides communications between the Windows PowerShell engine and the user. The implementation details are as given below.
internal class TFSBuildHost : PSHost{
    private ScriptOutput _output;
    private CultureInfo originalCultureInfo =
        System.Threading.Thread.CurrentThread.CurrentCulture;
    private CultureInfo originalUICultureInfo =
        System.Threading.Thread.CurrentThread.CurrentUICulture;
    private Guid _hostId = Guid.NewGuid();
    private TFSBuildUserInterface _tfsBuildInterface;
 
    public TFSBuildHost(ScriptOutput output)
    {
        this._output = output;
        _tfsBuildInterface = new TFSBuildUserInterface(_output);
    }
    public override System.Globalization.CultureInfo CurrentCulture
    {
        get { return this.originalCultureInfo; }
    }
    public override System.Globalization.CultureInfo CurrentUICulture
    {
        get { return this.originalUICultureInfo; }
    }
    public override Guid InstanceId
    {
        get { return this._hostId; }
    }
    public override string Name
    {
        get { return "TFSBuildPowerShellImplementation"; }
    }
    public override Version Version
    {
        get { return new Version(1, 0, 0, 0); }
    }
    public override PSHostUserInterface UI
    {
        get { return this._tfsBuildInterface; }
    }
    public override void NotifyBeginApplication()
    {
        _output.Started = true;
    }
    public override void NotifyEndApplication()
    {
        _output.Started = false;
        _output.Stopped = true;
    }
    public override void SetShouldExit(int exitCode)
    {
        this._output.ShouldExit = true;
        this._output.ExitCode = exitCode;
    }
    public override void EnterNestedPrompt()
    {
        throw new NotImplementedException(
                "The method or operation is not implemented.");
    }
    public override void ExitNestedPrompt()
    {
        throw new NotImplementedException(
                "The method or operation is not implemented.");
    }
} 

internal class TFSBuildUserInterface : PSHostUserInterface
{
    private ScriptOutput _output; 
    private TFSBuildRawUserInterface _tfsBuildRawUi = new TFSBuildRawUserInterface();
 
    public TFSBuildUserInterface(ScriptOutput output)
    {
        _output = output;
    }
    public override PSHostRawUserInterface RawUI
    {
        get { return this._tfsBuildRawUi; }
    }
    public override string ReadLine()
    {
        return Environment.NewLine;
    }
    public override void Write(string value)
    {
        _output.Write(value);
    }
    public override void Write(
                                ConsoleColor foregroundColor,
                                ConsoleColor backgroundColor,
                                string value)
    {
        //Ignore the colors for TFS build process.
        _output.Write(value);
    }
    public override void WriteDebugLine(string message)
    {
        Debug.WriteLine(String.Format("DEBUG: {0}", message));
    }
    public override void WriteErrorLine(string value)
    {
        _output.WriteError(value);
    }
    public override void WriteLine()
    {
        _output.WriteLine();
    }
    public override void WriteLine(string value)
    {
        _output.WriteLine(value);
    }
    public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
    {
        //Ignore the colors for TFS build process.
        _output.WriteLine(value);
    }
}

internal class TFSBuildRawUserInterface : PSHostRawUserInterface
{
    private ConsoleColor _backColor = ConsoleColor.Black;
    private ConsoleColor _foreColor = ConsoleColor.White;
    private Size _bufferSize = new Size(300, 900);
    private Size _windowSize = new Size(100, 400);
    private Coordinates _cursorPosition = new Coordinates { X = 0, Y = 0 };
    private Coordinates _windowPosition = new Coordinates { X = 50, Y = 10 };
    private int _cursorSize = 1;
    private string _title = "TFS build process";
     
    public override ConsoleColor BackgroundColor
    {
        get { return _backColor; }
        set { _backColor = value; }
    }
    public override Size BufferSize
    {
        get { return _bufferSize; }
        set { _bufferSize = value; }
    }
    public override Coordinates CursorPosition
    {
        get { return _cursorPosition; }
        set { _cursorPosition = value; }
    }
    public override int CursorSize
    {
        get { return _cursorSize; }
        set { _cursorSize = value; }
    }
    public override ConsoleColor ForegroundColor
    {
        get { return _foreColor; }
        set { _foreColor = value; }
    }
    public override bool KeyAvailable
    {
        get { return false; }
    }
    public override Size MaxPhysicalWindowSize
    {
        get { return new Size(500, 2000); }
    }
    public override Size MaxWindowSize
    {
        get { return new Size(500, 2000); }
    }
    public override Coordinates WindowPosition
    {
        get { return _windowPosition; }
        set { _windowPosition = value; }
    }
    public override Size WindowSize
    {
        get { return _windowSize; }
        set { _windowSize = value; }
    }
    public override string WindowTitle
    {
        get { return _title; }
        set { _title = value; }
    }
}