vSAN Health Alarm Check Script (Using PowerCLI)

NSX Manager to login with a local account

In the third and final part of this series I have taken my basic skeleton from the previous two blogs in order to solve the issue of bringing all of the vSAN Skyline Health checks into one central location using a vSAN Health Alarm Check Script.

My two previous blogs can be found here:
NSX Backup Check Script (Using the NSX Web API)
NSX Alarm Check Script (Using the NSX REST API)

Unfortunately this time, despite best efforts I was unable to get a suitable result using the vCenter REST API. Documentation is lacking and I was not able to get full results for the Skyline Health Checks. From asking around it seems that PowerCLI holds the answer for me, so it gave me an excuse to adapt the script again and get it to work with PowerCLI.

Again you might be asking ‘why not just use the vSAN Management Pack for vROps?’ but alas it does not keep pace with the vSAN Skyline Health and it is missing some alarms.

PowerCLI

For those not aware of everything PowerCLI can do you can find the full reference of the vSphere and vSAN cmdlets here:

https://developer.vmware.com/docs/powercli/latest/products/vmwarevsphereandvsan/

We are going to be using the Get-VSANView cmdlet in order to pull out the information from the vCenter.

The health information we can get with the “VsanVcClusterHealthSystem-vsan-cluster-health-system” Managed Object. Details of this can be found here:

https://vdc-download.vmware.com/vmwb-repository/dcr-public/3325c370-b58c-4799-99ff-58ae3baac1bd/45789cc5-aba1-48bc-a320-5e35142b50af/doc/vim.cluster.VsanVcClusterHealthSystem.html

The Code Changes

The Try Catch has been changed to connect to the vCenter first and then call a function to get the vSAN Health Summary

try{
    Connect-VIServer -Server $vCenter -Credential $encodedlogin
    $Clusters = Get-Cluster

    foreach ($Cluster in $Clusters) {
        Get-VsanHealthSummary -Cluster $Cluster
    }
 }
catch {catchFailure}

So lets have a look at the function itself.

The Get vSAN Cluster Health function

I have written a function to take in a cluster name as a parameter, find the Managed Object Reference (MORef) for the cluster, and then query the vCenter for the vSAN cluster health for that MORef and output any which are Yellow (Warning) or Red (Critical)

Function Get-VsanHealthSummary {

    param(
        [Parameter(Mandatory=$true)][String]$Cluster
    )
    
    $vchs = Get-VSANView -Id "VsanVcClusterHealthSystem-vsan-cluster-health-system"
    $cluster_view = (Get-Cluster -Name $Cluster).ExtensionData.MoRef
    $results = $vchs.VsanQueryVcClusterHealthSummary($cluster_view,$null,$null,$true,$null,$null,'defaultView')
    $healthCheckGroups = $results.groups
    $timestamp = (Get-Date).ToString("yyyy/MM/dd HH:mm:ss")

    foreach($healthCheckGroup in $healthCheckGroups) {
        
        $Health = @("Yellow","Red")
        $output = $healthCheckGroup.grouptests | where TestHealth -in $Health | select TestHealth,@{l="TestId";e={$_.testid.split(".") | select -last 1}},TestName,TestShortDescription,@{l="Group";e={$healthCheckGroup.GroupName}}
        $healthCheckTestHealth = $output.TestHealth
        $healthCheckTestName = $output.TestName
        $healthCheckTestShortDescription = $output.TestShortDescription
        
        if ($healthCheckTestHealth -eq "yellow") {
            $healthCheckTestHealthAlt = "Warning"
        }
        if ($healthCheckTestHealth -eq "red") {
            $healthCheckTestHealthAlt = "Critical"
            }
        if ($healthCheckTestName){
            Add-Content -Path $exportpath -Value "$timestamp [$healthCheckTestHealthAlt] $vCenter - vSAN Clustername $Cluster vSAN Alarm Name $healthCheckTestName Alarm Description $healthCheckTestShortDescription"
            Start-Sleep -Seconds 1
        }
    }
}

Saving Credentials

This time as we are using PowerCLI and Connect-VIServer we cannot use the encoded credentials we used last time for the Web and REST API, so we will use the cmdlet Export-CLIxml which allows us to create an XML-based representation of an object and stores it in a file.

Further details of this utility can be found here:

https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/export-clixml?view=powershell-7.3

We will use the Get-Credential to bring in the username and password to store and then export it to the path defined in the variables at the top of the script.

if (-Not(Test-Path -Path  $credPath)) {
    $credential = Get-Credential
    $credential | Export-Clixml -Path $credPath

}

$encodedlogin = Import-Clixml -Path $credPath

Handling the Outputs.

As per my previous scripts the outputs are formatted to be ingested into a syslog server (vRealize Log Insight in this case) which would then send emails to the appropriate places and allow for a nice dashboard for quick whole estate checks.

The Final vSAN Health Alarm Check Script

I have put all the variables at the top and the script is designed to be run in a folder and to have another separate folder with the logs. This was done in order to manage multiple scripts logging to the same location
eg:
c:\scripts\NSXBackupCheck\NSXBackupCheck.ps1
c:\scripts\Logs\NSXBackupCheck.log

param ($vCenter)

$curDir = &{$MyInvocation.PSScriptRoot}
$exportpath = "$curDir\..\Logs\vSANAlarmCheck.log"
$credPath = "$curDir\$vCenter.cred"
$scriptName = &{$MyInvocation.ScriptName}

add-type @"
   using System.Net;
   using System.Security.Cryptography.X509Certificates;
   public class TrustAllCertsPolicy : ICertificatePolicy {
      public bool CheckValidationResult(
      ServicePoint srvPoint, X509Certificate certificate,
      WebRequest request, int certificateProblem) {
      return true;
   }
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

Function Get-VsanHealthSummary {

    param(
        [Parameter(Mandatory=$true)][String]$Cluster
    )
    
    $vchs = Get-VSANView -Id "VsanVcClusterHealthSystem-vsan-cluster-health-system"
    $cluster_view = (Get-Cluster -Name $Cluster).ExtensionData.MoRef
    $results = $vchs.VsanQueryVcClusterHealthSummary($cluster_view,$null,$null,$true,$null,$null,'defaultView')
    $healthCheckGroups = $results.groups
    $timestamp = (Get-Date).ToString("yyyy/MM/dd HH:mm:ss")

    foreach($healthCheckGroup in $healthCheckGroups) {

        
        $Health = @("Yellow","Red")
        $output = $healthCheckGroup.grouptests | where TestHealth -in $Health | select TestHealth,@{l="TestId";e={$_.testid.split(".") | select -last 1}},TestName,TestShortDescription,@{l="Group";e={$healthCheckGroup.GroupName}}
        $healthCheckTestHealth = $output.TestHealth
        $healthCheckTestName = $output.TestName
        $healthCheckTestShortDescription = $output.TestShortDescription
        
        if ($healthCheckTestHealth -eq "yellow") {
            $healthCheckTestHealthAlt = "Warning"
        }

        if ($healthCheckTestHealth -eq "red") {
            $healthCheckTestHealthAlt = "Critical"
            }


        if ($healthCheckTestName){
            Add-Content -Path $exportpath -Value "$timestamp [$healthCheckTestHealthAlt] $vCenter - vSAN Clustername $Cluster vSAN Alarm Name $healthCheckTestName Alarm Description $healthCheckTestShortDescription"
            Start-Sleep -Seconds 1
        }
    }

}

function catchFailure {
    $timestamp = (Get-Date).ToString("yyyy/MM/dd HH:mm:ss")
    if (Test-Connection -BufferSize 32 -Count 1 -ComputerName $vCenter -Quiet) {
        Add-Content -Path $exportpath -Value "$timestamp [ERROR] $vCenter - $_"
    }
    else {
        Add-Content -Path $exportpath -Value "$timestamp [ERROR] $vCenter - Host Not Found"
    }
exit
}

if (!$vCenter) {
    Write-Host "please provide parameter 'vCenter' in the format '$scriptName -vCenter [FQDN of vCenter Server]'"
    exit
    }

if (-Not(Test-Path -Path  $credPath)) {
    $credential = Get-Credential
    $credential | Export-Clixml -Path $credPath

}

$encodedlogin = Import-Clixml -Path $credPath


try{
    Connect-VIServer -Server $vCenter -Credential $encodedlogin
    $Clusters = Get-Cluster

    foreach ($Cluster in $Clusters) {
        Get-VsanHealthSummary -Cluster $Cluster
    }
 }
catch {catchFailure}

Disconnect-VIServer $vCenter -Confirm:$false

Overview

The final script above can be altered to be used as a skeleton for any other PowerShell or PowerCLI commands, as well as being adapted for REST APIs and Web API as per the previous Blogs. Important to note that these will use a different credential store function.

The two previous blogs can be found here:
NSX Backup Check Script (Using the NSX Web API)
NSX Alarm Check Script (Using the NSX REST API)

NSX Alarm Check Script (Using the NSX REST API)

NSX Manager to login with a local account

In my previous blog I created a script to get the last backup status from NSX Manager in order to quickly check multiple NSX Managers. Today I had a need to bring the alarms raised in all of these NSX Managers into one single location, which necessitated creating an NSX Alarm Check Script.
‘But surely the NSX Management Pack would allow you to do this you?’ may ask. Unfortunately it is missing some of the alarms which gets raised on the NSX Managers such as passwords expiring for example. This one being an annoyance if you do not notice until after it’s expired and you are having LDAP issues.

Now luckily this time, we CAN use the NSX REST API to get these details, and I had a script lying around which could provide a skeleton for this. You can find that script here: NSX Backup Check Script

In order to adapt this script to use REST we need to change the Invoke-WebMethod to Invoke-RestMethod

Interrogating NSX REST API

I used the documentation from VMware {code} to find this API and how to handle the results. Luckily this is a lot more detailed than the web API. You can find the NSX API details here:

https://developer.vmware.com/apis/547/nsx-t

so we want to request /api/v1/alarms in order to return a list of all alarms on the nsx managers.

$result = Invoke-RestMethod -Uri https://$nsxmgr/api/v1/alarms -Headers $Header -Method 'GET' -UseBasicParsing

Handling the Outputs.

Running this command will give a response similar to this:

{
  "result_count": 4,
  "results": [
      {
        "id": "xxxx",
        "status": "OPEN",
        "feature_name": "manager_health",
        "event_type": "manager_cpu_usage_high",
        "feature_display_name": "Manager Health",
        "event_type_display_name": "CPU Usage High",
        "node_id": "xxxx",
        "last_reported_time": 1551994806,
        "description": |
          "The CPU usage for the manager node identified by 
           appears to be\nrising.",
        "recommended_action": |
          "Use the top command to check which processes have the most CPU
           usages, and\nthen check \/var\/log\/syslog and these processes'
           local logs to see if there\nare any outstanding errors to be
           resolved.",
        "node_resource_type": "ClusterNodeConfig",
        "severity": "WARNING",
        "entity_resource_type": "ClusterNodeConfig",
      },
      ...
  ]
}

From this output I wanted to pull out the severity, status, alarm description and the node which was impacted, so I pulled these into an array and add the items to variables.

$nsxAlarms = $result.results 
    foreach ($nsxAlarm in $nsxalarms) {
        $nsxAlarmCreated = (get-date 01.01.1970).AddSeconds([int]($nsxAlarm._create_time/1000)).ToString("yyyy/MM/dd HH:mm:ss")
        $timestamp = (Get-Date).ToString("yyyy/MM/dd HH:mm:ss")
        $nsxAlarmSeverity = $nsxAlarm.severity
        $nsxAlarmStatus = $nsxAlarm.status
        $nsxAlarmNode_display_name = $nsxAlarm.node_display_name
        $nsxAlarmDescription = $nsxAlarm.description

From here I wanted to only include any alarms which had not been marked acknowledged or resolved to avoid constantly reporting a condition which was known about.

if($nsxAlarm.status -ne "ACKNOWLEDGED" -and $nsxAlarm.status -ne "RESOLVED"){ 
    Add-Content -Path $exportpath -Value "$timestamp [$nsxAlarmSeverity] $NSXMGR - Alarm Created $nsxAlarmCreated Status $nsxAlarmStatus Affected Node $nsxAlarmNode_display_name Description  $nsxAlarmDescription"
}

It is also possible to bypass this by running the following command, however I wanted to pull in all alarms for my specific use case.

GET /api/v1/alarms?status=OPEN

As per the previous script, this was wrapped in a try catch and the catch failure tested if the host was up. A full explanation can be found on the blog about this script here: NSX Backup Check Script

.

The Final NSX Alarm Check Script

param ($nsxmgr)

$curDir = &{$MyInvocation.PSScriptRoot}
$exportpath = "$curDir\..\Logs\NSXAlarmCheck.log"
$credPath = "$curDir\$nsxmgr.cred"
$scriptName = &{$MyInvocation.ScriptName}

add-type @"
   using System.Net;
   using System.Security.Cryptography.X509Certificates;
   public class TrustAllCertsPolicy : ICertificatePolicy {
      public bool CheckValidationResult(
      ServicePoint srvPoint, X509Certificate certificate,
      WebRequest request, int certificateProblem) {
      return true;
   }
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

function catchFailure {
    $timestamp = (Get-Date).ToString("yyyy/MM/dd HH:mm:ss")
    if (Test-Connection -BufferSize 32 -Count 1 -ComputerName $nsxmgr -Quiet) {
        Add-Content -Path $exportpath -Value "$timestamp [ERROR] $NSXMGR - $_"
    }
    else {
        Add-Content -Path $exportpath -Value "$timestamp [ERROR] $NSXMGR - Host Not Found"
    }
exit
}

if (!$nsxmgr) {
    Write-Host "please provide parameter 'nsxmgr' in the format '$scriptName -nsxmgr [FQDN of NSX Manager]'"
    exit
    }

if (-Not(Test-Path -Path  $credPath)) {
    $username = Read-Host "Enter username for NSX Manager" 
    $pass = Read-Host "Enter password" -AsSecureString 
    $password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pass))
    $userpass  = $username + ":" + $password

    $bytes= [System.Text.Encoding]::UTF8.GetBytes($userpass)
    $encodedlogin=[Convert]::ToBase64String($bytes)
    
    Set-Content -Path $credPath -Value $encodedlogin
}

$encodedlogin = Get-Content -Path $credPath

$authheader = "Basic " + $encodedlogin
$header = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$header.Add("Authorization",$authheader)

try{
    $result = Invoke-RestMethod -Uri https://$nsxmgr/api/v1/alarms -Headers $Header -Method 'GET' -UseBasicParsing

        $nsxAlarms = $result.results 
        foreach ($nsxAlarm in $nsxalarms) {
            
            $nsxAlarmCreated = (get-date 01.01.1970).AddSeconds([int]($nsxAlarm._create_time/1000)).ToString("yyyy/MM/dd HH:mm:ss")
            $timestamp = (Get-Date).ToString("yyyy/MM/dd HH:mm:ss")
            $nsxAlarmSeverity = $nsxAlarm.severity
            $nsxAlarmStatus = $nsxAlarm.status
            $nsxAlarmNode_display_name = $nsxAlarm.node_display_name
            $nsxAlarmDescription = $nsxAlarm.description

            if($nsxAlarm.status -ne "ACKNOWLEDGED" -and $nsxAlarm.status -ne "RESOLVED"){ 
                Add-Content -Path $exportpath -Value "$timestamp [$nsxAlarmSeverity] $NSXMGR - Alarm Created $nsxAlarmCreated Status $nsxAlarmStatus Affected Node $nsxAlarmNode_display_name Description  $nsxAlarmDescription"
            }
        
    }
 }
catch {catchFailure}

Overview

The final script above can be altered to be used as a skeleton for any other Invoke-RestRequest APIs as well as simply being adapted for Web API. I will be following up this post with further updates to adapt the script in order to use PowerCLI, which required a different credential store.


NSX Backup Check Script (Using the NSX Web API)

NSX Manager to login with a local account

I was recently asked for a way to have a simple check and report on the last backup status for a global company with multiple VMware NSX managers.

For some reason their NSX Managers were not reporting the backup status via syslog to VMware vRealize Log Insight (vRLI) and even if it was, they only have one vRLI cluster per site and wanted one simple place to do their daily checks.

So let’s make an NSX backup check script, PowerShell and REST API to the rescue! … right?

So … no, you cannot get the backup status via REST API

Great.

But you can via the WebAPI!

Hurrah! Lets throw in some Invoke-WebRequest and get the data we need.

After some basic checks, I got the info I wanted – now I need to schedule it and have it run a short period after the backup window.

This part resulted in a path of trying to figure out a way to hold account passwords in a usable manner without them being written in clear-text anywhere because that’s just no good. There are a few different ways to do this, but they either tie it to one user profile and computer, or don’t work with the basic auth needed to run against NSX to get the data via webrequest. I will go into how I achieved that further down, but first, the web API to get backup status.

Interrogating NSX Web API

After some looking around, I discovered the following URL called via Invoke-WebRequest would give us the backup results:

Invoke-WebRequest -Uri https://[nsxmgr]/api/v1/cluster/backups/overview -Headers $Header 

Now the big problem with Invoke-WebRequest is that you would have assumed that it would return any response status such as 403 Forbidden. Nope!

You don’t get any helpful error catching, it either works or bombs out. Not much good for an unattended script that you want to tell you about any issues.

So the best fix I came up with was using a try and catch

try { 
    $result = Invoke-WebRequest -Uri https://...
    }
catch {catchFailure}

I then created a function to run in the event of the failure which will ping the host to see if it’s online and if it is output the error, if it isn’t output that the host is unreachable.

if (Test-Connection -BufferSize 32 -Count 1 -ComputerName $nsxmgr -Quiet) {
        <error output>
    } else { <host offline output> } exit

Job jobbed, no more bombing out with red text.

Dealing with certificates

When you run Invoke-WebRequest against an NSX manager with self signed certificates you get the error "The Underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel"

The fix for this is to add in this code near the top of your script to resolve the error.

add-type @"
   using System.Net;
   using System.Security.Cryptography.X509Certificates;
   public class TrustAllCertsPolicy : ICertificatePolicy {
      public bool CheckValidationResult(
      ServicePoint srvPoint, X509Certificate certificate,
      WebRequest request, int certificateProblem) {
      return true;
   }
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

Password Management

Great stuff I now have a working Script, but ideally, I want it to be scheduled and unattended.

This is where I spun around for a while trying different ways to store credentials in a secure format, because passwords in plain text is uncool.

I initially was trying to use the encoded credentials modules but having little luck getting it passed as a header value, so bugged a colleague (@pauldavey_79) for some help and ideas from his many years of experience prodding APIs.

What we came up with was to take the username and password as an requested input via Read-Host and encode it in the Base 64 format required to pass via the header in Invoke-WebRequest and store that in a text file.

[System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pass))
    $userpass  = $username + ":" + $password

    $bytes= [System.Text.Encoding]::UTF8.GetBytes($userpass)
    $encodedlogin=[Convert]::ToBase64String($bytes)
    
    Set-Content -Path $credPath -Value $encodedlogin

This worked a charm.

Handling the Outputs.

With this script I wanted to feed the output into a syslog server (vRealize Log Insight in this case) which would then send emails to the appropriate places and allow for a nice dashboard for quick whole estate checks.

In order to achieve this, I used the Add-Content command to append the data to a .log file which was monitored by the Log Insight Agent and sent off to the Log Insight Server.

if($LatestBackup.success -eq $true){ 
  Add-Content -Path $exportpath -Value "$timestamp [INFO] $NSXMGR - Last backup successful. Start time $start End time $end"
} else{ 
  Add-Content -Path $exportpath -Value "$timestamp [ERROR] $NSXMGR - Last backup failed $start $end"

This gives us a nice syslog formatted output which can be easily manipulated within Log Insight. Hurrah.

One thing to note is that the NSX WebAPI returned the start and end times in the usual unix format, so I needed to convert that to a more suitable human readable date, that was done with the line:

 $var = (get-date 01.01.1970).AddSeconds([int]($LatestBackup.end_time/1000))

I also needed to get my try-catch error collector to output the error messages in the same format so that was done as so:

Add-Content -Path $exportpath -Value "$timestamp [ERROR] $NSXMGR - $_"

Pulling all of that together we get the final script which can be used as a skeleton for any future work required. A few of them will be posted at a later date.

The Final NSX Backup Check Script

I have put all the variables at the top and the script is designed to be run in a folder and to have another separate folder with the logs. This was done in order to manage multiple scripts logging to the same location
eg:
c:\scripts\NSXBackupCheck\NSXBackupCheck.ps1
c:\scripts\Logs\NSXBackupCheck.log

param ($nsxmgr)

$curDir = &{$MyInvocation.PSScriptRoot}
$exportpath = "$curDir\..\Logs\NSXBackupCheck.log"
$credPath = "$curDir\$nsxmgr.cred"
$scriptName = &{$MyInvocation.ScriptName}

add-type @"
   using System.Net;
   using System.Security.Cryptography.X509Certificates;
   public class TrustAllCertsPolicy : ICertificatePolicy {
      public bool CheckValidationResult(
      ServicePoint srvPoint, X509Certificate certificate,
      WebRequest request, int certificateProblem) {
      return true;
   }
}
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

function catchFailure {
    $timestamp = (Get-Date).ToString("yyyy/MM/dd HH:mm:ss")
    if (Test-Connection -BufferSize 32 -Count 1 -ComputerName $nsxmgr -Quiet) {
        Add-Content -Path $exportpath -Value "$timestamp [ERROR] $NSXMGR - $_"
    }
    else {
        Add-Content -Path $exportpath -Value "$timestamp [ERROR] $NSXMGR - Host Not Found"
    }
exit
}

if (!$nsxmgr) {
    Write-Host "please provide parameter 'nsxmgr' in the format '$scriptName -nsxmgr [FQDN of NSX Manager]'"
    exit
    }

if (-Not(Test-Path -Path  $credPath)) {
    $username = Read-Host "Enter username for NSX Manager" 
    $pass = Read-Host "Enter password" -AsSecureString 
    $password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pass))
    $userpass  = $username + ":" + $password

    $bytes= [System.Text.Encoding]::UTF8.GetBytes($userpass)
    $encodedlogin=[Convert]::ToBase64String($bytes)
    
    Set-Content -Path $credPath -Value $encodedlogin
}

$encodedlogin = Get-Content -Path $credPath

$authheader = "Basic " + $encodedlogin
$header = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$header.Add("Authorization",$authheader)

try{
    $result = Invoke-WebRequest -Uri https://$nsxmgr/api/v1/cluster/backups/overview -Headers $Header -UseBasicParsing
    if($result.StatusCode -eq 200) {
        $nsxbackups = $result.Content | ConvertFrom-Json
        $LatestBackup = $nsxbackups.backup_operation_history.cluster_backup_statuses
        $start = (get-date 01.01.1970).AddSeconds([int]($LatestBackup.start_time/1000))
        $end = (get-date 01.01.1970).AddSeconds([int]($LatestBackup.end_time/1000))
        $timestamp = (Get-Date).ToString("yyyy/MM/dd HH:mm:ss")
        if($LatestBackup.success -eq $true){ 
            Add-Content -Path $exportpath -Value "$timestamp [INFO] $NSXMGR - Last backup successful. Start time $start End time $end"
        } else{ 
            Add-Content -Path $exportpath -Value "$timestamp [ERROR] $NSXMGR - Last backup failed $start $end"
        }
    }
 }
catch {catchFailure}

Overview

The final script above can be altered to be used as a skeleton for any other Invoke-WebRequest APIs as well as simply being adapted for REST API. I will be following up this post with further updates to this script using RESTAPI and also an adaption to use PowerCLI which required a different credential store.

The REST API Script can be found here: NSX Alarm Check Script

How to Build a PowerShell Menu GUI for your PowerShell Scripts

Purely for prosperity I have recreated this post from Nathan Kasco so I can find it better in future. Copyright Nathan Kasco. All the words and code is his.

It’s weekend project time again and today you will learn how to build a lightweight system tray context menu where you can quickly and easily launch your most coveted PowerShell scripts. You can see below the end result.

In this article, you’ll learn how to build your own PowerShell menu GUI by breaking the process down step-by-step.

Table of Contents

  • Environment and Knowledge Requirements
  • Show/Hide Console Window
  • Create Menu Options
  • Creating A Launcher Form
  • Show the Launcher Form

Environment and Knowledge Requirements

Before you dive in, please be sure you meet the following minimum requirements:

For this project, the good news is that you won’t really need to rely on Visual StudioPoshGUI, or any other UI development tool as the primary components that this project will rely on the following:

  • NotifyIcon – This will represent our customizable system tray icon for the user to interact with.
  • ContextMenu – Container for when the user right-clicks on the tray icon.
  • MenuItem – Individual objects for each option within the right-click menu.

Open up your favorite PowerShell script editor and let’s get started!

For this project you are going to build three functions: two functions to show/hide the console to provide a cleaner user experience and one to add items to your systray menu. These functions will serve as a foundation for later use to make your life much easier as you will learn a bit later in this article.

Show/Hide Console Window

Unless hidden, when you launch a PowerShell script, the familiar PowerShell console will come up. Since the menu items you’ll create will launch scripts, you should ensure the console doesn’t up. You just want it to execute.

When a script is executed, you can toggle the PowerShell console window showing or not using a little .NET.

First add the Window .NET type into the current session. To do this, you’ll use some C# as you’ll see below. The two methods you need to load into context are GetConsoleWindow and ShowWindow. By loading these DLLs into memory you are exposing certain parts of the API, this allows you to use them in the context of your PowerShell script:

 #Load dlls into context of the current console session
 Add-Type -Name Window -Namespace Console -MemberDefinition '
    [DllImport("Kernel32.dll")]
    public static extern IntPtr GetConsoleWindow();
 
    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
 '

Create two functions using the loaded above using the GetConsoleWindow() and ShowWindow() method as shown below.

 function Start-ShowConsole {
    $PSConsole = [Console.Window]::GetConsoleWindow()
    [Console.Window]::ShowWindow($PSConsole, 5)
 }
 
 function Start-HideConsole {
    $PSConsole = [Console.Window]::GetConsoleWindow()
    [Console.Window]::ShowWindow($PSConsole, 0)
 }

With these two functions you now have created a way in which you can show or hide the console window at will.

Note: If you’d like to see output from the scripts executed via the menu, you can use PowerShell transcripts or other text-based logging features. This allows you to maintain control versus only running the PowerShell session with the WindowStyle parameter to hide.

Now begin building script code by calling Start-HideConsole. When the menu-driven script executes, this will ensure the PowerShell console window doesn’t come up.

<# 
	Initialization of functions and objects loading into memory
	Display a text-based loading bar or Write-Progress to the host
#>
 
Start-HideConsole
 
<# 
	Code to display your form/systray icon
	This will hold the console here until closed
 #>

Create Menu Options

Now it’s time to create the menu options. Ensuring you can easily create new options later on, create another function this time called New-MenuItem. When you call this function, it will create a new MenuItem .NET object which you can then add to the menu later.

Each menu option will launch another script or exit the launcher. To accommodate for this functionality,  the New-MenuItem function has three parameters:

  • Text – The label the user will click on
  • MyScriptPath – The path to the PowerShell script to execute
  • ExitOnly – The option to exit the launcher.

Add the below function snippet to the menu script.

 function New-MenuItem{
     param(
         [string]
         $Text = "Placeholder Text",
 
         $MyScriptPath,
         
         [switch]
         $ExitOnly = $false
     )         

Continuing on building the New-MenuItem function, create a MenuItem object by assigning it to a variable.

 #Initialization
 $MenuItem = New-Object System.Windows.Forms.MenuItem

Next, assign the text label to the menu item.

 # Apply desired text
 if($Text) {
 	$MenuItem.Text = $Text
 }

Now add a custom property to the MenuItem called MyScriptPath. This path will be called upon when the item is clicked in the menu.

 #Apply click event logic
 if($MyScriptPath -and !$ExitOnly){
 	$MenuItem | Add-Member -Name MyScriptPath -Value $MyScriptPath -MemberType NoteProperty

Add a click event to the MenuItem that launches the desired script. Start-Process provides a clean way to do this within a try/catch block so that you can make sure any errors launching the script (such as PowerShell not being available or the script not existing at the provided path) fall to your catch block.

   $MenuItem.Add_Click({
        try{
            $MyScriptPath = $This.MyScriptPath #Used to find proper path during click event
            
            if(Test-Path $MyScriptPath){
                Start-Process -FilePath "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -ArgumentList "-NoProfile -NoLogo -ExecutionPolicy Bypass -File `"$MyScriptPath`"" -ErrorAction Stop
            } else {
                throw "Could not find at path: $MyScriptPath"
            }
        } catch {
          $Text = $This.Text
          [System.Windows.Forms.MessageBox]::Show("Failed to launch $Text`n`n$_") > $null
        }
  })

Sdd the remaining logic to provide an exit condition for the launcher followed by returning your newly created MenuItem back to be assigned to another variable at runtime.

    #Provide a way to exit the launcher
    if($ExitOnly -and !$MyScriptPath){
        $MenuItem.Add_Click({
            $Form.Close()
    
            #Handle any hung processes
            Stop-Process $PID
        })
    }
 
 	 #Return our new MenuItem
    $MenuItem
 }

You should now have the New-MenuItem function created! The final function should look like this:

  function New-MenuItem{
     param(
         [string]
         $Text = "Placeholder Text",
 
         $MyScriptPath,
         
         [switch]
         $ExitOnly = $false
     )
 
     #Initialization
     $MenuItem = New-Object System.Windows.Forms.MenuItem
 
     #Apply desired text
     if($Text){
         $MenuItem.Text = $Text
     }
 
     #Apply click event logic
     if($MyScriptPath -and !$ExitOnly){
         $MenuItem | Add-Member -Name MyScriptPath -Value $MyScriptPath -MemberType NoteProperty
     }
 
     $MenuItem.Add_Click({
             try{
                 $MyScriptPath = $This.MyScriptPath #Used to find proper path during click event
             
                 if(Test-Path $MyScriptPath){
                     Start-Process -FilePath "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -ArgumentList "-NoProfile -NoLogo -ExecutionPolicy Bypass -File `"$MyScriptPath`"" -ErrorAction Stop
                 } else {
                     throw "Could not find at path: $MyScriptPath"
                 }
             } catch {
                 $Text = $This.Text
                 [System.Windows.Forms.MessageBox]::Show("Failed to launch $Text`n`n$_") > $null
             }
         })
 
     #Provide a way to exit the launcher
     if($ExitOnly -and !$MyScriptPath){
         $MenuItem.Add_Click({
                 $Form.Close()
    
                 #Handle any hung processes
                 Stop-Process $PID
             })
     }
 
     #Return our new MenuItem
     $MenuItem
 }

Test the New-MenuItem function by copying and pasting the above code into your PowerShell console and running the function providing some fake parameter values. You’ll see that a .NET MenuItem object is returned.

 PS51> (New-MenuItem -Text "Test" -MyScriptPath "C:\\test.ps1").GetType()
 
 IsPublic IsSerial Name                                     BaseType
 -------- -------- ----                                     --------
 True     False    MenuItem                                 System.Windows.Forms.Menu

Creating A Launcher Form

Want more tips like this? Check out my personal PowerShell blog at: https://nkasco.com/FriendsOfATA

Now that you can easily create new menu items, it’s time to create a system tray launcher which will display the menu.

Create a basic form object to add components to. This doesn’t need to be anything fancy as it will be hidden to the end user and will keep the console running in the background as well.

 #Create Form to serve as a container for our components
 $Form = New-Object System.Windows.Forms.Form
 ​
 #Configure our form to be hidden
 $Form.BackColor = "Magenta" #Match this color to the TransparencyKey property for transparency to your form
 $Form.TransparencyKey = "Magenta"
 $Form.ShowInTaskbar = $false
 $Form.FormBorderStyle = "None"

Next, create the icon that will show up in the system tray. Below I’ve chosen to use the PowerShell icon. At runtime, the below code creates an actual system tray icon. This icon can be customized to your liking by setting the SystrayIcon variable to your desired icon.

Check out the documentation for the System.Drawing.Icon class to see other methods in which you can load an icon into memory.

 #Initialize/configure necessary components
 $SystrayLauncher = New-Object System.Windows.Forms.NotifyIcon
 $SystrayIcon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:\\windows\\system32\\WindowsPowerShell\\v1.0\\powershell.exe")
 $SystrayLauncher.Icon = $SystrayIcon
 $SystrayLauncher.Text = "PowerShell Launcher"
 $SystrayLauncher.Visible = $true

When the script is run, you should then see a PowerShell icon show up in your system tray as you can see below.

Now, create a container for your menu items with a new ContextMenu object and create all of your menu items. For this example, the menu will have two scripts to run and an exit option.

 $ContextMenu = New-Object System.Windows.Forms.ContextMenu
 ​
 $LoggedOnUser = New-MenuItem -Text "Get Logged On User" -MyScriptPath "C:\\scripts\\GetLoggedOn.ps1"
 $RestartRemoteComputer = New-MenuItem -Text "Restart Remote PC" -MyScriptPath "C:\\scripts\\restartpc.ps1"
 $ExitLauncher = New-MenuItem -Text "Exit" -ExitOnly

Next, add all of the menu items just created to the context menu. This will ensure each menu option shows up in the form context menu.

 #Add menu items to context menu
 $ContextMenu.MenuItems.AddRange($LoggedOnUser)
 $ContextMenu.MenuItems.AddRange($RestartRemoteComputer)
 $ContextMenu.MenuItems.AddRange($ExitLauncher)
 ​
 #Add components to our form
 $SystrayLauncher.ContextMenu = $ContextMenu

Show the Launcher Form

Now that the form is complete, the last thing to do is to show it while ensuring the PowerShell console window doesn’t come up. Do this by using your Start-HideConsole , displaying the launcher form and then showing the console again withStart-ShowConsole to prevent a hung powershell.exe process.

#Launch
Start-HideConsole
$Form.ShowDialog() > $null
Start-ShowConsole

The full code in its entirety can be found here: https://github.com/nkasco/PSSystrayLauncher

or below as a text file. Please convert to ps1 to run

Script to Add Custom Icons to a Horizon Application

Script to add icon to horizon application

Quick way to get a list of icons:

Get-HVApplication | Select-Object name,displayname |Export-Csv -path C:\HVIcons\newlist.csv

I wrote the following script to take a csv file of application name and description, and then connect to a Horizon Connection Server and cycle through the csv looking for an icon file in the format desc.png or desc-0.png and apply the icon to the application.

It is made up of three modules, one of which is a lightly modified version of a VMware staffer’s module, which included a load of “breaks” which I had to remove to ensure it would complete the full array.



############################################################
### Add Icons to Horizon Application from a CSV File
### .NOTES
###    Author                      : Chris Mitchell
###    Author email                : mitchellc@vmware.com
###    Version                     : 1.0
###
###    ===Tested Against Environment====
###    Horizon View Server Version : 7.7
###    PowerCLI Version            : PowerCLI 11.3
###    PowerShell Version          : 5.1
###
###    Add-HVAppIcon -h VCS01.local.net -csv apps.csv -u admin -p Password01
###
############################################################

param (
    [string]$h = $(Read-Host "Horizon Connection Server Name:"),
    [string]$c = $(Read-Host "csv filename:"),
    [string]$u = $(Read-Host "username:"),
    [string]$p = $(Read-Host "password:")
    )


function Get-ViewAPIService {
  param(
    [Parameter(Mandatory = $false)]
    $HvServer
  )
  if ($null -ne $hvServer) {
    if ($hvServer.GetType().name -ne 'ViewServerImpl') {
      $type = $hvServer.GetType().name
      Write-Error "Expected hvServer type is ViewServerImpl, but received: [$type]"
      return $null
    }
    elseif ($hvServer.IsConnected) {
      return $hvServer.ExtensionData
    }
  } elseif ($global:DefaultHVServers.Length -gt 0) {
     $hvServer = $global:DefaultHVServers[0]
     return $hvServer.ExtensionData
  }
  return $null
}



function HVApplicationIcon {
<#
.SYNOPSIS
   Used to create/update an icon association for a given application.

.DESCRIPTION
   This function is used to create an application icon and associate it with the given application. If the specified icon already exists in the LDAP, it will just updates the icon association to the application. Any of the existing customized icon association to the given application will be overwritten.

.PARAMETER ApplicationName
   Name of the application to which the association to be made.

.PARAMETER IconPath
   Path of the icon.

.PARAMETER HvServer
   View API service object of Connect-HVServer cmdlet.

.EXAMPLE
   Creating the icon I1 and associating with application A1. Same command is used for update icon also.
   Set-HVApplicationIcon -ApplicationName A1 -IconPath C:\I1.ico -HvServer $hvServer

.OUTPUTS
   None

.NOTES
    Author                      : Paramesh Oddepally.
    Author email                : poddepally@vmware.com
    Version                     : 1.1

    ===Tested Against Environment====
    Horizon View Server Version : 7.1
    PowerCLI Version            : PowerCLI 6.5.1
    PowerShell Version          : 5.0
#>

  [CmdletBinding(
    SupportsShouldProcess = $true,
    ConfirmImpact = 'High'
  )]

  param(
   [Parameter(Mandatory = $true)]
   [string] $ApplicationName,

   [Parameter(Mandatory = $true)]
   $IconPath,

   [Parameter(Mandatory = $false)]
   $HvServer = $null
  )

  begin {
    $services = Get-ViewAPIService -HvServer $HvServer
    if ($null -eq $services) {
      Write-Error "Could not retrieve ViewApi services from connection object."
      
    }
    Add-Type -AssemblyName System.Drawing
  }

  process {
	if (!(Test-Path $IconPath)) {
      Write-Error "File:[$IconPath] does not exist."
      
    }

    if ([IO.Path]::GetExtension($IconPath) -ne '.png') {
      Write-Error "Unsupported file format:[$IconPath]. Only PNG image files are supported."
      
    }

    try {
      $appInfo = Get-HVQueryResult -EntityType ApplicationInfo -Filter (Get-HVQueryFilter data.name -Eq $ApplicationName) -HvServer $HvServer
    } catch {
      # EntityNotFound, InsufficientPermission, InvalidArgument, InvalidType, UnexpectedFault
      Write-Error "Error in querying the ApplicationInfo for Application:[$ApplicationName] $_"
      
    }

    if ($null -eq $appInfo) {
      Write-Error "No application found with specified name:[$ApplicationName]."
      
    }

    $spec = New-Object VMware.Hv.ApplicationIconSpec
    $base = New-Object VMware.Hv.ApplicationIconBase

    try {
      $fileHash = Get-FileHash -Path $IconPath -Algorithm MD5
      $base.IconHash = $fileHash.Hash
      $base.Data = (Get-Content $iconPath -Encoding byte)
      $bitMap = [System.Drawing.Bitmap]::FromFile($iconPath)
      $base.Width = $bitMap.Width
      $base.Height = $bitMap.Height
      $base.IconSource = "broker"
      $base.Applications = @($appInfo.Id)
      $spec.ExecutionData = $base
    } catch {
      Write-Error "Error in reading the icon parameters: $_"
      
    }

    if ($base.Height -gt 256 -or $base.Width -gt 256) {
      Write-Error "Invalid image resolution. Maximum resolution for an icon should be 256*256."
      
    }

    $ApplicationIconHelper = New-Object VMware.Hv.ApplicationIconService
    try {
      $ApplicationIconId = $ApplicationIconHelper.ApplicationIcon_CreateAndAssociate($services, $spec)
    } catch {
        if ($_.Exception.InnerException.MethodFault.GetType().name.Equals('EntityAlreadyExists')) {
           # This icon is already part of LDAP and associated with some other application(s).
           # In this case, call updateAssociations
           $applicationIconId = $_.Exception.InnerException.MethodFault.Id
           Write-Host "Some application(s) already have an association for the specified icon."
           $ApplicationIconHelper.ApplicationIcon_UpdateAssociations($services, $applicationIconId, @($appInfo.Id))
           Write-Host "Successfully updated customized icon association for Application:[$ApplicationName]."
           
        }
        Write-Host "Error in associating customized icon for Application:[$ApplicationName] $_"
        
    }
    Write-Host "Successfully associated customized icon for Application:[$ApplicationName]."
  }

  end {
    [System.gc]::collect()
  }
}



Function AddIconToApp($a) {

    foreach($Item in $a)
    {
        $IconFile = $IconFolder+"\"+$Item.DisplayName+".png"
        if (!(Test-Path $IconFile)) { 
            $IconFile = $IconFolder+"\"+$Item.DisplayName+"-0.png"
        }
        Write-Host Trying $IconFile
        if (Test-Path $IconFile) { 
            Write-Host "Adding Icon to" $Item.DisplayName
            $ItemName = $Item.Name
            HVApplicationIcon -ApplicationName $ItemName -IconPath $IconFile -ErrorAction SilentlyContinue
        } else {
                Write-Host "File Doesn't Exist"
        }  
        
    }
}


$dir = Get-Location

$IconFolder = $dir.Path + "\png"


Connect-HVServer $h -u $u -p $p

$csv = Import-Csv .\$c

AddIconToApp $csv

Disconnect-HVServer -Server * -confirm:$false




Converting ICO to PNG for Adding Application Icons to a Horizon Application

Prior to Horizon 7.9, in order to add custom application icons to an Application launcher you are required to run a PowerCLI cmdlet:

Set-HVApplicationIcon -ApplicationName MyApp -IconPath "C:\MyIcons\MyApp.png

The important thing to note here is that it only accepts PNG files as input. However what if you only have a giant collection of ICO files you want to use.

That’s where I found myself with a few hundred icons which i needed to convert to PNG and remove the transparency setting it as white, so I wrote the following PowerShell Script to extract the bitmaps from the ico and then convert to PNG.

#Convert-ICO2PNG.ps1
$files = Get-ChildItem "C:\icotest" -Filter *.ico -file -Recurse | 
foreach-object {

$Source = $_.FullName
$test = [System.IO.Path]::GetDirectoryName($source)
$base= $_.BaseName+".png"
$basedir = $test+"\"+$base
Write-Host $basedir
Add-Type -AssemblyName system.drawing
$imageFormat = "System.Drawing.Imaging.ImageFormat" -as [type]
$image = [drawing.image]::FromFile($Source)

# Create a new image
$NewImage = [System.Drawing.Bitmap]::new($Image.Width,$Image.Height)
$NewImage.SetResolution($Image.HorizontalResolution,$Image.VerticalResolution)

# Add graphics based on the new image
$Graphics = [System.Drawing.Graphics]::FromImage($NewImage)
$Graphics.Clear([System.Drawing.Color]::White) # Set the color to white
$Graphics.DrawImageUnscaled($image,0,0) # Add the contents of $image

# Now save the $NewImage 
$NewImage.Save($basedir,$imageFormat::Png)}  

Now you’ve got your PNGs you can add them to your application. See here for a script on doing that: LINK

Script for restarting vSAN services on ESXi hosts

On a recent project we discovered an issue with vSAN which necessitated restarting the vSAN services on all of the hosts in the affected vSAN cluster.

So digging into the bag brought out the script detailed in an earlier post for updating Likewise settings on ESXi hosts http://www.caenotech.co.uk/vmware/script-for-updating-likewise-and-registry-configuration-on-esxi/

This is an adaption of that script, advanced and simplified for restarting vSAN Services, which might be more applicable and useful for reuse in the future without the complexity of escape characters and spaces within the Likewise registry

This script is designed to run on a Windows Server 2012 R2 server using Powershell 4.0 along with plink (a command-line interface to the PuTTY back ends) which can be downloaded from https://www.putty.org

If you run the script it will produce a csv file called hosts.csv in the folder c:\temp to be used as a template. Running the script again with the csv file completed will proceed to run the script connecting to each host listed in the csv file and then output a csv file called export.csv in the c:\temp\ folder with the results.

$hostList = "c:\temp\Hosts.csv"


if (-NOT (Test-Path $hostList)) {


	[pscustomobject]@{ Hostname =  'Host1'; Password = 'Password1' } | Export-Csv -Path  $hostList -Append -NoTypeInformation

	" "
	"----------------------------------------------"
	"-------- Host file CSV does not exist --------"
	"-- Creating empty file in" + $hostList + " --"
	"---- Please complete and run script again ----"
	"----------------------------------------------"
	" "		



	exit
}

$csv = Import-Csv $hostList


$table=@()


foreach($item in $csv)
	{
		" "
		"-------------------------------------------"
		"-- Hostname = "+$($item.Hostname)+" --"
		"-------------------------------------------"
		" "		
		
		$plink = '"C:\Program Files\PuTTY\plink.exe" -v -batch -pw'
        $plinkCachekey = 'echo y | "C:\Program Files\PuTTY\plink.exe" -pw'
		$esxUser = ' root@'
        $exitCmd = ' exit'
		$serviceCmd = ' /etc/init.d/vsanmgtd status'
		$serviceCmd2 = ' /etc/init.d/vsanmgtd restart'
		$serviceCmd3 = ' /etc/init.d/vsanvpd status'
		$serviceCmd4 = ' /etc/init.d/vsanvpd restart'
		
		$grep = '| grep '
		$quote = '"'

        $plinkCacheKeyCmd = $plinkCachekey + " " + $($item.Password) + $esxUser + $($item.Hostname) + $exitCmd


		$serviceRestartCmd = $plink + " " + $($item.Password) + $esxUser + $($item.Hostname) + $serviceCmd
		$serviceRestartCmd2 = $plink + " " + $($item.Password) + $esxUser + $($item.Hostname) + $serviceCmd2
		$serviceRestartCmd3 = $plink + " " + $($item.Password) + $esxUser + $($item.Hostname) + $serviceCmd3
		$serviceRestartCmd4 = $plink + " " + $($item.Password) + $esxUser + $($item.Hostname) + $serviceCmd4


		" "
		"----------------------------------"
		"-------- Cacheing SSH Key --------"
		"----------------------------------"
		" "
        
        Invoke-Expression -command 'cmd.exe /c $plinkCacheKeyCmd'
        

			" "
			"----------------------------------"
			"-------- Status vsanmgtd Service -------"
			"----------------------------------"
			" "

			$StatusResult = Invoke-Expression -command 'cmd.exe /c $serviceRestartCmd'
			$StatusResult1 = $StatusResult
			
			" "
			"----------------------------------"
			"------- Restarting vsanmgtd Service -------"
			"----------------------------------"
			" "

			Invoke-Expression -command 'cmd.exe /c $serviceRestartCmd2'
			$table += $row
		
					" "
			"----------------------------------"
			"-------- Status vsanvpd Service -------"
			"----------------------------------"
			" "

			$StatusResult = Invoke-Expression -command 'cmd.exe /c $serviceRestartCmd3'
			$StatusResult2 = $StatusResult
			
			" "
			"----------------------------------"
			"------- Restarting vsanvpd Service -------"
			"----------------------------------"
			" "

			Invoke-Expression -command 'cmd.exe /c $serviceRestartCmd4'
			$row = new-object PSObject -Property @{
				Hostname = $($item.Hostname);
				StatusResult1 = $StatusResult1;
				State1 = 'Complete'
				StatusResult2 = $StatusResult2;
				State2 = 'Complete'
				}
			$table += $row
		
		" "
		"--------------------------------------------------------"
		"-- Finished with Hostname = "+$($item.Hostname)+" --"
		"--------------------------------------------------------"
		" "	


	}

$table | Select-Object Hostname,StatusResult1,StatusResult2,State1,State2 | Export-Csv -Path C:\temp\export.csv -NoTypeInformation 

Script for Updating Likewise and registry configuration on ESXi

On my latest project we had an issue with ESXi hosts joined to a disjointed namespace domain which resulted in a large amount of logfiles which quickly overwhelmed the host’s local log buffer and quickly reduced our Log Insight cluster’s planned retention period. The workaround was to make a change to the Registry in Likewise (setting DomainManagerIgnoreAllTrusts to “1”) on each host, which necessitated logging into each host and running some commands.

With the sheer number of hosts we were faced with, each with their own unique complex root password, and limited change windows, a script to automate this was called for. Especially if it can be quickly altered for reuse with other commands.

I apologise in advance to all my Powershell aficionado colleagues for the quality, but needs must.

This script is designed to run on a Windows Server 2012 R2 server using just Powershell 4.0 (no PowerCLI) along with plink (a command-line interface to the PuTTY back ends) which can be downloaded from https://www.putty.org

If you run the script it will produce a csv file called hosts.csv in the folder c:\temp to be used as a template. Running the script again with the csv file completed will proceed to run the script connecting to each host listed in the csv file and then output a csv file called export.csv in the c:\temp\ folder with the results.

$hostList = "c:\temp\Hosts.csv"


if (-NOT (Test-Path $hostList)) {

	" "
	"----------------------------------------------"
	"-------- Host file CSV does not exist --------"
	"-- Creating empty file in" + $hostList --"
	"---- Please complete and run script again ----"
	"----------------------------------------------"
	" "		


	[pscustomobject]@{ Hostname =  'Host1'; Password = 'Password1' } | Export-Csv -Path  $hostList -Append -NoTypeInformation
	exit
}

$csv = Import-Csv $hostList


$table=@()


foreach($item in $csv)
	{
		" "
		"-------------------------------------------"
		"-- Hostname = "+$($item.Hostname)+" --"
		"-------------------------------------------"
		" "		
		
		$plink = '"C:\Program Files\PuTTY\plink.exe" -v -batch -pw'
        $plinkCachekey = 'echo y | "C:\Program Files\PuTTY\plink.exe" -pw'
		$esxUser = ' root@'
        $exitCmd = ' exit'
		$remoteCmd1 = ' .//usr/lib/vmware/likewise/bin/lwregshell set_value ''[HKEY_THIS_MACHINE\Services\lsass\Parameters\Providers\ActiveDirectory]'''
		$remoteCmd2 = ' "DomainManagerIgnoreAllTrusts"'
		$remoteCmdTest = ' ".//usr/lib/vmware/likewise/bin/lwregshell ls ''[HKEY_THIS_MACHINE\Services\lsass\Parameters\Providers\ActiveDirectory]'''
		$serviceCmd = ' /etc/init.d/lwsmd reload'
		$serviceCmd2 = ' /etc/init.d/lwsmd restart'
		$serviceCmd3 = ' /etc/init.d/lwsmd start'
		$grep = '| grep '
		$quote = '"'

        $plinkCacheKeyCmd = $plinkCachekey + " " + $($item.Password) + $esxUser + $($item.Hostname) + $exitCmd

		$finalCmd = $plink + " " + $($item.Password) + $esxUser + $($item.Hostname) + $remoteCmd1 + $remoteCmd2 + " 1"
		$finalCmdTest = $plink + " " + $($item.Password) + $esxUser + $($item.Hostname) + $remoteCmdTest + $grep + $remoteCmd2 + $quote

		$serviceRestartCmd = $plink + " " + $($item.Password) + $esxUser + $($item.Hostname) + $serviceCmd
		$serviceRestartCmd2 = $plink + " " + $($item.Password) + $esxUser + $($item.Hostname) + $serviceCmd2
		$serviceRestartCmd3 = $plink + " " + $($item.Password) + $esxUser + $($item.Hostname) + $serviceCmd3

		" "
		"----------------------------------"
		"-------- Cacheing SSH Key --------"
		"----------------------------------"
		" "
        
        Invoke-Expression -command 'cmd.exe /c $plinkCacheKeyCmd'
        
		" "
		"----------------------------------"
		"---- Checking Current Setting ----"
		"----------------------------------"
		" "
        

		$regKeyCheck = Invoke-Expression -command 'cmd.exe /c $finalCmdTest'
        
        "keyreg = " + $regKeyCheck 
		
        If($regKeyCheck -ne $null){

            $regKeyCheck = $regKeyCheck.Substring($regKeyCheck.get_Length()-2)
		    $regKeyCheck = $regKeyCheck.Substring(0,1)
			$likewiseEnabledAtStart = 'Yes'
        }
		Else{
			$regKeyCheck = '0'
			Invoke-Expression -command 'cmd.exe /c $serviceRestartCmd3'
			$likewiseEnabledAtStart = 'No'

		}

		If($regKeyCheck -eq '0') {
		
			" "
			"----------------------------------"
			"--------- Applying Change --------"
			"----------------------------------"
			" "

			Invoke-Expression -command 'cmd.exe /c $finalCmd'		

			" "
			"----------------------------------"
			"------------ Checking ------------"
			"----------------------------------"
			" "
            
			$regKey = Invoke-Expression -command 'cmd.exe /c $finalCmdTest'

			$regKey = $regKey.Substring($regKey.get_Length()-2)
			$regKey = $regKey.Substring(0,1)



			" "
			"----------------------------------"
			"-------- Reloading Service -------"
			"----------------------------------"
			" "

			$reloadResult = Invoke-Expression -command 'cmd.exe /c $serviceRestartCmd'
			$reloadResult = $reloadResult.Substring($reloadResult.get_Length()-2)
			
			" "
			"----------------------------------"
			"------- Restarting Service -------"
			"----------------------------------"
			" "

			Invoke-Expression -command 'cmd.exe /c $serviceRestartCmd2'
			$row = new-object PSObject -Property @{
				Hostname = $($item.Hostname);
				KeyValue = $regKey;
				ReloadResult = $reloadResult;
				likewiseEnabledAtStart = $likewiseEnabledAtStart;
				State = 'Complete'
				}
			$table += $row
			
		}
		Else{
			$row = new-object PSObject -Property @{
				Hostname = $($item.Hostname);
				KeyValue = $regKeyCheck;
				ReloadResult = 'NA';
				likewiseEnabledAtStart = $likewiseEnabledAtStart;
				State = 'Not Required'
				}
			$table += $row
		}
		
		
		" "
		"--------------------------------------------------------"
		"-- Finished with Hostname = "+$($item.Hostname)+" --"
		"--------------------------------------------------------"
		" "	


	}

$table | Select-Object Hostname,KeyValue,ReloadResult,likewiseEnabledAtStart,State | Export-Csv -Path C:\temp\export.csv -NoTypeInformation