Using PowerShell script make any application highly available

This post has been republished via RSS; it originally appeared at: Failover Clustering articles.

First published on MSDN on Jun 06, 2016
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.

Visual Basic Shell


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.

PowerShell Scripts


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.).

Step-By-Step Walk-Through


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:


Create Group & Resource


Using PowerShell:



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:


Specify VB Script


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



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:



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:



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



To view your application state in Failover Cluster Manager GUI:


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:



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

Customizing 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.


Summary


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

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.