The Windows Admin Center 1809 PowerShell Scripts

“WAC can be seen as a collection of customizable PowerShell scripts, which are used remotely, and managed via a web-based GUI… Thus WAC is agentless… and yes, those scripts can be independently used. How cool is that!?”

We think it is cool, and we are going to be proactive and act on it. Here is the list of the PowerShell scripts available with Windows Admin Center 1809, and thereafter the scripts themselves.

Downloading Windows Admin Center

The newest version of WAC can be downloaded from Microsoft here. Direct download link for version 1809 is available here.

Referral to GitHub

Paul DiMaggio has kindly collected the scripts of WAC and posted them in a more suitable platform, here in GitHub. However, his post is from July/26/2018, prior to releasing the current version of WAC, which is 1809.

The PowerShell Scripts

  • Find-WindowsUpdateList
  • Get-AutomaticUpdatesOptions
  • Get-WindowsUpdateInstallerStatus
  • Install-WindowsUpdates
  • Set-AutomaticUpdatesOptions
  • Get-CimWin32ComputerSystem
  • Get-CimWin32NetworkAdapter
  • Get-CimWin32OperatingSystem
  • Get-CimWin32PhysicalMemory
  • Get-CimWin32Processor
  • Get-ClusterInventory
  • Get-ClusterNodes
  • Get-ServerInventory

Find-WindowsUpdateList

function Find-WindowsUpdateList {
<#

.SYNOPSIS
Create a sheduled task to run powershell script that find available or installed windows updates through COM object.

.DESCRIPTION
Create a sheduled task to run powershell script that find available or installed windows updates through COM object.

.EXAMPLE
Find available windows update.
PS C:\> Find-WindowsUpdateList “IsInstalled = 0”

.EXAMPLE
Find installed windows update.
PS C:\> Find-WindowsUpdateList “IsInstalled = 1”

.ROLE
Readers

#>

Param(
[Parameter(Mandatory = $true)]
[string]$searchCriteria,
[Parameter(Mandatory = $true)]
[string]$sessionId,
[Parameter(Mandatory = $true)]
[int16]$serverSelection
)

#PowerShell script to run. In some cases, you may need use back quote (`) to treat some character (eg. double/single quate, specail escape sequence) literally.
$Script = @’
function GenerateSearchHash($searchResults) {
foreach ($searchResult in $searchResults){
foreach ($KBArticleID in $searchResult.KBArticleIDs) {
$KBID = ‘KB’ + $KBArticleID
if ($KBArticleID -ne $null -and -Not $searchHash.ContainsKey($KBID)) {
$searchHash.Add($KBID, ($searchResult | Select msrcSeverity, title, IsMandatory))
}
}
}
}

function GenerateHistoryHash($historyResults) {
foreach ($historyResult in $historyResults){
$KBID = ([regex]::match($historyResult.Title,’KB(\d+)’)).Value.ToUpper()
if ($KBID -ne $null -and $KBID -ne ‘’) {
$title = $historyResult.Title.Trim()

if (-Not $historyHash.ContainsKey($KBID)) {
$historyHash.Add($KBID, ($historyResult | Select ResultCode, Date, Title))
} elseif (($historyHash[$KBID].Title -eq $null -or $historyHash[$KBID].Title -eq ‘’) -and ($title -ne $null -or $title.Length -gt 0)) {
#If the previous entry did not have a title and this item has one, update it
$historyHash[$KBID] = $historyResult | Select ResultCode, Date, $title
}
}
}
}

$objSession = New-Object -ComObject “Microsoft.Update.Session”
$objSearcher = $objSession.CreateUpdateSearcher()
$objSearcher.ServerSelection = $serverSelection
$objResults = $objSearcher.Search($searchCriteria)

$result = New-Object Collections.ArrayList

if ($searchCriteria -eq “IsInstalled=1”) {
$searchHash = @{}
GenerateSearchHash($objResults.Updates)

$historyCount = $objSearcher.GetTotalHistoryCount()
$historyResults = $objSearcher.QueryHistory(0, $historyCount)

$historyHash = @{}
GenerateHistoryHash($historyResults)

$installedItems = Get-Hotfix
foreach ($installedItem in $installedItems) {
$resultItem = $installedItem | Select HotFixID, InstalledBy
$title = $installedItem.Description + ‘ (‘ + $resultItem.HotFixID + ‘)’
$installDate = $installedItem.InstalledOn

$titleMatch = $null

$searchMatch = $searchHash.Item($installedItem.HotFixID)
if ($searchMatch -ne $null) {
$titleMatch = $searchMatch.title
$resultItem | Add-Member -MemberType NoteProperty -Name “msrcSeverity” -Value $searchMatch.msrcSeverity
$resultItem | Add-Member -MemberType NoteProperty -Name “IsMandatory” -Value $searchMatch.IsMandatory
}

$historyMatch = $historyHash.Item($installedItem.HotFixID)
if ($historyMatch -ne $null) {
$resultItem | Add-Member -MemberType NoteProperty -Name “installState” -Value $historyMatch.ResultCode
if ($titleMatch -eq $null -or $titleMatch -eq ‘’) {
# If there was no matching title in searchMatch
$titleMatch = $historyMatch.title
}

$installDate = $historyMatch.Date
}

if ($titleMatch -ne $null -or $titleMatch.Trim() -ne ‘’) {
$title = $titleMatch
}

$resultItem | Add-Member -MemberType NoteProperty -Name “title” -Value $title
$resultItem | Add-Member -MemberType NoteProperty -Name “installDate” -Value $installDate

$result.Add($resultItem)
}
} else {
foreach ($objResult in $objResults.Updates) {
$resultItem = $objResult | Select msrcSeverity, title, IsMandatory
$result.Add($resultItem)
}
}

if(Test-Path $ResultFile)
{
Remove-Item $ResultFile
}

$result | ConvertTo-Json -depth 10 | Out-File $ResultFile
‘@

#Pass parameters to script and generate script file in localappdata folder
$timeStamp = Get-Date -Format FileDateTimeUniversal
# use both ps sessionId and timestamp for file/task prefix so that multiple instances won’t delete others’ files and tasks
$fileprefix = “_PS”+ $sessionId + “_Time” + $timeStamp
$ResultFile = $env:TEMP + “\Find-Updates-result” + $fileprefix + “.json”
$Script = ‘$searchCriteria = ‘ + “‘$searchCriteria’;” + ‘$ResultFile = ‘ + “‘$ResultFile’;” + ‘$serverSelection =’ + “‘$serverSelection’;” + $Script
$ScriptFile = $env:TEMP + “\Find-Updates” + $fileprefix + “.ps1”
$Script | Out-File $ScriptFile
if (-Not(Test-Path $ScriptFile)) {
$message = “Failed to create file:” + $ScriptFile
Write-Error $message
return #If failed to create script file, no need continue just return here
}

#Create a scheduled task
$TaskName = “SMEWindowsUpdateFindUpdates” + $fileprefix

$User = [Security.Principal.WindowsIdentity]::GetCurrent()
$Role = (New-Object Security.Principal.WindowsPrincipal $User).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
$arg = “-NoProfile -NoLogo -NonInteractive -ExecutionPolicy Bypass -File $ScriptFile”
if(!$Role)
{
Write-Warning “To perform some operations you must run an elevated Windows PowerShell console.”
}

$Scheduler = New-Object -ComObject Schedule.Service

#Try to connect to schedule service 3 time since it may fail the first time
for ($i=1; $i -le 3; $i++)
{
Try
{
$Scheduler.Connect()
Break
}
Catch
{
if($i -ge 3)
{
Write-EventLog -LogName Application -Source “SME Windows Updates Find Updates” -EntryType Error -EventID 1 -Message “Can’t connect to Schedule service”
Write-Error “Can’t connect to Schedule service” -ErrorAction Stop
}
else
{
Start-Sleep -s 1
}
}
}

$RootFolder = $Scheduler.GetFolder(“\”)
#Delete existing task
if($RootFolder.GetTasks(0) | Where-Object {$_.Name -eq $TaskName})
{
Write-Debug(“Deleting existing task” + $TaskName)
$RootFolder.DeleteTask($TaskName,0)
}

$Task = $Scheduler.NewTask(0)
$RegistrationInfo = $Task.RegistrationInfo
$RegistrationInfo.Description = $TaskName
$RegistrationInfo.Author = $User.Name

$Triggers = $Task.Triggers
$Trigger = $Triggers.Create(7) #TASK_TRIGGER_REGISTRATION: Starts the task when the task is registered.
$Trigger.Enabled = $true

$Settings = $Task.Settings
$Settings.Enabled = $True
$Settings.StartWhenAvailable = $True
$Settings.Hidden = $False

$Action = $Task.Actions.Create(0)
$Action.Path = “powershell”
$Action.Arguments = $arg

#Tasks will be run with the highest privileges
$Task.Principal.RunLevel = 1

#Start the task to run in Local System account. 6: TASK_CREATE_OR_UPDATE
$RootFolder.RegisterTaskDefinition($TaskName, $Task, 6, “SYSTEM”, $Null, 1) | Out-Null
#Wait for running task finished
$RootFolder.GetTask($TaskName).Run(0) | Out-Null
while($Scheduler.GetRunningTasks(0) | Where-Object {$_.Name -eq $TaskName})
{
Start-Sleep -s 1
}

#Clean up
$RootFolder.DeleteTask($TaskName,0)
Remove-Item $ScriptFile
#Return result
if(Test-Path $ResultFile)
{
$result = Get-Content -Raw -Path $ResultFile | ConvertFrom-Json
Remove-Item $ResultFile
return $result
}

}

Get-AutomaticUpdatesOptions

function Get-AutomaticUpdatesOptions {
<#

.SYNOPSIS
Script that get windows update automatic update options from registry key.

.DESCRIPTION
Script that get windows update automatic update options from registry key.

.ROLE
Readers

#>

Import-Module Microsoft.PowerShell.Management

# If there is AUOptions, return it, otherwise return NoAutoUpdate value
$option = (Get-ItemProperty -Path “HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU” -Name “AUOptions” -ErrorVariable myerror -ErrorAction SilentlyContinue).AUOptions
if ($myerror) {
$option = (Get-ItemProperty -Path “HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU” -Name “NoAutoUpdate” -ErrorVariable myerror -ErrorAction SilentlyContinue).NoAutoUpdate
if ($myerror) {
$option = 0 # not defined
}
}
return $option

}

Get-WindowsUpdateInstallerStatus

function Get-WindowsUpdateInstallerStatus {
<#

.SYNOPSIS
Script that check scheduled task for install updates is still running or not.

.DESCRIPTION
Script that check scheduled task for install updates is still running or not. Notcied that using the following COM object has issue: when install-WUUpdates task is running, the busy status return false;
but right after the task finished, it returns true.

.ROLE
Readers

#>

Import-Module ScheduledTasks

$TaskName = “SMEWindowsUpdateInstallUpdates”
$ScheduledTask = Get-ScheduledTask | Microsoft.PowerShell.Utility\Select-Object TaskName, State | Where-Object {$_.TaskName -eq $TaskName}
if ($ScheduledTask -ne $Null -and $ScheduledTask.State -eq 4) { # Running
return $True
} else {
return $False
}

}

Install-WindowsUpdates

function Install-WindowsUpdates {
<#

.SYNOPSIS
Create a scheduled task to run a powershell script file to installs all available windows updates through ComObject, restart the machine if needed.

.DESCRIPTION
Create a scheduled task to run a powershell script file to installs all available windows updates through ComObject, restart the machine if needed.
This is a workaround since CreateUpdateDownloader() and CreateUpdateInstaller() methods can’t be called from a remote computer — E_ACCESSDENIED.
More details see https://msdn.microsoft.com/en-us/library/windows/desktop/aa387288(v=vs.85).aspx

.Parameters
$RestartTime: The user-defined time to restart after update (Optional).

.ROLE
Administrators

#>

param (
[Parameter(Mandatory = $false)]
[String]
$RestartTime,
[Parameter(Mandatory = $true)]
[int16]$serverSelection)

$Script = @’
$objServiceManager = New-Object -ComObject ‘Microsoft.Update.ServiceManager’;
$objSession = New-Object -ComObject ‘Microsoft.Update.Session’;
$objSearcher = $objSession.CreateUpdateSearcher();
$objSearcher.ServerSelection = $serverSelection;
$serviceName = ‘Windows Update’;
$search = ‘IsInstalled = 0’;
$objResults = $objSearcher.Search($search);
$Updates = $objResults.Updates;
$FoundUpdatesToDownload = $Updates.Count;

$NumberOfUpdate = 1;
$objCollectionDownload = New-Object -ComObject ‘Microsoft.Update.UpdateColl’;
$updateCount = $Updates.Count;
Foreach($Update in $Updates)
{
Write-Progress -Activity ‘Downloading updates’ -Status `”[$NumberOfUpdate/$updateCount]` $($Update.Title)`” -PercentComplete ([int]($NumberOfUpdate/$updateCount * 100));
$NumberOfUpdate++;
Write-Debug `”Show` update` to` download:` $($Update.Title)`” ;
Write-Debug ‘Accept Eula’;
$Update.AcceptEula();
Write-Debug ‘Send update to download collection’;
$objCollectionTmp = New-Object -ComObject ‘Microsoft.Update.UpdateColl’;
$objCollectionTmp.Add($Update) | Out-Null;

$Downloader = $objSession.CreateUpdateDownloader();
$Downloader.Updates = $objCollectionTmp;
Try
{
Write-Debug ‘Try download update’;
$DownloadResult = $Downloader.Download();
} <#End Try#>
Catch
{
If($_ -match ‘HRESULT: 0x80240044’)
{
Write-Warning ‘Your security policy do not allow a non-administator identity to perform this task’;
} <#End If $_ -match ‘HRESULT: 0x80240044’#>

Return
} <#End Catch#>

Write-Debug ‘Check ResultCode’;
Switch -exact ($DownloadResult.ResultCode)
{
0 { $Status = ‘NotStarted’; }
1 { $Status = ‘InProgress’; }
2 { $Status = ‘Downloaded’; }
3 { $Status = ‘DownloadedWithErrors’; }
4 { $Status = ‘Failed’; }
5 { $Status = ‘Aborted’; }
} <#End Switch#>

If($DownloadResult.ResultCode -eq 2)
{
Write-Debug ‘Downloaded then send update to next stage’;
$objCollectionDownload.Add($Update) | Out-Null;
} <#End If $DownloadResult.ResultCode -eq 2#>
}

$ReadyUpdatesToInstall = $objCollectionDownload.count;
Write-Verbose `”Downloaded` [$ReadyUpdatesToInstall]` Updates` to` Install`” ;
If($ReadyUpdatesToInstall -eq 0)
{
Return;
} <#End If $ReadyUpdatesToInstall -eq 0#>

$NeedsReboot = $false;
$NumberOfUpdate = 1;

<#install updates#>
Foreach($Update in $objCollectionDownload)
{
Write-Progress -Activity ‘Installing updates’ -Status `”[$NumberOfUpdate/$ReadyUpdatesToInstall]` $($Update.Title)`” -PercentComplete ([int]($NumberOfUpdate/$ReadyUpdatesToInstall * 100));
Write-Debug ‘Show update to install: $($Update.Title)’;

Write-Debug ‘Send update to install collection’;
$objCollectionTmp = New-Object -ComObject ‘Microsoft.Update.UpdateColl’;
$objCollectionTmp.Add($Update) | Out-Null;

$objInstaller = $objSession.CreateUpdateInstaller();
$objInstaller.Updates = $objCollectionTmp;

Try
{
Write-Debug ‘Try install update’;
$InstallResult = $objInstaller.Install();
} <#End Try#>
Catch
{
If($_ -match ‘HRESULT: 0x80240044’)
{
Write-Warning ‘Your security policy do not allow a non-administator identity to perform this task’;
} <#End If $_ -match ‘HRESULT: 0x80240044’#>

Return;
} #End Catch

If(!$NeedsReboot)
{
Write-Debug ‘Set instalation status RebootRequired’;
$NeedsReboot = $installResult.RebootRequired;
} <#End If !$NeedsReboot#>
$NumberOfUpdate++;
} <#End Foreach $Update in $objCollectionDownload#>

if($NeedsReboot){
<#Restart immediately#>
$waitTime = 0
if($RestartTime) {
<#Restart at given time#>
$waitTime = [decimal]::round(((Get-Date $RestartTime) — (Get-Date)).TotalSeconds);
if ($waitTime -lt 0 ) {
$waitTime = 0
}
Shutdown -r -t $waitTime -c “SME installing Windows updates”;
}
}
‘@

#Pass parameters to script and generate script file in localappdata folder
if ($RestartTime){
$Script = ‘$RestartTime = ‘ + “‘$RestartTime’;” + $Script
}
$Script = ‘$serverSelection =’ + “‘$serverSelection’;” + $Script

$ScriptFile = $env:LocalAppData + “\Install-Updates.ps1”
$Script | Out-File $ScriptFile
if (-Not(Test-Path $ScriptFile)) {
$message = “Failed to create file:” + $ScriptFile
Write-Error $message
return #If failed to create script file, no need continue just return here
}

#Create a scheduled task
$TaskName = “SMEWindowsUpdateInstallUpdates”

$User = [Security.Principal.WindowsIdentity]::GetCurrent()
$Role = (New-Object Security.Principal.WindowsPrincipal $User).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
$arg = “-NoProfile -NoLogo -NonInteractive -ExecutionPolicy Bypass -File $ScriptFile”
if(!$Role)
{
Write-Warning “To perform some operations you must run an elevated Windows PowerShell console.”
}

$Scheduler = New-Object -ComObject Schedule.Service

#Try to connect to schedule service 3 time since it may fail the first time
for ($i=1; $i -le 3; $i++)
{
Try
{
$Scheduler.Connect()
Break
}
Catch
{
if($i -ge 3)
{
Write-EventLog -LogName Application -Source “SME Windows Updates Install Updates” -EntryType Error -EventID 1 -Message “Can’t connect to Schedule service”
Write-Error “Can’t connect to Schedule service” -ErrorAction Stop
}
else
{
Start-Sleep -s 1
}
}
}

$RootFolder = $Scheduler.GetFolder(“\”)
#Delete existing task
if($RootFolder.GetTasks(0) | Where-Object {$_.Name -eq $TaskName})
{
Write-Debug(“Deleting existing task” + $TaskName)
$RootFolder.DeleteTask($TaskName,0)
}

$Task = $Scheduler.NewTask(0)
$RegistrationInfo = $Task.RegistrationInfo
$RegistrationInfo.Description = $TaskName
$RegistrationInfo.Author = $User.Name

$Triggers = $Task.Triggers
$Trigger = $Triggers.Create(7) #TASK_TRIGGER_REGISTRATION: Starts the task when the task is registered.
$Trigger.Enabled = $true

$Settings = $Task.Settings
$Settings.Enabled = $True
$Settings.StartWhenAvailable = $True
$Settings.Hidden = $False

$Action = $Task.Actions.Create(0)
$Action.Path = “powershell”
$Action.Arguments = $arg

#Tasks will be run with the highest privileges
$Task.Principal.RunLevel = 1

#Start the task to run in Local System account. 6: TASK_CREATE_OR_UPDATE
$RootFolder.RegisterTaskDefinition($TaskName, $Task, 6, “SYSTEM”, $Null, 1) | Out-Null
#Wait for running task finished
$RootFolder.GetTask($TaskName).Run(0) | Out-Null
while($Scheduler.GetRunningTasks(0) | Where-Object {$_.Name -eq $TaskName})
{
Start-Sleep -s 1
}

#Clean up
$RootFolder.DeleteTask($TaskName,0)
Remove-Item $ScriptFile

}

Set-AutomaticUpdatesOptions

function Set-AutomaticUpdatesOptions {
<#

.SYNOPSIS
Script that set windows update automatic update options in registry key.

.DESCRIPTION
Script that set windows update automatic update options in registry key.

.EXAMPLE
Set AUoptions
PS C:\> Set-AUoptions “2”

.ROLE
Administrators

#>

Param(
[Parameter(Mandatory = $true)]
[string]$AUOptions
)

$Path = “HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU”
switch($AUOptions)
{
‘0’ # Not defined, delete registry folder if exist
{
if (Test-Path $Path) {
Remove-Item $Path
}
}
‘1’ # Disabled, set NoAutoUpdate to 1 and delete AUOptions if existed
{
if (Test-Path $Path) {
Set-ItemProperty -Path $Path -Name NoAutoUpdate -Value 0x1 -Force
Remove-ItemProperty -Path $Path -Name AUOptions
}
else {
New-Item $Path -Force
New-ItemProperty -Path $Path -Name NoAutoUpdate -Value 0x1 -Force
}
}
default # else 2–5, set AUoptions
{
if (!(Test-Path $Path)) {
New-Item $Path -Force
}
Set-ItemProperty -Path $Path -Name AUOptions -Value $AUOptions -Force
Set-ItemProperty -Path $Path -Name NoAutoUpdate -Value 0x0 -Force
}
}

}

Get-CimWin32ComputerSystem

function Get-CimWin32ComputerSystem {
<#

.SYNOPSIS
Gets Win32_ComputerSystem object.

.DESCRIPTION
Gets Win32_ComputerSystem object.

.ROLE
Readers

#>

import-module CimCmdlets

Get-CimInstance -Namespace root/cimv2 -ClassName Win32_ComputerSystem

}

Get-CimWin32LogicalDisk

function Get-CimWin32LogicalDisk {
<#

.SYNOPSIS
Gets Win32_LogicalDisk object.

.DESCRIPTION
Gets Win32_LogicalDisk object.

.ROLE
Readers

#>

import-module CimCmdlets

Get-CimInstance -Namespace root/cimv2 -ClassName Win32_LogicalDisk

}

Get-CimWin32NetworkAdapter

function Get-CimWin32NetworkAdapter {
<#

.SYNOPSIS
Gets Win32_NetworkAdapter object.

.DESCRIPTION
Gets Win32_NetworkAdapter object.

.ROLE
Readers

#>

import-module CimCmdlets

Get-CimInstance -Namespace root/cimv2 -ClassName Win32_NetworkAdapter

}

Get-CimWin32OperatingSystem

function Get-CimWin32OperatingSystem {
<#

.SYNOPSIS
Gets Win32_OperatingSystem object.

.DESCRIPTION
Gets Win32_OperatingSystem object.

.ROLE
Readers

#>

import-module CimCmdlets

Get-CimInstance -Namespace root/cimv2 -ClassName Win32_OperatingSystem

}

Get-CimWin32PhysicalMemory

function Get-CimWin32PhysicalMemory {
<#

.SYNOPSIS
Gets Win32_PhysicalMemory object.

.DESCRIPTION
Gets Win32_PhysicalMemory object.

.ROLE
Readers

#>

import-module CimCmdlets

Get-CimInstance -Namespace root/cimv2 -ClassName Win32_PhysicalMemory

}

Get-CimWin32Processor

function Get-CimWin32Processor {
<#

.SYNOPSIS
Gets Win32_Processor object.

.DESCRIPTION
Gets Win32_Processor object.

.ROLE
Readers

#>

import-module CimCmdlets

Get-CimInstance -Namespace root/cimv2 -ClassName Win32_Processor

}

Get-ClusterInventory

function Get-ClusterInventory {
<#

.SYNOPSIS
Retrieves the inventory data for a cluster.

.DESCRIPTION
Retrieves the inventory data for a cluster.

.ROLE
Readers

#>

import-module CimCmdlets -ErrorAction SilentlyContinue

# JEA code requires to pre-import the module (this is slow on failover cluster environment.)
import-module FailoverClusters -ErrorAction SilentlyContinue

<#

.SYNOPSIS
Get the name of this computer.

.DESCRIPTION
Get the best available name for this computer. The FQDN is preferred, but when not avaialble
the NetBIOS name will be used instead.

#>

function getComputerName() {
$computerSystem = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue | Microsoft.PowerShell.Utility\Select-Object Name, DNSHostName

if ($computerSystem) {
$computerName = $computerSystem.DNSHostName

if ($null -eq $computerName) {
$computerName = $computerSystem.Name
}

return $computerName
}

return $null
}

<#

.SYNOPSIS
Are the cluster PowerShell cmdlets installed on this server?

.DESCRIPTION
Are the cluster PowerShell cmdlets installed on this server?

#>

function getIsClusterCmdletAvailable() {
$cmdlet = Get-Command “Get-Cluster” -ErrorAction SilentlyContinue

return !!$cmdlet
}

<#

.SYNOPSIS
Get the MSCluster Cluster CIM instance from this server.

.DESCRIPTION
Get the MSCluster Cluster CIM instance from this server.

#>
function getClusterCimInstance() {
$namespace = Get-CimInstance -Namespace root/MSCluster -ClassName __NAMESPACE -ErrorAction SilentlyContinue

if ($namespace) {
return Get-CimInstance -Namespace root/mscluster MSCluster_Cluster -ErrorAction SilentlyContinue | Microsoft.PowerShell.Utility\Select-Object fqdn, S2DEnabled
}

return $null
}

<#

.SYNOPSIS
Determines if the current cluster supports Failover Clusters Time Series Database.

.DESCRIPTION
Use the existance of the path value of cmdlet Get-StorageHealthSetting to determine if TSDB
is supported or not.

#>
function getClusterPerformanceHistoryPath() {
return $null -ne (Get-StorageSubSystem clus* | Get-StorageHealthSetting -Name “System.PerformanceHistory.Path”)
}

<#

.SYNOPSIS
Get some basic information about the cluster from the cluster.

.DESCRIPTION
Get the needed cluster properties from the cluster.

#>
function getClusterInfo() {
$returnValues = @{}

$returnValues.Fqdn = $null
$returnValues.isS2DEnabled = $false
$returnValues.isTsdbEnabled = $false

$cluster = getClusterCimInstance
if ($cluster) {
$returnValues.Fqdn = $cluster.fqdn
$isS2dEnabled = !!(Get-Member -InputObject $cluster -Name “S2DEnabled”) -and ($cluster.S2DEnabled -eq 1)
$returnValues.isS2DEnabled = $isS2dEnabled

if ($isS2DEnabled) {
$returnValues.isTsdbEnabled = getClusterPerformanceHistoryPath
} else {
$returnValues.isTsdbEnabled = $false
}
}

return $returnValues
}

<#

.SYNOPSIS
Are the cluster PowerShell Health cmdlets installed on this server?

.DESCRIPTION
Are the cluster PowerShell Health cmdlets installed on this server?

s#>
function getisClusterHealthCmdletAvailable() {
$cmdlet = Get-Command -Name “Get-HealthFault” -ErrorAction SilentlyContinue

return !!$cmdlet
}
<#

.SYNOPSIS
Are the Britannica (sddc management resources) available on the cluster?

.DESCRIPTION
Are the Britannica (sddc management resources) available on the cluster?

#>
function getIsBritannicaEnabled() {
return $null -ne (Get-CimInstance -Namespace root/sddc/management -ClassName SDDC_Cluster -ErrorAction SilentlyContinue)
}

<#

.SYNOPSIS
Are the Britannica (sddc management resources) virtual machine available on the cluster?

.DESCRIPTION
Are the Britannica (sddc management resources) virtual machine available on the cluster?

#>
function getIsBritannicaVirtualMachineEnabled() {
return $null -ne (Get-CimInstance -Namespace root/sddc/management -ClassName SDDC_VirtualMachine -ErrorAction SilentlyContinue)
}

<#

.SYNOPSIS
Are the Britannica (sddc management resources) virtual switch available on the cluster?

.DESCRIPTION
Are the Britannica (sddc management resources) virtual switch available on the cluster?

#>
function getIsBritannicaVirtualSwitchEnabled() {
return $null -ne (Get-CimInstance -Namespace root/sddc/management -ClassName SDDC_VirtualSwitch -ErrorAction SilentlyContinue)
}

###########################################################################
# main()
###########################################################################

$clusterInfo = getClusterInfo

$result = New-Object PSObject

$result | Add-Member -MemberType NoteProperty -Name ‘Fqdn’ -Value $clusterInfo.Fqdn
$result | Add-Member -MemberType NoteProperty -Name ‘IsS2DEnabled’ -Value $clusterInfo.isS2DEnabled
$result | Add-Member -MemberType NoteProperty -Name ‘IsTsdbEnabled’ -Value $clusterInfo.isTsdbEnabled
$result | Add-Member -MemberType NoteProperty -Name ‘IsClusterHealthCmdletAvailable’ -Value (getIsClusterHealthCmdletAvailable)
$result | Add-Member -MemberType NoteProperty -Name ‘IsBritannicaEnabled’ -Value (getIsBritannicaEnabled)
$result | Add-Member -MemberType NoteProperty -Name ‘IsBritannicaVirtualMachineEnabled’ -Value (getIsBritannicaVirtualMachineEnabled)
$result | Add-Member -MemberType NoteProperty -Name ‘IsBritannicaVirtualSwitchEnabled’ -Value (getIsBritannicaVirtualSwitchEnabled)
$result | Add-Member -MemberType NoteProperty -Name ‘IsClusterCmdletAvailable’ -Value (getIsClusterCmdletAvailable)
$result | Add-Member -MemberType NoteProperty -Name ‘CurrentClusterNode’ -Value (getComputerName)

$result

}

Get-ClusterNodes

function Get-ClusterNodes {
<#

.SYNOPSIS
Retrieves the inventory data for cluster nodes in a particular cluster.

.DESCRIPTION
Retrieves the inventory data for cluster nodes in a particular cluster.

.ROLE
Readers

#>

import-module CimCmdlets

# JEA code requires to pre-import the module (this is slow on failover cluster environment.)
import-module FailoverClusters -ErrorAction SilentlyContinue

<#

.SYNOPSIS
Are the cluster PowerShell cmdlets installed?

.DESCRIPTION
Use the Get-Command cmdlet to quickly test if the cluster PowerShell cmdlets
are installed on this server.

#>

function getClusterPowerShellSupport() {
$cmdletInfo = Get-Command ‘Get-ClusterNode’ -ErrorAction SilentlyContinue

return $cmdletInfo -and $cmdletInfo.Name -eq “Get-ClusterNode”
}

<#

.SYNOPSIS
Get the cluster nodes using the cluster CIM provider.

.DESCRIPTION
When the cluster PowerShell cmdlets are not available fallback to using
the cluster CIM provider to get the needed information.

#>

function getClusterNodeCimInstances() {
# Change the WMI property NodeDrainStatus to DrainStatus to match the PS cmdlet output.
return Get-CimInstance -Namespace root/mscluster MSCluster_Node -ErrorAction SilentlyContinue | `
Microsoft.PowerShell.Utility\Select-Object @{Name=”DrainStatus”; Expression={$_.NodeDrainStatus}}, DynamicWeight, Name, NodeWeight, FaultDomain, State
}

<#

.SYNOPSIS
Get the cluster nodes using the cluster PowerShell cmdlets.

.DESCRIPTION
When the cluster PowerShell cmdlets are available use this preferred function.

#>

function getClusterNodePsInstances() {
return Get-ClusterNode -ErrorAction SilentlyContinue | Microsoft.PowerShell.Utility\Select-Object DrainStatus, DynamicWeight, Name, NodeWeight, FaultDomain, State
}

<#

.SYNOPSIS
Use DNS services to get the FQDN of the cluster NetBIOS name.

.DESCRIPTION
Use DNS services to get the FQDN of the cluster NetBIOS name.

.Notes
It is encouraged that the caller add their approprate -ErrorAction when
calling this function.

#>

function getClusterNodeFqdn($clusterNodeName) {
return ([System.Net.Dns]::GetHostEntry($clusterNodeName)).HostName
}

<#

.SYNOPSIS
Get the cluster nodes.

.DESCRIPTION
When the cluster PowerShell cmdlets are available get the information about the cluster nodes
using PowerShell. When the cmdlets are not available use the Cluster CIM provider.

#>

function getClusterNodes() {
$isClusterCmdletAvailable = getClusterPowerShellSupport

if ($isClusterCmdletAvailable) {
$clusterNodes = getClusterNodePsInstances
} else {
$clusterNodes = getClusterNodeCimInstances
}

$clusterNodeMap = @{}

foreach ($clusterNode in $clusterNodes) {
$clusterNodeName = $clusterNode.Name.ToLower()
$clusterNodeFqdn = getClusterNodeFqdn $clusterNodeName -ErrorAction SilentlyContinue

$clusterNodeResult = New-Object PSObject

$clusterNodeResult | Add-Member -MemberType NoteProperty -Name ‘FullyQualifiedDomainName’ -Value $clusterNodeFqdn
$clusterNodeResult | Add-Member -MemberType NoteProperty -Name ‘Name’ -Value $clusterNodeName
$clusterNodeResult | Add-Member -MemberType NoteProperty -Name ‘DynamicWeight’ -Value $clusterNode.DynamicWeight
$clusterNodeResult | Add-Member -MemberType NoteProperty -Name ‘NodeWeight’ -Value $clusterNode.NodeWeight
$clusterNodeResult | Add-Member -MemberType NoteProperty -Name ‘FaultDomain’ -Value $clusterNode.FaultDomain
$clusterNodeResult | Add-Member -MemberType NoteProperty -Name ‘State’ -Value $clusterNode.State
$clusterNodeResult | Add-Member -MemberType NoteProperty -Name ‘DrainStatus’ -Value $clusterNode.DrainStatus

$clusterNodeMap.Add($clusterNodeName, $clusterNodeResult)
}

return $clusterNodeMap
}

###########################################################################
# main()
###########################################################################

getClusterNodes

}

Get-ServerInventory

function Get-ServerInventory {
<#

.SYNOPSIS
Retrieves the inventory data for a server.

.DESCRIPTION
Retrieves the inventory data for a server.

.ROLE
Readers

#>

Set-StrictMode -Version 5.0

Import-Module CimCmdlets

<#

.SYNOPSIS
Converts an arbitrary version string into just ‘Major.Minor’

.DESCRIPTION
To make OS version comparisons we only want to compare the major and
minor version. Build number and/os CSD are not interesting.

#>

function convertOsVersion([string]$osVersion) {
[Ref]$parsedVersion = $null
if (![Version]::TryParse($osVersion, $parsedVersion)) {
return $null
}

$version = [Version]$parsedVersion.Value
return New-Object Version -ArgumentList $version.Major, $version.Minor
}

<#

.SYNOPSIS
Determines if CredSSP is enabled for the current server or client.

.DESCRIPTION
Check the registry value for the CredSSP enabled state.

#>

function isCredSSPEnabled() {
Set-Variable credSSPServicePath -Option Constant -Value “WSMan:\localhost\Service\Auth\CredSSP”
Set-Variable credSSPClientPath -Option Constant -Value “WSMan:\localhost\Client\Auth\CredSSP”

$credSSPServerEnabled = $false;
$credSSPClientEnabled = $false;

$credSSPServerService = Get-Item $credSSPServicePath -ErrorAction SilentlyContinue
if ($credSSPServerService) {
$credSSPServerEnabled = [System.Convert]::ToBoolean($credSSPServerService.Value)
}

$credSSPClientService = Get-Item $credSSPClientPath -ErrorAction SilentlyContinue
if ($credSSPClientService) {
$credSSPClientEnabled = [System.Convert]::ToBoolean($credSSPClientService.Value)
}

return ($credSSPServerEnabled -or $credSSPClientEnabled)
}

<#

.SYNOPSIS
Determines if the Hyper-V role is installed for the current server or client.

.DESCRIPTION
The Hyper-V role is installed when the VMMS service is available. This is much
faster then checking Get-WindowsFeature and works on Windows Client SKUs.

#>

function isHyperVRoleInstalled() {
$vmmsService = Get-Service -Name “VMMS” -ErrorAction SilentlyContinue

return $vmmsService -and $vmmsService.Name -eq “VMMS”
}

<#

.SYNOPSIS
Determines if the Hyper-V PowerShell support module is installed for the current server or client.

.DESCRIPTION
The Hyper-V PowerShell support module is installed when the modules cmdlets are available. This is much
faster then checking Get-WindowsFeature and works on Windows Client SKUs.

#>
function isHyperVPowerShellSupportInstalled() {
# quicker way to find the module existence. it doesn’t load the module.
return !!(Get-Module -ListAvailable Hyper-V -ErrorAction SilentlyContinue)
}

<#

.SYNOPSIS
Determines if Windows Management Framework (WMF) 5.0, or higher, is installed for the current server or client.

.DESCRIPTION
Windows Admin Center requires WMF 5 so check the registey for WMF version on Windows versions that are less than
Windows Server 2016.

#>
function isWMF5Installed([string] $operatingSystemVersion) {
Set-Variable Server2016 -Option Constant -Value (New-Object Version ‘10.0’) # And Windows 10 client SKUs
Set-Variable Server2012 -Option Constant -Value (New-Object Version ‘6.2’)

$version = convertOsVersion $operatingSystemVersion
if (-not $version) {
# Since the OS version string is not properly formatted we cannot know the true installed state.
return $false
}

if ($version -ge $Server2016) {
# It’s okay to assume that 2016 and up comes with WMF 5 or higher installed
return $true
}
else {
if ($version -ge $Server2012) {
# Windows 2012/2012R2 are supported as long as WMF 5 or higher is installed
$registryKey = ‘HKLM:\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine’
$registryKeyValue = Get-ItemProperty -Path $registryKey -Name PowerShellVersion -ErrorAction SilentlyContinue

if ($registryKeyValue -and ($registryKeyValue.PowerShellVersion.Length -ne 0)) {
$installedWmfVersion = [Version]$registryKeyValue.PowerShellVersion

if ($installedWmfVersion -ge [Version]’5.0') {
return $true
}
}
}
}

return $false
}

<#

.SYNOPSIS
Determines if the current usser is a system administrator of the current server or client.

.DESCRIPTION
Determines if the current usser is a system administrator of the current server or client.

#>
function isUserAnAdministrator() {
return ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] “Administrator”)
}

<#

.SYNOPSIS
Get some basic information about the Failover Cluster that is running on this server.

.DESCRIPTION
Create a basic inventory of the Failover Cluster that may be running in this server.

#>
function getClusterInformation() {
$returnValues = @{}

$returnValues.IsS2dEnabled = $false
$returnValues.IsCluster = $false
$returnValues.ClusterFqdn = $null

$namespace = Get-CimInstance -Namespace root/MSCluster -ClassName __NAMESPACE -ErrorAction SilentlyContinue
if ($namespace) {
$cluster = Get-CimInstance -Namespace root/MSCluster -ClassName MSCluster_Cluster -ErrorAction SilentlyContinue
if ($cluster) {
$returnValues.IsCluster = $true
$returnValues.ClusterFqdn = $cluster.Fqdn
$returnValues.IsS2dEnabled = !!(Get-Member -InputObject $cluster -Name “S2DEnabled”) -and ($cluster.S2DEnabled -gt 0)
}
}

return $returnValues
}

<#

.SYNOPSIS
Get the Fully Qaulified Domain (DNS domain) Name (FQDN) of the passed in computer name.

.DESCRIPTION
Get the Fully Qaulified Domain (DNS domain) Name (FQDN) of the passed in computer name.

#>
function getComputerFqdnAndAddress($computerName) {
$hostEntry = [System.Net.Dns]::GetHostEntry($computerName)
$addressList = @()
foreach ($item in $hostEntry.AddressList) {
$address = New-Object PSObject
$address | Add-Member -MemberType NoteProperty -Name ‘IpAddress’ -Value $item.ToString()
$address | Add-Member -MemberType NoteProperty -Name ‘AddressFamily’ -Value $item.AddressFamily.ToString()
$addressList += $address
}

$result = New-Object PSObject
$result | Add-Member -MemberType NoteProperty -Name ‘Fqdn’ -Value $hostEntry.HostName
$result | Add-Member -MemberType NoteProperty -Name ‘AddressList’ -Value $addressList
return $result
}

<#

.SYNOPSIS
Get the Fully Qaulified Domain (DNS domain) Name (FQDN) of the current server or client.

.DESCRIPTION
Get the Fully Qaulified Domain (DNS domain) Name (FQDN) of the current server or client.

#>
function getHostFqdnAndAddress($computerSystem) {
$computerName = $computerSystem.DNSHostName
if (!$computerName) {
$computerName = $computerSystem.Name
}

return getComputerFqdnAndAddress $computerName
}

<#

.SYNOPSIS
Are the needed management CIM interfaces available on the current server or client.

.DESCRIPTION
Check for the presence of the required server management CIM interfaces.

#>
function getManagementToolsSupportInformation() {
$returnValues = @{}

$returnValues.ManagementToolsAvailable = $false
$returnValues.ServerManagerAvailable = $false

$namespaces = Get-CimInstance -Namespace root/microsoft/windows -ClassName __NAMESPACE -ErrorAction SilentlyContinue

if ($namespaces) {
$returnValues.ManagementToolsAvailable = !!($namespaces | Where-Object { $_.Name -ieq “ManagementTools” })
$returnValues.ServerManagerAvailable = !!($namespaces | Where-Object { $_.Name -ieq “ServerManager” })
}

return $returnValues
}

<#

.SYNOPSIS
Check the remote app enabled or not.

.DESCRIPTION
Check the remote app enabled or not.

#>
function isRemoteAppEnabled() {
Set-Variable key -Option Constant -Value “HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Terminal Server\\TSAppAllowList”

$registryKeyValue = Get-ItemProperty -Path $key -Name fDisabledAllowList -ErrorAction SilentlyContinue

if (-not $registryKeyValue) {
return $false
}
return $registryKeyValue.fDisabledAllowList -eq 1
}

<#

.SYNOPSIS
Check the remote app enabled or not.

.DESCRIPTION
Check the remote app enabled or not.

#>

<#
c
.SYNOPSIS
Get the Win32_OperatingSystem information

.DESCRIPTION
Get the Win32_OperatingSystem instance and filter the results to just the required properties.
This filtering will make the response payload much smaller.

#>
function getOperatingSystemInfo() {
return Get-CimInstance Win32_OperatingSystem | Microsoft.PowerShell.Utility\Select-Object csName, Caption, OperatingSystemSKU, Version, ProductType
}

<#

.SYNOPSIS
Get the Win32_ComputerSystem information

.DESCRIPTION
Get the Win32_ComputerSystem instance and filter the results to just the required properties.
This filtering will make the response payload much smaller.

#>
function getComputerSystemInfo() {
return Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue | `
Microsoft.PowerShell.Utility\Select-Object TotalPhysicalMemory, DomainRole, Manufacturer, Model, NumberOfLogicalProcessors, Domain, Workgroup, DNSHostName, Name, PartOfDomain
}

###########################################################################
# main()
###########################################################################

$operatingSystem = getOperatingSystemInfo
$computerSystem = getComputerSystemInfo
$isAdministrator = isUserAnAdministrator
$fqdnAndAddress = getHostFqdnAndAddress $computerSystem
$hostname = hostname
$netbios = $env:ComputerName
$managementToolsInformation = getManagementToolsSupportInformation
$isWmfInstalled = isWMF5Installed $operatingSystem.Version
$clusterInformation = getClusterInformation -ErrorAction SilentlyContinue
$isHyperVPowershellInstalled = isHyperVPowerShellSupportInstalled
$isHyperVRoleInstalled = isHyperVRoleInstalled
$isCredSSPEnabled = isCredSSPEnabled
$isRemoteAppEnabled = isRemoteAppEnabled

$result = New-Object PSObject
$result | Add-Member -MemberType NoteProperty -Name ‘IsAdministrator’ -Value $isAdministrator
$result | Add-Member -MemberType NoteProperty -Name ‘OperatingSystem’ -Value $operatingSystem
$result | Add-Member -MemberType NoteProperty -Name ‘ComputerSystem’ -Value $computerSystem
$result | Add-Member -MemberType NoteProperty -Name ‘Fqdn’ -Value $fqdnAndAddress.Fqdn
$result | Add-Member -MemberType NoteProperty -Name ‘AddressList’ -Value $fqdnAndAddress.AddressList
$result | Add-Member -MemberType NoteProperty -Name ‘Hostname’ -Value $hostname
$result | Add-Member -MemberType NoteProperty -Name ‘NetBios’ -Value $netbios
$result | Add-Member -MemberType NoteProperty -Name ‘IsManagementToolsAvailable’ -Value $managementToolsInformation.ManagementToolsAvailable
$result | Add-Member -MemberType NoteProperty -Name ‘IsServerManagerAvailable’ -Value $managementToolsInformation.ServerManagerAvailable
$result | Add-Member -MemberType NoteProperty -Name ‘IsWmfInstalled’ -Value $isWmfInstalled
$result | Add-Member -MemberType NoteProperty -Name ‘IsCluster’ -Value $clusterInformation.IsCluster
$result | Add-Member -MemberType NoteProperty -Name ‘ClusterFqdn’ -Value $clusterInformation.ClusterFqdn
$result | Add-Member -MemberType NoteProperty -Name ‘IsS2dEnabled’ -Value $clusterInformation.IsS2dEnabled
$result | Add-Member -MemberType NoteProperty -Name ‘IsHyperVRoleInstalled’ -Value $isHyperVRoleInstalled
$result | Add-Member -MemberType NoteProperty -Name ‘IsHyperVPowershellInstalled’ -Value $isHyperVPowershellInstalled
$result | Add-Member -MemberType NoteProperty -Name ‘IsCredSSPEnabled’ -Value $isCredSSPEnabled
$result | Add-Member -MemberType NoteProperty -Name ‘IsRemoteAppEnabled’ -Value $isRemoteAppEnabled

$result

}

Thank you! :)

Author: Haggai Yedidya, by Real Network Labs: Providing evaluation labs (aka: innovation labs, test labs) for enterprises to test new products prior to purchasing or deployment; or for software developers to assess their product releases pre or post product launch.

Popular assessments: remote management, 3rd party integration such as with active directory, large-scale distribution, stress tests, performance, compatibility and conflicts, maintenance, possible support needs, vulnerabilities, resistency to cyber attacks and more.

Contact us to remotely evaluate a sample test-lab and discuss your owns: Contact@Realnetworklabs.com

--

--

RNL is friends with Startups Israel: where we review innovative startups & write on how to raise money from investors & beyond... https://startupsisrael.com

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Real Network Labs

Real Network Labs

138 Followers

RNL is friends with Startups Israel: where we review innovative startups & write on how to raise money from investors & beyond... https://startupsisrael.com