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.
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
In the above sample VB script, the following entry points are defined:
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.
For resources of any type, Failover Cluster supports two types of 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):
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:
Example of PS scripts:
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
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
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
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
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…
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:
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:
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.
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!
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:
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.
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:
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
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:
- 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.
- 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.
- 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.
- 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
Updated Mar 15, 2019
Version 2.0John Marlin
Microsoft
Joined August 24, 2017
Failover Clustering
Follow this blog board to get notified when there's new activity