Using PowerShell script make any application highly available

Author:
Amitabh Tamhane
Senior Program Manager
Windows Server Microsoft

OS releases: Applicable to Windows Server 2008 R2 or later

Now you can use PowerShell scripts to make any application highly available with Failover Clusters!!!

The Generic Script is a built-in resource type included in Windows Server Failover Clusters. Its advantage is flexibility: you can make applications highly available by writing a simple script. For instance, you can make any PowerShell script highly available! Interested?

We created GenScript in ancient times and it supports only Visual Basic scripts – including Windows Server 2016. This means you can’t directly configure PowerShell as GenScript resource. However, in this blog post, I’ll walk you through a sample Visual Basic script – and associated PS scripts – to build a custom GenScript resource that works well with PowerShell.

Pre-requisites: This blog assumes you have the basic understanding of Windows Server Failover Cluster & built-in resource types.

Disclaimer: Microsoft does not intend to officially support any source code/sample scripts provided as part of this blog. This blog is written only for a quick walk-through on how to run PowerShell scripts using GenScript resource. To make your application highly available, you are expected to modify all the scripts (Visual Basic/PowerShell) as per the needs of your application.

It so happens that Visual Basic Shell supports calling PowerShell script, then passing parameters and reading output. Here’s a Visual Basic Shell sample that uses some custom Private Properties:

 

'<your application name> Resource Type

Function Open( )
    Resource.LogInformation "Enter Open()"

    If Resource.PropertyExists("PSScriptsPath") = False Then
        Resource.AddProperty("PSScriptsPath")
    End If

    If Resource.PropertyExists("Name") = False Then
        Resource.AddProperty("Name")
    End If

    If Resource.PropertyExists("Data1") = False Then
        Resource.AddProperty("Data1")
    End If

    If Resource.PropertyExists("Data2") = False Then
        Resource.AddProperty("Data2")
    End If

    If Resource.PropertyExists("DataStorePath") = False Then
        Resource.AddProperty("DataStorePath")
    End If

    '...Result...
    Open = 0

    Resource.LogInformation "Exit Open()"
End Function


Function Online( )
    Resource.LogInformation "Enter Online()"

    '...Check for required private properties...

    If Resource.PropertyExists("PSScriptsPath") = False Then
        Resource.LogInformation "PSScriptsPath is a required private property."
        Online = 1
        Exit Function
    End If
    '...Resource.LogInformation "PSScriptsPath is " & Resource.PSScriptsPath

    If Resource.PropertyExists("Name") = False Then
        Resource.LogInformation "Name is a required private property."
        Online = 1
        Exit Function
    End If
    Resource.LogInformation "Name is " & Resource.Name

    If Resource.PropertyExists("Data1") = False Then
        Resource.LogInformation "Data1 is a required private property."
        Online = 1
        Exit Function
    End If
    '...Resource.LogInformation "Data1 is " & Resource.Data1

    If Resource.PropertyExists("Data2") = False Then
        Resource.LogInformation "Data2 is a required private property."
        Online = 1
        Exit Function
    End If
    '...Resource.LogInformation "Data2 is " & Resource.Data2

    If Resource.PropertyExists("DataStorePath") = False Then
        Resource.LogInformation "DataStorePath is a required private property."
        Online = 1
        Exit Function
    End If
    '...Resource.LogInformation "DataStorePath is " & Resource.DataStorePath

    PScmd = "powershell.exe -file " & Resource.PSScriptsPath & "\PS_Online.ps1 " & Resource.PSScriptsPath & " " & Resource.Name & " " & Resource.Data1 & " " & Resource.Data2 & " " & Resource.DataStorePath

    Dim WshShell
    Set WshShell = CreateObject("WScript.Shell")

    Resource.LogInformation "Calling Online PS script= " & PSCmd
    rv = WshShell.Run(PScmd, , True)
    Resource.LogInformation "PS return value is: " & rv

    '...Translate result from PowerShell ...
    '...1 (True in PS) == 0 (True in VB)
    '...0 (False in PS) == 1 (False in VB)
    If rv = 1 Then
        Resource.LogInformation "Online Success"
        Online = 0
    Else
        Resource.LogInformation "Online Error"
        Online = 1
    End If

    Resource.LogInformation "Exit Online()"
End Function

Function Offline( )
    Resource.LogInformation "Enter Offline()"

    '...Check for required private properties...

    If Resource.PropertyExists("PSScriptsPath") = False Then
        Resource.LogInformation "PSScriptsPath is a required private property."
        Offline = 1
        Exit Function
    End If
    '...Resource.LogInformation "PSScriptsPath is " & Resource.PSScriptsPath

    If Resource.PropertyExists("Name") = False Then
        Resource.LogInformation "Name is a required private property."
        Offline = 1
        Exit Function
    End If
    Resource.LogInformation "Name is " & Resource.Name

    If Resource.PropertyExists("Data1") = False Then
        Resource.LogInformation "Data1 is a required private property."
        Offline = 1
        Exit Function
    End If
    '...Resource.LogInformation "Data1 is " & Resource.Data1

    If Resource.PropertyExists("Data2") = False Then
        Resource.LogInformation "Data2 is a required private property."
        Offline = 1
        Exit Function
    End If
    '...Resource.LogInformation "Data2 is " & Resource.Data2

    If Resource.PropertyExists("DataStorePath") = False Then
        Resource.LogInformation "DataStorePath is a required private property."
        Offline = 1
        Exit Function
    End If
    '...Resource.LogInformation "DataStorePath is " & Resource.DataStorePath

    PScmd = "powershell.exe -file " & Resource.PSScriptsPath & "\PS_Offline.ps1 " & Resource.PSScriptsPath & " " & Resource.Name & " " & Resource.Data1 & " " & Resource.Data2 & " " & Resource.DataStorePath

    Dim WshShell
    Set WshShell = CreateObject("WScript.Shell")

    Resource.LogInformation "Calling Offline PS script= " & PSCmd
    rv = WshShell.Run(PScmd, , True)
    Resource.LogInformation "PS return value is: " & rv

    '...Translate result from PowerShell ...
    '...1 (True in PS) == 0 (True in VB)
    '...0 (False in PS) == 1 (False in VB)
    If rv = 1 Then
        Resource.LogInformation "Offline Success"
        Offline = 0
    Else
        Resource.LogInformation "Offline Error"
        Offline = 1
    End If

    Resource.LogInformation "Exit Offline()"
End Function

Function LooksAlive( )
    '...Result...
    LooksAlive = 0
End Function

Function IsAlive( )
    Resource.LogInformation "Entering IsAlive" 

    '...Check for required private properties...

    If Resource.PropertyExists("PSScriptsPath") = False Then
        Resource.LogInformation "PSScriptsPath is a required private property."
        IsAlive = 1
        Exit Function
    End If
    '...Resource.LogInformation "PSScriptsPath is " & Resource.PSScriptsPath

    If Resource.PropertyExists("Name") = False Then
        Resource.LogInformation "Name is a required private property."
        IsAlive = 1
        Exit Function
    End If
    Resource.LogInformation "Name is " & Resource.Name

    If Resource.PropertyExists("Data1") = False Then
        Resource.LogInformation "Data1 is a required private property."
        IsAlive = 1
        Exit Function
    End If
    '...Resource.LogInformation "Data1 is " & Resource.Data1

    If Resource.PropertyExists("Data2") = False Then
        Resource.LogInformation "Data2 is a required private property."
        IsAlive = 1
        Exit Function
    End If
    '...Resource.LogInformation "Data2 is " & Resource.Data2

    If Resource.PropertyExists("DataStorePath") = False Then
        Resource.LogInformation "DataStorePath is a required private property."
        IsAlive = 1
        Exit Function
    End If
    '...Resource.LogInformation "DataStorePath is " & Resource.DataStorePath

    PScmd = "powershell.exe -file " & Resource.PSScriptsPath & "\PS_IsAlive.ps1 " & Resource.PSScriptsPath & " " & Resource.Name & " " & Resource.Data1 & " " & Resource.Data2 & " " & Resource.DataStorePath

    Dim WshShell
    Set WshShell = CreateObject("WScript.Shell")

    Resource.LogInformation "Calling IsAlive PS script= " & PSCmd
    rv = WshShell.Run(PScmd, , True)
    Resource.LogInformation "PS return value is: " & rv

    '...Translate result from PowerShell ...
    '...1 (True in PS) == 0 (True in VB)
    '...0 (False in PS) == 1 (False in VB)
    If rv = 1 Then
        Resource.LogInformation "IsAlive Success"
        IsAlive = 0
    Else
        Resource.LogInformation "IsAlive Error"
        IsAlive = 1
    End If

    Resource.LogInformation "Exit IsAlive()"
End Function

Function Terminate( )
    Resource.LogInformation "Enter Terminate()"

    '...Check for required private properties...

    If Resource.PropertyExists("PSScriptsPath") = False Then
        Resource.LogInformation "PSScriptsPath is a required private property."
        Terminate = 1
        Exit Function
    End If
    '...Resource.LogInformation "PSScriptsPath is " & Resource.PSScriptsPath

    If Resource.PropertyExists("Name") = False Then
        Resource.LogInformation "Name is a required private property."
        Terminate = 1
        Exit Function
    End If
    Resource.LogInformation "Name is " & Resource.Name

    If Resource.PropertyExists("Data1") = False Then
        Resource.LogInformation "Data1 is a required private property."
        Terminate = 1
        Exit Function
    End If
    '...Resource.LogInformation "Data1 is " & Resource.Data1

    If Resource.PropertyExists("Data2") = False Then
        Resource.LogInformation "Data2 is a required private property."
        Terminate = 1
        Exit Function
    End If
    '...Resource.LogInformation "Data2 is " & Resource.Data2

    If Resource.PropertyExists("DataStorePath") = False Then
        Resource.LogInformation "DataStorePath is a required private property."
        Terminate = 1
        Exit Function
    End If
    '...Resource.LogInformation "DataStorePath is " & Resource.DataStorePath

    PScmd = "powershell.exe -file " & Resource.PSScriptsPath & "\PS_Terminate.ps1 " & Resource.PSScriptsPath & " " & Resource.Name & " " & Resource.Data1 & " " & Resource.Data2 & " " & Resource.DataStorePath

    Dim WshShell
    Set WshShell = CreateObject("WScript.Shell")

    Resource.LogInformation "Calling Terminate PS script= " & PSCmd
    rv = WshShell.Run(PScmd, , True)
    Resource.LogInformation "PS return value is: " & rv

    '...Translate result from PowerShell ...
    '...1 (True in PS) == 0 (True in VB)
    '...0 (False in PS) == 1 (False in VB)
    If rv = 1 Then
        Terminate = 0
    Else
        Terminate = 1
    End If

    Resource.LogInformation "Exit Terminate()"
End Function

Function Close( )
    '...Result...
    Close = 0
End Function

Entry Points

In the above sample VB script, the following entry points are defined:

  • Open – Ensures all necessary steps complete before starting your application
  • Online – Function to start your application
  • Offline – Function to stop your application
  • IsAlive – Function to validate your application startup and monitor health
  • Terminate – Function to forcefully cleanup application state (ex: Error during Online/Offline)
  • Close – Ensures all necessary cleanup completes after stopping your application

Each of the above entry points is defined as a function (ex: “Function Online( )”). Failover Cluster then calls these entry point functions as part of the GenScript resource type definition.

Private Properties

For resources of any type, Failover Cluster supports two types of properties:

  • Common Properties – Generic properties that can have unique value for each resource
  • Private Properties – Custom properties that are unique to that resource type. Each resource of that resource type has these private properties.

When writing a GenScript resource, you need to evaluate if you need private properties. In the above VB sample script, I have defined five sample private properties (only as an example):

  • PSScriptsPath – Path to the folder containing PS scripts
  • Name
  • Data1 – some custom data field
  • Data2 – another custom data field
  • DataStorePath – path to a common backend store (if any)

The above private properties are shown as example only & you are expected to modify the above VB script to customize it for your application.

The Visual Basic script simply connects the Failover Clusters’ RHS (Resource Hosting Service) to call PowerShell scripts. You may notice the “PScmd” parameter containing the actual PS command that will be called to perform the action (Online, Offline etc.) by calling into corresponding PS scripts.

For this sample, here are four PowerShell scripts:

  • Online.ps1 – To start your application
  • Offline.ps1 – To stop your application
  • Terminate.ps1 – To forcefully cleanup your application
  • IsAlive.ps1 – To monitor health of your application

Example of PS scripts:

Entry Point: Online

Param(
    # Sample properties…
    [Parameter(Mandatory=$  true, Position=0)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  PSScriptsPath,

    #
    [Parameter(Mandatory=$  true, Position=1)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Name,

    #
    [Parameter(Mandatory=$  true, Position=2)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Data1,

    #
    [Parameter(Mandatory=$  true, Position=3)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Data2,

    #
    [Parameter(Mandatory=$  true, Position=4)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  DataStorePath
)

$  filePath = Join-Path $  PSScriptsPath "Output_Online.log"

@"
    Starting Online...
    Name= $  Name
    Data1= $  Data1
    Data2= $  Data2
    DataStorePath= $  DataStorePath
"@ | Out-File -FilePath $  filePath

$  error.clear()

### Do your online script logic here

if ($  errorOut -eq $  true)
{
    "Error $  error" | Out-File -FilePath $  filePath -Append
    exit $  false
}

"Success" | Out-File -FilePath $  filePath -Append
exit $  true

Entry Point: Offline

Param(
    # Sample properties…
    [Parameter(Mandatory=$  true, Position=0)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  PSScriptsPath,

    #
    [Parameter(Mandatory=$  true, Position=1)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Name,

    #
    [Parameter(Mandatory=$  true, Position=2)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Data1,

    #
    [Parameter(Mandatory=$  true, Position=3)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Data2,

    #
    [Parameter(Mandatory=$  true, Position=4)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  DataStorePath
)

$  filePath = Join-Path $  PSScriptsPath "Output_Offline.log"

@"
    Starting Offline...
    Name= $  Name
    Data1= $  Data1
    Data2= $  Data2
    DataStorePath= $  DataStorePath
"@ | Out-File -FilePath $  filePath

$  error.clear()

### Do your offline script logic here

if ($  errorOut -eq $  true)
{
    "Error $  error" | Out-File -FilePath $  filePath -Append
    exit $  false
}

"Success" | Out-File -FilePath $  filePath -Append
exit $  true

Entry Point: Terminate

Param(
    # Sample properties…
    [Parameter(Mandatory=$  true, Position=0)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  PSScriptsPath,

    #
    [Parameter(Mandatory=$  true, Position=1)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Name,

    #
    [Parameter(Mandatory=$  true, Position=2)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Data1,

    #
    [Parameter(Mandatory=$  true, Position=3)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Data2,

    #
    [Parameter(Mandatory=$  true, Position=4)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  DataStorePath
)

$  filePath = Join-Path $  PSScriptsPath "Output_Terminate.log"

@"
    Starting Terminate...
    Name= $  Name
    Data1= $  Data1
    Data2= $  Data2
    DataStorePath= $  DataStorePath
"@ | Out-File -FilePath $  filePath

$  error.clear()

### Do your terminate script logic here

if ($  errorOut -eq $  true)
{
    "Error $  error" | Out-File -FilePath $  filePath -Append
    exit $  false
}

"Success" | Out-File -FilePath $  filePath -Append
exit $  true

Entry Point: IsAlive

Param(
    # Sample properties…
    [Parameter(Mandatory=$  true, Position=0)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  PSScriptsPath,

    #
    [Parameter(Mandatory=$  true, Position=1)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Name,

    #
    [Parameter(Mandatory=$  true, Position=2)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Data1,

    #
    [Parameter(Mandatory=$  true, Position=3)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  Data2,

    #
    [Parameter(Mandatory=$  true, Position=4)]
    [ValidateNotNullOrEmpty()]
    [string]
    $  DataStorePath
)

$  filePath = Join-Path $  PSScriptsPath "Output_IsAlive.log"

@"
    Starting IsAlive...
    Name= $  Name
    Data1= $  Data1
    Data2= $  Data2
    DataStorePath= $  DataStorePath
"@ | Out-File -FilePath $  filePath

$  error.clear()

### Do your isalive script logic here

if ($  errorOut -eq $  true)
{
    "Error $  error" | Out-File -FilePath $  filePath -Append
    exit $  false
}

"Success" | Out-File -FilePath $  filePath -Append
exit $  true

Parameters

The private properties are passed in as arguments to the PS script. In the sample scripts, these are all string values. You can potentially pass in different value types with more advanced VB script magic.

Note: Another way to simplify this is by writing only one PS script, such that the entry points are all functions, with only a single primary function called by the VB script. To achieve this, you can pass in additional parameters giving the context of the action expected (ex: Online, Offline etc.).

Great! Now that you have the VB Shell & Entry Point Scripts ready, let’s make the application highly available…

Copy VB + PS Scripts to Server

It is important to copy the VB script & all PS scripts to a folder on each cluster node. Ensure that the scripts is copied to the same folder on all cluster nodes. In this walk-through, the VB + PS scripts are copied to “C:\SampleScripts” folder:

Copy Scripts Using PowerShell script make any application highly available

Create Group & Resource

Using PowerShell:

Create Group Resource Using PowerShell script make any application highly available

The “ScriptFilePath” private property gets automatically added. This is the path to the VB script file. There are no other private properties which get added (see above).

You can also create Group & Resource using Failover Cluster Manager GUI:

Add Resource Using PowerShell script make any application highly available

Specify VB Script

To specify VB script, set the “ScriptFilePath” private property as:

Get Properties Not Set Using PowerShell script make any application highly available

When the VB script is specified, cluster automatically calls the Open Entry Point (in VB script). In the above VB script, additional private properties are added as part of the Open Entry Point.

Configure Private Properties

You can configure the private properties defined for the Generic Script resource as:

Configure Properties Using PowerShell script make any application highly available

In the above example, “PSScriptsPath” was specified as “C:\SampleScripts” which is the folder where all my PS scripts are stored. Additional example private properties like Name, Data1, Data2, DataStoragePath are set with custom values as well.

At this point, the Generic Script resource using PS scripts is now ready!

Starting Your Application

To start your application, you simply will need to start (aka online) the group (ex: SampleGroup) or resource (ex: SampleResUsingPS). You can start the group or resource using PS as:

Start Application Using PowerShell script make any application highly available

You can use Failover Cluster Manager GUI to start your Group/Role as well:

Start Application GUI Using PowerShell script make any application highly available

To view your application state in Failover Cluster Manager GUI:

View Online Application GUI Using PowerShell script make any application highly available

Verify PS script output:

In the sample PS script, the output log is stored in the same directory as the PS script corresponding to each entry point. You can see the output of PS scripts for Online & IsAlive Entry Points below:

Verify Scripts Using PowerShell script make any application highly available

Awesome! Now, let’s see what it takes to customize the generic scripts for your application.

The sample VB Script above is a generic shell that any application can reuse. There are few important things that you may need to edit:

  1. Defining Custom Private Properties: The “Function Open” in the VB script defines sample private properties. You will need to edit those add/remove private properties for your application.
  2. Validating Custom Private Properties: The “Function Online”, “Function Offline”, “Function Terminate”, “Function IsAlive” validate private properties whether they are set or not (in addition to being required or not). You will need to edit the validation checks for any private properties added/removed.
  3. Calling the PS scripts: The “PSCmd” variable contains the exact syntax of the PS script which gets called. For any private properties added/removed you would need to edit that PS script syntax as well.
  4. PowerShell scripts: Parameters for the PowerShell scripts would need to be edited for any private properties added/removed. In addition, your application specific logic would need to be added as specified by the comment in the PS scripts.

Now you can use PowerShell scripts to make any application highly available with Failover Clusters!!!

The sample VB script & the corresponding PS scripts allow you to take any custom application & make it highly available using PowerShell scripts.
Thanks,
Amitabh

This entry passed through the Full-Text RSS service – if this is your content and you’re reading it on someone else’s site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.
Recommended article from FiveFilters.org: Most Labour MPs in the UK Are Revolting.

Clustering and High-Availability