VMs with Virtual SCSI Bus Sharing

If you’ve ever spent time patching a set of ESXi hosts, the following scenario will likely feel all too familiar.

You kick off the deployment of a new cluster image in a VMware HA cluster. Everything starts smoothly, updates are rolling out, hosts are entering maintenance mode as expected, and for a moment it feels like a smooth upgrade. But then, suddenly, progress comes to a halt. One of the hosts refuses to enter maintenance mode, and the entire update process gets stuck.

In most cases, the root cause isn’t immediately obvious. You might start checking DRS settings, VM migrations, or resource constraints. However, more often than not, the issue comes down to a virtual machine that cannot be migrated due to a dependency on the host. A common cause here is a VM configured with Virtual SCSI Bus Sharing.

From my experience, when this happens, there’s usually one or multiple VMs configured with Virtual SCSI Bus Sharing enabled. Because of this configuration, vMotion is restricted, and the VM becomes “pinned” to its current host. As a result, the host cannot evacuate all workloads and is therefore unable to enter maintenance mode, blocking your patching workflow.

To avoid running into this situation mid-update, it would be incredibly helpful to have visibility into these VMs beforehand. Imagine having a simple report that lists all VMs with Virtual SCSI Bus Sharing enabled across your environment. You could proactively address potential blockers, scheduling downtime, or planning the update sequence more carefully. With that insight and with a bit of help from Claude AI, this script was created.

In short, a bit of preparation goes a long way. Identifying these special cases in advance can save you from frustrating interruptions and ensure your ESXi patching process runs smoothly from start to finish.

The Find-SharedSCSI-BusOnly.ps1 script generates this overview and works with vCenter read-only permissions.


<# 
.SYNOPSIS
  Report VMs that have SCSI controllers with Bus Sharing enabled (Virtual/Physical).

.DESCRIPTION
  - Scans VMs (optionally scoped by Datacenter(s) and Cluster(s)).
  - Detects only SCSI controllers with BusSharing set to Virtual or Physical.
  - Outputs a dark-themed HTML report and opens it in Edge (preferred), or Chrome/Firefox.

.NOTES
    Script  : Find-SharedSCSI-BusOnly.ps1
    Version : 1.0
    Author  : Vincent Jansen
    Blog    : https://www.vrmware.nl
    Tested  : VMware vCenter 8.0.x

.CHANGELOG
      v1.0.0  2026-05-19  VJ  Initial release

.PARAMETER vCenter
  One or more vCenter FQDN/IPs to connect to. If omitted, uses any existing PowerCLI session.

.PARAMETER Cluster
  One or more cluster names to scope the check. If omitted, all clusters.

.PARAMETER Datacenter
  One or more datacenter names to scope the check. If omitted, all datacenters.

.PARAMETER HtmlPath
  Output path for the HTML report (default: .\SharedSCSIReport.html)

.PARAMETER Credential
  PSCredential used to connect to vCenter(s). Overrides UserName/Password if supplied.

.PARAMETER UserName
  vCenter username (used if -Credential not provided). If provided without -Password, a prompt will appear.

.PARAMETER Password
  SecureString password for the given -UserName.

.EXAMPLE
  .\Find-SharedSCSI-BusOnly.ps1 -vCenter vcsa01.lab.local -Cluster "Prod-HA-01" -Username "User01@lab.local"

.NOTES
  Requires VMware.PowerCLI. Run: Install-Module VMware.PowerCLI
#>

[CmdletBinding()]
param(
  [string[]] $vCenter,
  [string[]] $Cluster,
  [string[]] $Datacenter,
  [string]   $HtmlPath = ".\SharedSCSIReport.html",

  [Parameter(ValueFromPipelineByPropertyName=$true)]
  [System.Management.Automation.PSCredential] $Credential,

  [string] $UserName,
  [SecureString] $Password
)

# --- Preconditions -----------------------------------------------------------
if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) {
  Write-Error "VMware.PowerCLI module not found. Install with: Install-Module VMware.PowerCLI"
  exit 1
}

Set-PowerCLIConfiguration -Scope User -ParticipateInCEIP $false -InvalidCertificateAction Ignore -Confirm:$false | Out-Null

# --- Build credential if needed ----------------------------------------------
if (-not $Credential) {
  if ($UserName) {
    if (-not $Password) {
      $Password = Read-Host ("Enter password for {0}" -f $UserName) -AsSecureString
    }
    try {
      $Credential = New-Object System.Management.Automation.PSCredential ($UserName, $Password)
    } catch {
      Write-Error ("Failed to construct PSCredential for {0}: {1}" -f $UserName, $_.Exception.Message)
      exit 1
    }
  }
}

# --- vCenter connections (optional) ------------------------------------------
if ($vCenter) {
  foreach ($vc in $vCenter) {
    $already = $null
    try { $already = Get-VIServer -Server $vc -ErrorAction Stop } catch {}
    if (-not $already) {
      try {
        Write-Verbose ("Connecting to {0} ..." -f $vc)
        if ($Credential) {
          Connect-VIServer -Server $vc -Credential $Credential -ErrorAction Stop | Out-Null
        } else {
          Connect-VIServer -Server $vc -ErrorAction Stop | Out-Null
        }
      } catch {
        Write-Warning ("Failed to connect to {0}: {1}" -f $vc, $_.Exception.Message)
      }
    }
  }
}

# --- Scope selection ---------------------------------------------------------
$dcScope = if ($Datacenter) { Get-Datacenter -Name $Datacenter -ErrorAction SilentlyContinue } else { Get-Datacenter -ErrorAction SilentlyContinue }
if (-not $dcScope) { Write-Warning "No datacenters found in the current session/scope." }

$clusterScope = @()
foreach ($dc in $dcScope) {
  if ($Cluster) {
    $clusterScope += Get-Cluster -Location $dc -Name $Cluster -ErrorAction SilentlyContinue
  } else {
    $clusterScope += Get-Cluster -Location $dc -ErrorAction SilentlyContinue
  }
}

$vms = if ($clusterScope) { $clusterScope | Get-VM -ErrorAction SilentlyContinue } else { Get-VM -ErrorAction SilentlyContinue }
if (-not $vms) { Write-Warning "No VMs found in the current scope."; $vms = @() }

# --- Helper: SCSI controller info only ---------------------------------------
function Get-ScsiControllerInfo {
  param([VMware.VimAutomation.ViCore.Impl.V1.Inventory.VirtualMachineImpl] $Vm)

  $hw = $Vm.ExtensionData.Config.Hardware
  if (-not $hw) { return @() }

  $controllers = $hw.Device | Where-Object {
    $_ -is [VMware.Vim.ParaVirtualSCSIController]     -or
    $_ -is [VMware.Vim.VirtualLsiLogicController]     -or
    $_ -is [VMware.Vim.VirtualLsiLogicSASController]  -or
    $_ -is [VMware.Vim.VirtualBusLogicController]
  }

  foreach ($c in $controllers) {
    $busSharingPretty = switch ($c.SharedBus) {
      'noSharing'       {'NoSharing'}
      'virtualSharing'  {'Virtual'}
      'physicalSharing' {'Physical'}
      default           { [string]$c.SharedBus }
    }

    [pscustomobject]@{
      VM              = $Vm.Name
      ControllerKey   = $c.Key
      ControllerType  = $c.GetType().Name
      BusNumber       = $c.BusNumber
      BusSharing      = $busSharingPretty
    }
  }
}

# --- Analysis: only BusSharing findings --------------------------------------
Write-Verbose "Inspecting SCSI controllers (bus sharing only)..."
$results = New-Object System.Collections.Generic.List[object]

foreach ($vm in $vms) {
  try {
    $ctrls = Get-ScsiControllerInfo -Vm $vm
    $sharedBusCtrls = $ctrls | Where-Object { $_.BusSharing -and $_.BusSharing -ne 'NoSharing' }
    if ($sharedBusCtrls) {
      $dcName      = ($vm | Get-Datacenter -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name -First 1)
      $clusterName = ($vm | Get-Cluster -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name -First 1)
      foreach ($c in $sharedBusCtrls) {
        $results.Add([pscustomobject]@{
          Datacenter     = $dcName
          Cluster        = $clusterName
          VM             = $vm.Name
          PowerState     = $vm.PowerState
          ControllerType = $c.ControllerType
          BusNumber      = $c.BusNumber
          BusSharing     = $c.BusSharing
        })
      }
    }
  } catch {
    Write-Warning ("Error analyzing VM '{0}': {1}" -f $vm.Name, $_.Exception.Message)
  }
}

# --- Build HTML (dark theme) -------------------------------------------------
$now = Get-Date
$findings      = $results | Sort-Object Cluster, VM, BusNumber
$totalFindings = $findings.Count
$totalVMs      = ($findings | Select-Object -ExpandProperty VM -Unique).Count
$totalClusters = ($findings | Select-Object -ExpandProperty Cluster -Unique).Count

$clusterAgg = $findings | Group-Object Cluster | ForEach-Object {
  [pscustomobject]@{
    Cluster  = $_.Name
    VMs      = ($_.Group | Select-Object -ExpandProperty VM -Unique).Count
    Findings = $_.Count
  }
} | Sort-Object Cluster

$css = @'
:root {
  color-scheme: dark;
  --bg: #0f1115; --panel: #141821; --text: #e5e7eb; --muted: #9ca3af;
  --accent: #60a5fa; --border: #1f2937; --row: #0b0e14; --ok: #34d399; --danger:#f87171;
}
*{box-sizing:border-box}
body{margin:0;padding:24px;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial,sans-serif}
h1,h2{margin:0 0 12px 0} h1{font-size:22px} h2{font-size:18px;color:var(--muted)}
.panel{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:16px}
.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
.stat{display:flex;align-items:center;justify-content:space-between;padding:14px;background:#0c1220;border:1px solid var(--border);border-radius:10px}
.stat .label{color:var(--muted);font-size:12px} .stat .value{font-size:22px;font-weight:700}
.table-wrap{overflow:auto;max-height:60vh;border:1px solid var(--border);border-radius:10px}
table{width:100%;border-collapse:collapse}
thead th{position:sticky;top:0;background:#111827;color:var(--muted);text-align:left;padding:10px;font-weight:600;font-size:12px;cursor:pointer;user-select:none;border-bottom:1px solid var(--border)}
tbody td{padding:10px;border-bottom:1px solid var(--border);font-size:13px}
tbody tr:nth-child(even){background:var(--row)}
.badge{padding:2px 8px;border-radius:999px;font-size:12px;border:1px solid var(--border)}
.badge.on{color:var(--ok);background:rgba(52,211,153,.08);border-color:rgba(52,211,153,.4)}
.badge.off{color:var(--danger);background:rgba(248,113,113,.08);border-color:rgba(248,113,113,.4)}
.small{font-size:12px;color:var(--muted)}
.controls{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
'@

$js = @'
(function(){
  const q = s => document.querySelector(s);
  const rows = Array.from(document.querySelectorAll("tbody tr"));
  let sortDir = 1, sortCol = -1;
  function text(el){ return (el?.textContent || "").trim().toLowerCase(); }
  function applyFilter(){
    const fCluster = q("#fCluster").value.toLowerCase();
    const fVM      = q("#fVM").value.toLowerCase();
    let visible = 0;
    rows.forEach(tr=>{
      const c = tr.children;
      const cluster = text(c[1]);
      const vm      = text(c[2]);
      const ok = (fCluster==="" || cluster.includes(fCluster))
              && (fVM===""      || vm.includes(fVM));
      tr.style.display = ok ? "" : "none";
      if(ok) visible++;
    });
    q("#visibleCount").textContent = visible;
  }
  function sortBy(colIdx){
    const tbody = rows[0]?.parentElement; if(!tbody) return;
    if(sortCol === colIdx){ sortDir = -sortDir; } else { sortCol = colIdx; sortDir = 1; }
    const sorted = rows.slice().sort((a,b)=>{
      const A = text(a.children[colIdx]), B = text(b.children[colIdx]);
      const nA = parseFloat(A.replace(/[^0-9.\-]/g,"")), nB = parseFloat(B.replace(/[^0-9.\-]/g,""));
      const bothNum = !isNaN(nA) && !isNaN(nB) && A.match(/^[\d .\-]+$/) && B.match(/^[\d .\-]+$/);
      return (bothNum ? (nA-nB) : A.localeCompare(B)) * sortDir;
    });
    sorted.forEach(tr=>tbody.appendChild(tr));
  }
  ["fCluster","fVM"].forEach(id => q("#"+id).addEventListener("input", applyFilter));
  document.querySelectorAll("thead th").forEach((th,i)=> th.addEventListener("click", ()=>sortBy(i)));
  applyFilter();
})();
'@

function Escape-Html {
  param([AllowNull()] [string] $s)
  if ($null -eq $s) { return "" }
  ($s -replace '&','&amp;' -replace '<','&lt;' -replace '>','&gt;' -replace '"','&quot;' -replace "'","&#39;")
}

# Build rows (Bus-sharing only)
$tbody = New-Object System.Text.StringBuilder
foreach ($r in $findings) {
  $pstate = if ($r.PowerState -eq 'PoweredOn') { '<span class="badge on">On</span>' } else { '<span class="badge off">Off</span>' }
  [void]$tbody.AppendLine( ("<tr>" +
    "<td>{0}</td>" +   # Datacenter
    "<td>{1}</td>" +   # Cluster
    "<td>{2}</td>" +   # VM
    "<td>{3}</td>" +   # Power
    "<td>{4}</td>" +   # ControllerType
    "<td>{5}</td>" +   # Bus#
    "<td>{6}</td>" +   # BusSharing
    "</tr>"
  ) -f (
    (Escape-Html $r.Datacenter),
    (Escape-Html $r.Cluster),
    (Escape-Html $r.VM),
    $pstate,
    (Escape-Html $r.ControllerType),
    (Escape-Html $r.BusNumber),
    (Escape-Html $r.BusSharing)
  ))
}

# Cluster summary rows
$clusterRows = New-Object System.Text.StringBuilder
foreach ($c in $clusterAgg) {
  [void]$clusterRows.AppendLine( ("<tr><td>{0}</td><td>{1}</td><td>{2}</td></tr>" -f (Escape-Html $c.Cluster), $c.VMs, $c.Findings) )
}

# HTML
$html = @"
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Shared SCSI Bus Report</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>$css</style>
</head>
<body>
  <div class="panel">
    <h1>Shared SCSI Bus Report</h1>
    <h2>Generated: $($now) </h2>
    <div class="grid" style="margin-top:12px;">
      <div class="stat"><div><div class="label">Findings</div><div class="value" id="visibleCount">$totalFindings</div></div></div>
      <div class="stat"><div><div class="label">Unique VMs</div><div class="value">$totalVMs</div></div></div>
      <div class="stat"><div><div class="label">Clusters</div><div class="value">$totalClusters</div></div></div>
    </div>
  </div>

  <div class="panel">
    <h2>Filters</h2>
    <div class="controls">
      <input id="fCluster" type="text" placeholder="Filter by Cluster...">
      <input id="fVM" type="text" placeholder="Filter by VM name...">
    </div>
  </div>

  <div class="panel">
    <h2>Cluster Summary</h2>
    <div class="table-wrap">
      <table>
        <thead>
          <tr><th>Cluster</th><th>Unique VMs</th><th>Findings</th></tr>
        </thead>
        <tbody>
          $clusterRows
        </tbody>
      </table>
    </div>
  </div>

  <div class="panel">
    <h2>Detailed Findings</h2>
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Datacenter</th>
            <th>Cluster</th>
            <th>VM</th>
            <th>Power</th>
            <th>ControllerType</th>
            <th>Bus#</th>
            <th>BusSharing</th>
          </tr>
        </thead>
        <tbody>
          $tbody
        </tbody>
      </table>
    </div>
    <div class="small">Click a column header to sort. Use filters above to narrow results.</div>
  </div>

  <script>$js</script>
</body>
</html>
"@

# Write HTML
try {
  $fullPath = (Resolve-Path -Path $HtmlPath -ErrorAction SilentlyContinue)
  if (-not $fullPath) {
    $dir = Split-Path -Path $HtmlPath -Parent
    if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
    $html | Out-File -FilePath $HtmlPath -Encoding UTF8
    $fullPath = Resolve-Path -Path $HtmlPath
  } else {
    $html | Out-File -FilePath $fullPath -Encoding UTF8
  }
  Write-Host ("HTML report saved to: {0}" -f $fullPath.Path)
} catch {
  Write-Warning ("Failed to write HTML to '{0}': {1}" -f $HtmlPath, $_.Exception.Message)
  return
}

# --- Open in browser ---------------------------------------------------------
function Open-InBrowser {
  param([string]$Path)
  $candidates = @(
    "$Env:ProgramFiles (x86)\Microsoft\Edge\Application\msedge.exe",
    "$Env:ProgramFiles\Microsoft\Edge\Application\msedge.exe",
    "$Env:LocalAppData\Microsoft\Edge\Application\msedge.exe",
    "$Env:ProgramFiles\Google\Chrome\Application\chrome.exe",
    "$Env:ProgramFiles (x86)\Google\Chrome\Application\chrome.exe",
    "$Env:LocalAppData\Google\Chrome\Application\chrome.exe",
    "$Env:ProgramFiles\Mozilla Firefox\firefox.exe",
    "$Env:ProgramFiles (x86)\Mozilla Firefox\firefox.exe",
    "$Env:LocalAppData\Mozilla Firefox\firefox.exe"
  )
  foreach ($exe in $candidates) {
    if (Test-Path $exe) { Start-Process -FilePath $exe -ArgumentList ("`"{0}`"" -f $Path); return $true }
  }
  try { Start-Process $Path; return $true } catch { return $false }
}
$ok = Open-InBrowser -Path $fullPath.Path
if (-not $ok) { Write-Warning ("Could not open the report automatically. Please open: {0}" -f $fullPath.Path) }

Once the data is gathered, it generates a HTML report that provides a complete overview at a glance.


The script provides a clear, proactive, and repeatable way to identify VMs with Virtual SCSI Bus Sharing, preventing maintenance mode issues and keeping your ESXi patching process running smoothly.

This script is also available in my GitHub repositiry.

Automating NIC Firmware Inventory Across VMware Clusters with PowerShell

Keeping firmware versions consistent across VMware hosts is crucial for stability, performance, and security. Yet, identifying which ESXi hosts are running which network interface firmware versions can quickly become a tedious task, especially in environments with multiple clusters and dozens of servers.

Recently, I faced exactly this challenge. I needed a clear and reliable overview of the installed NIC firmware versions across all hosts in specific VMware clusters. Rather than performing the checks manually (or clicking through countless vSphere UI tabs), I decided to automate the process.

VMware environments often contain a mix of hardware models, driver versions, and firmware levels. These variables play a major role in network reliability and performance. When a firmware update is released or when troubleshooting network issues, it’s important to know:

  • Which hosts are running outdated firmware?
  • Which clusters might be at risk due to inconsistent versions?

However, VMware doesn’t provide a quick, consolidated overview of NIC firmware versions per cluster out of the box.

To streamline this task, I wrote a PowerShell script that retrieves the NIC firmware versions for each host in a VMware cluster. Using PowerCLI, the script collects:

  • vCenter & Cluster name on top of the report
  • Hostname
  • Server Model
  • NIC
  • NIC Model
  • Firmware Version

Once the data is gathered, it generates a HTML report that provides a complete overview at a glance.

Below is the script used to build this overview.


#Version 1.0
#2026-03-28
#Check NIC Firmware script with HTML output
#Adds vCenter, Cluster & Server Model to HTML report

# ============================
#  Ask user for vCenter & Cluster
# ============================
$vCenter = Read-Host "Enter vCenter Server name or IP"
$cluster  = Read-Host "Enter Cluster name"

Write-Host "Using vCenter: $vCenter" -ForegroundColor Cyan
Write-Host "Using Cluster: $cluster" -ForegroundColor Cyan

# ============================
#  Function: Check NIC Firmware
# ============================
function CheckNICFirmware {
    Param (
        $ESXi
    )

    $esxcli = Get-EsxCli -V2 -VMHost $ESXi
    $result = ""

    # Get Server Model
    $serverModel = (Get-View -Id $ESXi.ExtensionData.MoRef).Hardware.SystemInfo.Model

    # Get NIC list
    $nicList = $esxcli.network.nic.list.Invoke()

    foreach ($nic in $nicList) {

        $nicName = $nic.Name
        $nicGet  = $esxcli.network.nic.get.Invoke(@{nicname=$nicName})

        $model = $nic.Description
        $firmwareVersion = $nicGet.DriverInfo.FirmwareVersion

        $rowColor = "#e6f0ff"   # Light blue row

        $result += "<tr style='background-color: $rowColor;'>
                        <td>$($ESXi.Name)</td>
                        <td>$serverModel</td>
                        <td>$nicName</td>
                        <td>$model</td>
                        <td>$firmwareVersion</td>
                    </tr>"
    }

    return $result
}

# ============================
#  Connect to vCenter
# ============================
Try {Disconnect-VIServer * -Confirm:$false -ErrorAction SilentlyContinue | Out-Null}
Catch {}

Connect-VIServer $vCenter

$ESXis = Get-Cluster -Name $cluster | Get-VMHost | Sort-Object Name | Where-Object {
    $_.ConnectionState -eq 'Connected' -or $_.ConnectionState -eq 'Maintenance'
}

# ============================
#  HTML: Header
# ============================
$html = @"
<html>
<head>
    <title>NIC Firmware Report - vCenter: $vCenter | Cluster: $cluster</title>
    <style>
        table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
        th, td { border: 1px solid black; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
        h1 { margin-bottom: 5px; }
        .subheader { color: #555; font-size: 18px; margin-bottom: 20px; }
    </style>
</head>
<body>
    <h1>NIC Firmware Report</h1>
    <div class="subheader">
        vCenter Server: <b>$vCenter</b><br>
        Cluster: <b>$cluster</b>
    </div>

    <table>
        <tr>
            <th>Host</th>
            <th>Server Model</th>
            <th>NIC</th>
            <th>NIC Model</th>
            <th>Firmware Version</th>
        </tr>
"@

# ============================
#  NIC Firmware Section
# ============================
foreach ($ESXi in $ESXis) {
    $html += CheckNICFirmware -ESXi $ESXi
}

$html += "</table>"

# ============================
#  HTML Footer
# ============================
$creationDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

$html += @"
<p>Report generated on: $creationDate</p>
</body>
</html>
"@

# ============================
#  Output Report
# ============================
$outputPath = "C:\Scripts\NIC_Firmware_Report.html"
$html | Out-File -FilePath $outputPath

Start-Process "msedge.exe" $outputPath

Write-Host "Disconnecting from $vCenter..."
Disconnect-VIServer -Confirm:$False | Out-Null

Write-Host "Report generated at $outputPath" -ForegroundColor Green

The final output is an automatically generated HTML page containing a complete NIC firmware inventory per VMware cluster.


It provides a quick, accurate, and repeatable way to check firmware versions across an environment, saving time and avoiding configuration drift.

Monitoring datastores under /mnt in Linux with VCF Operations Telegraf agent

Recently, I have spent a lot of time monitoring Linux servers with the Telegraf agent in VCF Operations. This includes metrics such as /boot, /var, /var/log, etc. This was fairly easy to implement. However, I also wanted to be able to monitor datastores under /mnt. It turned out that these metrics are not available by default after installing the Telegraf agent.

Objective: Raise an alert when a datastore under /mnt exceeds 75% capacity.

After installing the Telegraf agent on a Linux server, the following directory was created: /opt/vmware. In this directory, I created the following bash script: pct_used.sh. Please note that a script is executed from Telegraf using the system account arcuser. The arcuser account must have read and execute permissions for the script. Set the permissions using the following command:

chmod 755 /opt/vmware/pct_used.sh

The script below is a generic script for reading the % used from the datastores under /mnt. By providing the correct arguments you will receive a value (Use%) that you can use as a metric in VCF Operations as input for the alert. In the following examples:

  • store1 = Linux Server
  • 001 = Datastore 001
  • 002 = Datastore 002

Examples:
root@vrmware001:/opt/vmware# ./pct_used.sh /mnt/store1/001
Result: 70

root@vrmware001:/opt/vmware# ./pct_used.sh /mnt/store1/002
Result: 64

Please note that the system account arcuser also has read + execute permissions on the /mnt and /mnt/server directory. In the examples mentioned above, this means that these rights must be located on /mnt/store1. Here’s how to do it.

  • chmod 755 /mnt
  • chmod 755 /mnt/store1

With the next command you can check if the arcuser account have permissions the read the datastores.

sudo -u arcuser df -P /mnt/store1/001 where /mnt/store1/001 should be replaced with your own datastore path.

#!/usr/bin/env bash
# Usage: ./pct_used.sh /mnt/store1/001

set -euo pipefail

MOUNT_PATH="${1:-}"

# Check if argument is provided
if [[ -z "$MOUNT_PATH" ]]; then
  echo "Usage: $0 <mount_path>" >&2
  exit 2
fi

# Check if argument is provided
if [[ ! -d "$MOUNT_PATH" ]]; then
  echo "Path not found: $MOUNT_PATH" >&2
  exit 3
fi

# Get percentage used via df; NR==2 = the data row
# +0 forces numeric output (strips '%')
pct_used="$(df -P "$MOUNT_PATH" 2>/dev/null | awk 'NR==2{print $5+0}')"

# Validate: empty or not numeric?
if [[ -z "$pct_used" || ! "$pct_used" =~ ^[0-9]+$ ]]; then
  echo "Unable to read usage for: $MOUNT_PATH" >&2
  exit 4
fi

# Output only the number to stdout (for Telegraf)
echo "$pct_used"

Now the script must be launched from VCF Operations.

Go to Manage Telegraf Agents section, select your favourite Linux server and add a Custom Script.

If the custom script is configured correctly, the first data should arrive after 5 to 10 minutes. You can see one of the following two statuses at the Telegraf agent.

This means that the data is being received.

This means that the data has not changed since the previous measurement point.

Go to the inventory view of the Linux server and select Custom Script. Check if the status is Normal (green).

If the status is normal, go to the Metrics tab, Metrics, Scripts, Custom Script (store1-001). Double click on the Custom Script and there is the Metric value.

With these metrics, we can now create alerts (Use%) for datastores on Linux servers mounted under /mnt.

I tested it in my lab on both Aria Operations 8.18.5 and VCF Operations 9.0.1. It works on both versions.

ESXi Lockdown Menu Script

The following Powershell script allows you to set the ESXi Lockdown Mode of all ESXi hosts within a vCenter to “Disabled” or “Normal”. You can also get a summary of the current Lockdown status of all ESXi hosts. This overview is displayed in an HTML page that automatically opens in the MS Edge browser.

After starting the script, you get the following menu with options:

The output of option 0, Show current Lockdown Mode status looks as follows.

This way you have an overview of the Lockdown Status of all ESXi hosts in a vCenter.

#Version 1.0
#2025-05-27
#ESXi Set Lockdown Mode and Script

# Prompt for vCenter and connect
Clear-Host
$vCenterServer = Read-Host "Enter the vCenter Server to connect to"
try {
    Connect-VIServer -Server $vCenterServer -ErrorAction Stop
    Write-Host "Connected to $vCenterServer" -ForegroundColor Green
} catch {
    Write-Host "Failed to connect to $vCenterServer. Exiting script." -ForegroundColor Red
    exit
}

function Show-Menu {
    Clear-Host
    Write-Host ""
    Write-Host "======================" -ForegroundColor Cyan
    Write-Host " Lockdown Mode Menu"
    Write-Host "======================" -ForegroundColor Cyan
    Write-Host "0. Show current Lockdown Mode status"
    Write-Host "1. Disable Lockdown Mode (lockdownDisabled)"
    Write-Host "2. Enable Lockdown Mode to 'normal' (lockdownNormal)"
    Write-Host "Q. Quit"
    $choice = Read-Host "Enter your choice (0, 1, 2 or Q)"
    return $choice
}

function Set-LockdownMode {
    param (
        [string]$mode
    )

    $ESXiHosts = Get-VMHost

    foreach ($ESXiHost in $ESXiHosts) {
        try {
            $ESXiHostView = Get-View -Id $ESXiHost.Id
            $accessManager = Get-View -Id $ESXiHostView.ConfigManager.HostAccessManager
            $accessManager.ChangeLockdownMode($mode)
            Write-Host "Lockdown Mode set to '$mode' for $($ESXiHost.Name)" -ForegroundColor Green
        } catch {
            Write-Host "Error on host $($ESXiHost.Name): $_" -ForegroundColor Red
        }
    }
}

function Generate-HTMLReport {
    $ESXiHosts = Get-VMHost
    $rows = @()

    foreach ($ESXiHost in $ESXiHosts) {
        try {
            $lockdownMode = $ESXiHost.ExtensionData.Config.LockdownMode
            $rows += "<tr><td>$($ESXiHost.Name)</td><td>$lockdownMode</td></tr>"
        } catch {
            $rows += "<tr><td>$($ESXiHost.Name)</td><td>Error: $_</td></tr>"
        }
    }

    $html = @"
<html>
<head>
    <title>Lockdown Mode Status</title>
    <style>
        body { font-family: Arial; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; }
        th { background-color: #f2f2f2; }
    </style>
</head>
<body>
    <h2>Lockdown Mode Status Report</h2>
    <p>Generated on $(Get-Date)</p>
    <table>
        <tr><th>Hostname</th><th>Lockdown Mode</th></tr>
        $($rows -join "`n")
    </table>
</body>
</html>
"@

    $path = "$env:TEMP\\LockdownStatus.html"
    $html | Out-File -FilePath $path -Encoding UTF8
    Start-Process "msedge.exe" $path
}

# Main loop
do {
    $choice = Show-Menu

    switch ($choice.ToLower()) {
        "0" { Generate-HTMLReport }
        "1" { Set-LockdownMode -mode "lockdownDisabled" }
        "2" { Set-LockdownMode -mode "lockdownNormal" }
        "q" {
            Write-Host "Disconnecting from vCenter..." -ForegroundColor Cyan
            Disconnect-VIServer -Server $vCenterServer -Confirm:$false
            Write-Host "Disconnected. Exiting script." -ForegroundColor Cyan
        }
        default { Write-Host "Invalid choice. Please try again." -ForegroundColor Yellow }
    }

} while ($choice.ToLower() -ne "q")

Which Vm have a VM Override Check Script

Does the following sound familiar? While patching a VMware cluster, one ESXi host does not want to enter Maintenance Mode. It appears that one or more vms cannot be migrated to another ESXi host. Manually these vms can be migrated and the host still goes into maintenance mode.

The cause is usually a VM Override that has been configured. How useful would it be if, prior to patching the ESXi hosts in the cluster, you have an overview of the vms with a VM Override.

The following script lists all vms with a VM Override in an HTML page sorted by cluster.

#Version 1.0
#2025-04-28
#Check VMs in Cluster with a VM Override

# Connect to vCenter
$vcServer = Read-Host "Enter the vCenter Server name or IP address"
Connect-VIServer -Server $vcServer

# Retrieve all clusters
$clusters = Get-Cluster

# Create an empty array to store results
$results = @()

# Loop through each cluster
foreach ($cluster in $clusters) {
    Write-Output "Processing cluster: $($cluster.Name)"

    $vms = Get-VM -Location $cluster

    foreach ($vm in $vms) {
        $automationLevel = $vm.DrsAutomationLevel

        $results += [PSCustomObject]@{
            ClusterName        = $cluster.Name
            VMName             = $vm.Name
            DRSAutomationLevel = $automationLevel
        }
    }
}

# Filter: Only VMs with different automation levels
$filteredResults = $results | Where-Object {
    $_.DRSAutomationLevel -ne "AsSpecifiedByCluster" -and
    $_.DRSAutomationLevel -ne "UseClusterSettings"
}

# HTML file path
$outputHtmlPath = "D:\Temp\vm_overrides.html"

# HTML header with basic table CSS styling
$htmlHeader = @"
<html>
<head>
<title>VM DRS Overrides Report</title>
<style>
body { font-family: Arial, sans-serif; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid black; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h2>VMs with DRS Automation Level Overrides (VM Overrides) in vCenter $vcServer</h2>
"@

$htmlFooter = @"
</body>
</html>
"@

# Generate the HTML table from the data
$htmlTable = $filteredResults | ConvertTo-Html -Fragment -Property ClusterName, VMName, DRSAutomationLevel

# Combine everything
$htmlContent = $htmlHeader + $htmlTable + $htmlFooter

# Write to the HTML file
$htmlContent | Out-File -Encoding UTF8 -FilePath $outputHtmlPath

Write-Output "HTML report saved to: $outputHtmlPath"

# Open the HTML file with Microsoft Edge
Start-Process "msedge.exe" -ArgumentList $outputHtmlPath

# Disconnect from vCenter
Disconnect-VIServer -Server * -Confirm:$false

The HTML page looks like this.

VMware Aria Operations Telegraf Endpoint Agent installation fails on Bare Metal Linux server

Recent I was running into the following error when installing an VMware Aria Operations Telegraf Endpoint Agent on a Bare Metal Linux server.

Error:Failed to update VM bootstrap failure message. Error code: {“message”:”Request Validation Failed. Reason is \”Collection of resource property contents can not be null or empty\”.”,”httpStatusCode”:400,”apiErrorCode”:1517}400
install agents failed at Runtime user: arcuser is not having password-less privileges. If runtime user was created manually, ensure required privileges are available. Also ensure that /etc/sudoers file has a line ‘#includedir /etc/sudoers.d’. Please check /root/arc_install_tmp_dir/uaf_bootstrap.log on the endpoint VM.. please check logs for more detail
.”

To solve this issue add the following line to /etc/sudoers.

@includedir /etc/sudoers.d

There is a Broadcom KB about this error but personally I don’t find this one very clear to me.

PowerCLI script to get Syslog.Global.Host advanced setting

The following script may be useful if you are in the process of migrating vRealize Log Insight to a new appliance/cluster. You can use this script before, during and after migrating to check the settings of Syslog.Global.Loghost of all ESXi hosts in vCenter.

# Version 1.0
# 2024-11-17
# Check ESXi Syslog Global LogHost script with HTML output
# This script checks if specified Syslog Global LogHost are configured on all hosts in a datacenter and writes the output to an HTML file

# Connect to vCenter Server
$vCenter = "FQDN vCenter"
Connect-VIServer $vCenter

# Get all ESXi hosts in the cluster
$hosts = get-vmhost

# Initialize HTML content with styles for alternating row colors
$htmlContent = @"
<html>
<head>
    <title>ESXi Syslog Settings</title>
    <style>
        table {
            width: 100%;
            border-collapse: collapse;
        }
        th, td {
            padding: 8px;
            text-align: left;
            border: 1px solid #ddd;
        }
        tr:nth-child(odd) {
            background-color: white;
        }
        tr:nth-child(even) {
            background-color: lightgrey;
        }
    </style>
</head>
<body>
    <h1>ESXi Syslog Settings</h1>
    <table>
        <tr>
            <th>ESXi Host</th>
            <th>Syslog Global Loghost</th>
        </tr>
"@

# Loop through each ESXi host and get the syslog.global.loghost advanced setting
foreach ($esxi in $hosts) {
    $setting = Get-AdvancedSetting -Entity $esxi -Name 'syslog.global.loghost'
    $htmlContent += "<tr><td>$($esxi.Name)</td><td>$($setting.Value)</td></tr>"
}

# Add the current date and time
$currentDateTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$htmlContent += @"
    </table>
    <p>Report generated on: $currentDateTime</p>
</body>
</html>
"@

# Save HTML content to a file
$outputPath = "C:\Scripts\esxi_syslog_settings.html"
$htmlContent | Out-File -FilePath $outputPath

# Open the HTML file
Start-Process "msedge.exe" $outputPath

# Disconnect from vCenter Server
Disconnect-VIServer -Confirm:$false

For example: You can use the script during vRealize Log Insight in the following way:

  • Before migration
    Check the current configured syslog endpoint
  • During migration
    Check the current and new syslog endpoints are configured
  • After migration
    Check the new configured syslog endpoint

Check wether VIBs are installed or not on ESXi hosts in a cluster with VIB Report Script

In my last blog, I shared a Powershell script that can be used to remove VIBs from all ESXi hosts in a cluster. I have created a small Powershell script that you can run before and after removing of VIBs to check the availability of VIBs.

The output is displayed in an HTML file once the script is finished. The VIBs being checked are defined in $arrayvibs. The location of the HTML file can be defined in $outputPath. The number of VIBs you can have checked depends on how many you define in $arrayvibs.

Here are a few examples of the HTML output files.

Example output before run VIB Check Report script to remove the VIBs “nenic” and “iavmd”.

Example output after run VIB Check Report script to remove the VIBs “nenic” and “iavmd”.

Please be aware that using this script is at your own risk!

# Version 1.0
# 2024-11-14
# Check VIBs script with HTML output
# This script checks if specified VIBs are installed on all hosts in a cluster and writes the output to an HTML file

# Function Check VIB
function CheckVIB {
    Param (
        $ESXi  # The EsxCli object
    )

    [array]$arrayvibs = @("nenic", "iavmd")
    $esxcli = Get-EsxCli -V2 -VMHost $ESXi
    $result = ""

    foreach ($vib in $arrayvibs) {
        if ($esxcli.software.vib.list.Invoke() | Where-Object {$_.Name -eq "$vib"}) {
            $result += "<tr style='background-color: #ffe6e6;'><td>$($ESXi.Name)</td><td>$vib</td><td>Found</td></tr>"
        } else {
            $result += "<tr style='background-color: #e6ffe6;'><td>$($ESXi.Name)</td><td>$vib</td><td>Not Found</td></tr>"
        }
    }

    return $result
}

# vCenter & Cluster Parameters
$vCenter = "FQDN vCenter"
$cluster = "Cluster"

# Connect vCenter
Try {Disconnect-VIServer * -Confirm:$false -ErrorAction SilentlyContinue | Out-Null}
Catch {}

Connect-VIServer $vCenter

$ESXis = Get-Cluster -Name $cluster | Get-VMHost | Sort-Object Name | Where-Object {$_.ConnectionState -eq 'Connected' -or $_.ConnectionState -eq 'Maintenance'}

# HTML header
$html = @"
<html>
<head>
    <title>VIB Check Report</title>
    <style>
        table { width: 100%; border-collapse: collapse; }
        th, td { border: 1px solid black; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
    </style>
</head>
<body>
    <h1>VIB Check Report</h1>
    <table>
        <tr>
            <th>Host</th>
            <th>VIB</th>
            <th>Status</th>
        </tr>
"@

foreach ($ESXi in $ESXis) {
    $html += CheckVIB -ESXi $ESXi
}

# HTML footer with creation date and time
$creationDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$html += @"
    </table>
    <p>Report generated on: $creationDate</p>
</body>
</html>
"@

# Output HTML to file
$outputPath = "C:\Scripts\VIB_Check_Report.html"
$html | Out-File -FilePath $outputPath

# Open the HTML file in Microsoft Edge
Start-Process "msedge.exe" $outputPath

# Disconnect vCenter
Write-Host "Disconnecting vCenter $vCenter"
Disconnect-VIServer -Confirm:$False | Out-Null

Write-Host "Report generated at $outputPath"

VMware Cluster upgrade vSphere 7 to vSphere 8 with a single image is blocked by not supported VIBs

Recently, I wanted to upgrade a vSphere 7 non vSAN cluster to version 8 with a single image. During the compliance check it turned out that all hosts in the cluster were not compliant and therefore the upgrade could not be started. The following error was displayed.

There appeared to be two VIBs installed that were not supported. On the host’s commandline, I looked up the names of VIBs and noted them. The VIBs involved in this case were the following:

  • nenic
  • iavmd

These will be needed later in the Powershell script.

Since I don’t want to log into every server and then have to delete the VIBs via the CLI, I created the following script. I would like to thank my colleague Kabir very much for his time, explanation and mentoring. He has great scripting and automation skills and has written great articles that you can find here, whatkabirwrites.nl

Back to the script. The script removes the two VIBs on all hosts in a cluster. Before using the script be sure that the VIBs are not used. The following steps are performed by the script:

  • Host is put into maintenance mode
  • Check if VIBs are installed
  • If present, they are removed
  • Host is rebooted
  • Host goes out of maintenance mode
  • Next host

If a host is already in maintenance mode, it will remain in maintenance mode after the reboot.

At the top of the script, the Remove VIBs function is defined. Adjusting the setting $settings.dryrun = $true to $false really removes the VIBs. Without adjusting this, the VIBs are not removed and only verified to be present or not. Regardless of the value of $settings.dryrun = $true or $false the hosts are always put into maintenance mode and rebooted.

After executing the host reboot, the script waits 4 minutes before continuing. I built in this pause because nested ESXi hosts reboot so quickly that otherwise the script won’t enter or exit the wait loop. I have used the script in a test lab and it works very well.

Please be aware that using this script is at your own risk!

#Version 1.0
#2024-10-20
#Pre-Upgrade script ESXi7 to ESXi8
#This script remove vibs on all hosts in a cluster that blocks the upgrade

# Function Remove VIBs
function RemoveVIB {
    Param (
        $ESXi  # The EsxCli object
      
    )


[array]$arrayvibs = @("nenic", "iavmd")

$esxcli = Get-EsxCli -V2 -VMHost $ESXi

    foreach ($vib in $arrayvibs) {
        
        if ($esxcli.software.vib.list.Invoke() | where {$_.Name -eq "$VIB"}) {
            $settings = $esxcli.software.vib.remove.CreateArgs()
            $settings.dryrun = $true
            $settings.vibname = "$VIB"
            echo "$VIB VIB found, remove VIB $esx"
            $esxcli.software.vib.remove.Invoke($settings)        
        } 
        else {
            echo "No $VIB VIB found $esx"             
        } 
    }

}

# vCenter & Cluster Parameters
$vCenter = "FQDN vCENTER"
$cluster = "Cluster Name"


# Connect vCenter
Try {Disconnect-VIServer * -Confirm:$false -ErrorAction SilentlyContinue | out-null}
Catch {}
    
Connect-VIServer $vCenter 

$ESXis= Get-Cluster -Name $cluster| Get-VMHost | sort Name | where {$_.ConnectionState -eq 'Connected' -or $_.ConnectionState -eq 'Maintenance'}

foreach ($ESXi in $ESXis) {
    write-host "Working on host $($esxi)"
    
    # Host status is Maintenance Mode
    if ($ESXi.ConnectionState -eq 'Maintenance') {
        Write-host "Host is already in Maintenance Mode..."
        RemoveVIB -ESXi $ESXi
        Write-host "Host Reboot in Maintenance Mode..."
        write-host "Herstarten host $($esxi)"
        Restart-VMHost -VMHost $ESXi -Confirm:$False | Out-Null
        write-host "Waiting 4 minutes to make sure the host is disconnected before proceeding..."
        start-sleep 240
        $hoststat = (Get-VMHost -Name $ESXi.Name)
        While ($hoststat.ConnectionState -eq "NotResponding") {
        Write-host "Host is still rebooting... waiting 10sec..."
        Start-Sleep 10
        $hoststat = (Get-VMHost -Name $ESXi.Name)
       }
    }
    #Host status is not Maintenance Mode
    else {
        Write-Host "$($ESXi) is not in Maintenance Mode. Put host in Maintenance Mode..."
        write-host ""

        # Host in Maintenance Mode
        Set-VMHost -VMHost $ESXi -State Maintenance -Confirm:$False | Out-Null

        # Vib Remove
        RemoveVIB -ESXi $ESXi
        # Host Reboot
        Write-host "Host is in Maintenance Mode"
        write-host "Reboot host $($esxi)"
        Restart-VMHost -VMHost $ESXi -Confirm:$False | Out-Null
        write-host "Waiting 4 minutes to make sure the host is disconnected before proceeding..."
        start-sleep 240
        $hoststat = (Get-VMHost -Name $ESXi.Name)
        While ($hoststat.ConnectionState -eq "NotResponding") {
        Write-host "Host is still rebooting... waiting 10sec..."
        Start-Sleep 10
        $hoststat = (Get-VMHost -Name $ESXi.Name)
       }
        
    # Host uit MM
        write-host "Reboot on host $($esxi) is completed..."
        write-host ""
        Start-Sleep 20
        Set-VMHost -VMHost $ESXi -State Connected -Confirm:$False | Out-Null
   }  
    
    write-host "Done on host $($esxi)"
    write-host ""
}

#Disconnect vCenter
    write-host "Disconnecting vCenter $vCenter"
    Disconnect-VIServer  -Confirm:$False  | Out-Null

Recieve VMware Aria Operations Alerts in Microsoft Teams

This blog is the follow-up to the one I wrote earlier this week about sending VMware Aria Operations for Logs alerts to Microsoft (MS) Teams. This article describes the steps to forward alerts from VMware Aria Operations (Aria Ops) to MS Teams.

Use Case – Increase the ability to notice prio 1 alerts outside of office hours with the available technical resources.

Goal – In addition to the standard Aria Ops alerts, we also want to have the option available to receive alerts through Microsoft Teams.

Solution – Use Aria Ops Webhook to send alerts to MS Teams

Setup – In order to have Aria Ops alerts sent to MS Teams, we need to set up two things.

  1. Setup a MS Teams Connector to receive alerts
  2. Setup the Aria Ops Webhook configuration to push alerts

Setup a MS Teams Connector to receive alerts

First, decide in which Teams Channel you want to receive the Aria Ops alerts or add a new Teams Channel. I have created a new Channel called VRMware VMware Alerts.

Click on the 3 dots on the right side and select Manage Channel.

Select Edit under Connectors.

Select Incoming Webhook and hit the Configure button.

Provide a friendly name, upload an image and create the connector.

After creation copy the url to the clipboard. We need this URL later to configure the AO4L Webhook.

Before we move on to Aria Ops we need to enable the channel notifications. Click once again on the 3 dots on the right side and select Channel notifications > All New Posts.

Setting up Aria Ops Webhook configuration to push alerts

Go to Configure > Alerts > open Outbound Settings > Add > Plugin Type > Webhook Notification Plugin. Choose a Instance name. Copy the Webhook URL that was copied from MS Teams connector. Save, Test option does not work.

Webload Payload

Go to Configure > Alerts > Payload Templates > Add > Create Payload Template > Details. Choose a frienly name and select the Outbound Method.

On the next tab, Object Content we select Host System and Hardware|Model as property. This is just an example. Choose what you like to monitor.

On the next tab, Payload Details we add the Payload code.

The Payload code that is used in the Payload Details tab.

{
"text": "<b> ${ALERT_CRITICALITY} alert on ${RESOURCE_NAME}: ${ALERT_DEFINITION} at $(CREATE_TIME)</b>"
}

After completing the Webhook configuration we want test the Webhook configuration.

Create an alert, in our test case we use alert “Host has lost connection to vCenter Server“. Then we select the Outbound method “Webhook Notifcation Plugin” “VRMware MS-Teams“.

Next step is select the Payload Template.

Finally, we are almost there to send a test alert. We do this on the Test Notifcation tab. Hit the “Initiate Process” button.

We select the Alert Definitions “Host has lost connection to vCenter Server“. We also use the filter the alert definition… option.

Select a host and validate the configuration.

Now we can close the alert in Aira Ops. In Teams we have received the alert.

I hope this blog post will help you configure Aria Operations to send notifications to MS Teams. Please remember that MS Teams is not a monitoring tool. So be selective with the alerts you forward.

The payload code is based on the one that I found in this blog post from Brock Peterson.