Using PowerShell script make any application highly available

First published on MSDN on Jun 06, 2016

Author:

Amitabh Tamhane

Senior Program Manager

Microsoft

OS releases: Applicable to 2008 R2 or later

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

The Generic Script is a built-in resource type included in Windows Server 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 – including Windows . 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 – to build a custom GenScript resource that works well with PowerShell.

Pre-requisites: This blog assumes you have the basic understanding of Windows Server 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 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:



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

 

This article was originally published by Microsoft’s Failover Clustering Blog. You can find the original article here.