Sorting bits into bytes...

vSAN Unassociated Objects (2/2)

In my previous blog post, which you can find here, I talked about vSAN unassociated objects, explaining what they are and how objects can become unassociated. The reason behind both that blog and this one is a common challenge faced by many enterprise organizations, just like my client’s situation. They manage multiple vSAN clusters, and one of those clusters was running low on disk space. They were considering buying and adding more disks to the ESXi hosts as a solution.

But before going down the path of adding more disks, I decided to check if there were any unassociated objects in the cluster. To my surprise, there were nearly 1700 unassociated objects. This specific vSAN cluster was being used with Horizon, and changes made in Horizon seemed to have caused a large number of unassociated objects. However, not all of them could be attributed to Horizon changes. As I explained in my previous article, there are various other factors that can lead to such a high number of unassociated objects.

In this blog post, I want to explain how I tackled those 1700 unassociated objects. I’ll go through how my PowerShell script works to make this process easy and repeatable. Before we dive into the details of the script, I want to make a crucial point: using the script incorrectly can result in data corruption or even major system outages. This script is for educational purposes only, and I do not take responsibility for any issues that may arise from its use.


Before we get into the technical aspects of the script, I’d like to take a moment to thank Vladimir Behr (aka Jacques Behr, Lazy Admin), the owner of His profile can be found here. His assistance in utilizing AI technology made it much easier to create this script. So, once again, thank you, Vladimir!


Now, let’s walk through the script and explain how it works.


**Key Features:**

1. **Query and Deletion:** The script allows you to query and potentially delete these unassociated objects, freeing up storage resources and maintaining the efficiency of your vSAN.

2. **User Interaction:** The script offers user interaction for each unassociated object, ensuring careful consideration before deletion. You’ll be prompted to confirm each deletion, preventing accidental data loss.

3. **Logging:** The script logs all actions, providing a record of deleted or skipped objects for auditing and tracking purposes.

**How It Works:**

1. **Connection to vCenter:** The script begins by connecting to your vCenter server to access the vSAN cluster.

2. **Identification of Unassociated Objects:** It identifies unassociated objects within the vSAN cluster, collecting essential details such as the object’s UUID, description, and more.

3. **User Confirmation:** For each unassociated object found, the script prompts you to decide whether to delete it. You have the option to use force for deletion if required.

4. **Deletion Process:** If you choose to delete an object, the script initiates the deletion process, ensuring that you have full control over the operation.

5. **Logging:** Throughout the process, the script maintains a log of actions, including deleted objects, skipped objects, and any potential issues encountered.


– Efficiently manages unassociated objects, preventing unnecessary storage consumption.
– Provides control and user interaction to avoid accidental data loss.
– Offers a record of actions for tracking and auditing purposes.

**Note:** It’s important to exercise caution when using this script, as improper usage can lead to data loss or system issues. Use it responsibly and for learning purposes only.

By utilizing this script, you can effectively maintain and optimize your vSAN environment, ensuring that it operates smoothly and efficiently.


Script: vSAN_UnObj (vSAN Unassociated Objects)
Version: 1.0 (Tested)
Date: Aug 23, 2023
Author: Kabir Ali -
Description: Performs operations related to vSAN unassociated objects, including querying and deleting them.

Version history:
1.0 - Aug 23 - Initial version

.\vSAN_UnObj.ps1 -vCenter "vCenter@local.domain" -ClusterName "Prod-vSAN"

Param (
    [Parameter(Mandatory = $true)][string]$vCenter,
    [Parameter(Mandatory = $true)][string]$ClusterName

# Function to retrieve vSAN objects that are not associated with any virtual machines
function Get-UnusedVsanObjects {
    param (
        [string]$clusterMoRef # Managed Object Reference (MoRef) of the vSAN cluster

    # Get the vSAN cluster object system
    $vsanClusterObjectSys = Get-VsanView -Id VsanObjectSystem-vsan-cluster-object-system
    $results = ($vsanClusterObjectSys.VsanQueryObjectIdentities($clusterMoRef, $null, $null, $true, $true, $false)).Identities | Where-Object { $_.Vm -eq $null }
    # Return the result of the vSAN Cluster Objects
    return $results

# Function to delete a vSAN object based on its UUID, with user prompts and error handling
function Remove-UnusedVsanObjects {
    param (
        [string]$del_uuid # UUID of the vSAN object to delete
    # Attempt to delete the vSAN object using the vSAN Internal System
    $del_result = ($vsanIntSys.DeleteVsanObjects($result_uuid, $false))
    if ($del_result.Success -eq "true") {
        # If deletion was successful, log it as "Deleted"
        Write-log -vsanObj $vsanObj_item -ObjAction "Deleted"
        $result = "Deleted"
    if ($del_result.Success -ne "true") {
        # If deletion fails, prompt the user for action
        $user_delResponse = Read-Host -prompt "Deleting the vSAN Object failed. Use force to delete? (Y/N)"
        $user_delResponse = $user_delResponse.ToLower()
        if ($user_delResponse -eq "y") {
            # If the user chooses to use force, attempt to delete again with force
            $del_result = ($vsanIntSys.DeleteVsanObjects($result_uuid, $true))
            Write-log -vsanObj $vsanObj_item -ObjAction "$($del_result.FailureReason.Message)"
            $result = "Failed to delete"
        else {
            # If the user chooses not to use force, log it as "Skipped"
            Write-log -vsanObj $vsanObj_item -ObjAction "Skipped"
            $result = "Skipped"
    # Return the result of the deletion action (e.g., "Deleted," "Failed to delete," or "Skipped")
    return $result

# Function to get details of a vSAN object using its UUID
function Get-ObjDetails {
    param (
        [string]$result_uuid # UUID of the vSAN object to retrieve details for
    # Retrieve extended attributes of the vSAN object using the vSAN Internal System
    $ObjDetailJson = ($vsanIntSys.GetVsanObjExtAttrs($result_uuid)) | ConvertFrom-JSON
    # Extract specific attributes from the JSON response
    $ObjPath = $ObjDetailJson.PSObject.Properties.Value.'Object Path'
    $UFriendly = $ObjDetailJson.PSObject.Properties.Value.'User friendly name'
    # Store the extracted attributes in an array for return
    $result = @($ObjPath, $UFriendly)
    # Return an array containing the vSAN object's path and user-friendly name
    return $result

# Function to get the fault domain ID for a vSAN object
function Get-faultDomainId {
    param (
        [string]$result_uuid # UUID of the vSAN object to retrieve fault domain ID for

    # Create a CmmdsQuery object and set its type property
    $query = New-Object VMware.Vim.HostVsanInternalSystemCmmdsQuery
    $query.Uuid = $result_uuid
    # Explicitly create an array of the correct type and add the query to it
    $queries = [VMware.Vim.HostVsanInternalSystemCmmdsQuery[]]@($query)
    # Query the vSAN Internal System's CMMDS (Cluster Membership and Management Datastore) for object ownership information
    $QueryCmmds = $vsanIntSys.QueryCmmds($queries)
    # Parse the JSON response to extract the fault domain ID (selecting the first owner if multiple are found)
    $jsonData = ConvertFrom-Json $QueryCmmds
    $faultDomainId = $jsonData.result.owner | Select-Object -First 1

    # Return the fault domain ID of the vSAN object owner
    return $faultDomainId 

# Function to get the hostname based on the fault domain ID
function Get-UuidOwner {
    param (
        [string]$faultDomainId # Fault domain ID for which to retrieve the hostname

    # Create a CmmdsQuery object and set its type property
    $query = New-Object VMware.Vim.HostVsanInternalSystemCmmdsQuery
    $query.Uuid = $faultDomainId
    # Explicitly create an array of the correct type and add the query to it
    $queries = [VMware.Vim.HostVsanInternalSystemCmmdsQuery[]]@($query)
    # Query the vSAN Internal System's CMMDS (Cluster Membership and Management Datastore) for hostname information
    $OwnerHostname = $vsanIntSys.QueryCmmds($queries)
    # Parse the JSON response to extract the hostname (selecting the first hostname if multiple are found)
    $jsonData = ConvertFrom-Json $OwnerHostname
    $Owner = $jsonData.result.content.hostname | Select-Object -First 1

    # Return the hostname based on the provided fault domain ID
    return $Owner 

# Function for logging
function Write-log ($vsanObj, $ObjAction) {
    # Create an array for logging data and add a new object with specified properties
    [array]$logging += New-Object PSObject -Property @{
        "vSAN Friendly Name" = $vsanObj_item.'vSAN Friendly Name'
        "vSAN Description" = $vsanObj_item.'vSAN Description'
        "vSAN Unassociated Object UUID" = $vsanObj_item.'vSAN UUID'
        "vSAN Path" = $vsanObj_item.'vSAN Object Path'
        "vSAN UUID Owner" = $vsanObj_item.'vSAN UUID Owner'
        "vSAN Fault Domain ID" = $vsanObj_item.'vSAN Fault Domain ID'
        "Action" = $ObjAction
    # Export the logging data to a CSV file
    $logging | Export-csv C:\temp\vSANUnObj.csv -NoTypeInformation -NoClobber -Append

try {
    # Connect to vCenter
    Connect-VIServer -Server $vCenter
Catch {
    # Print error and stop script
    Write-Warning -Message "Error: Failed to connect to the vCenter. Stopping script."
try {
    # Check cluster
    $clusterView = Get-Cluster -Name $clusterName
Catch {
    Write-Warning -Message "Error: Could not find cluster. Stopping script."

# Get the Managed Object Reference (MoRef) of the vSAN cluster from the cluster view
$ClusterMoRef = $ClusterView.ExtensionData.MoRef

# Retrieve the first VM host from the cluster view and select it
$vmhost = ($clusterView | Get-VMHost) | Select-Object -First 1

# Get the vSAN Internal System view using the VM host's configManager property
$vsanIntSys = Get-View $vmhost.ExtensionData.configManager.vsanInternalSystem

# Create an empty array to store the unassociated vSAN objects
[array] $unusedVsanObjects = @()

# Populate the $unusedVsanObjects array by calling the Get-UnusedVsanObjects function
# and passing the clusterMoRef and vsanIntSys as parameters
$unusedVsanObjects = Get-UnusedVsanObjects -clusterMoRef $ClusterMoRef -vsanIntSys $vsanIntSys

# Create an empty array to store details about the unassociated vSAN objects
[array] $vsanObjDetails = @()

# Loop through each unassociated vSAN object in $unusedVsanObjects
foreach ($vsanObj in $unusedVsanObjects) {
    # Retrieve the fault domain ID for the vSAN object
    $ObjfaultDomainId = Get-faultDomainId -result_uuid $vsanObj.Uuid

    # Retrieve the UUID owner (hostname) for the fault domain ID
    $ObjOwner = Get-UuidOwner -faultDomainId $ObjfaultDomainId

    # Retrieve the object path from extended attributes using Get-ObjDetails function
    $ObjObjPath = (Get-ObjDetails -result_uuid $vsanObj.Uuid)[0]

    # Retrieve the user-friendly name from extended attributes using Get-ObjDetails function
    $ObjFriendly = (Get-ObjDetails -result_uuid $vsanObj.Uuid)[1]

    # Create a PSObject with properties for each vSAN object detail
    $vsanObjDetails += New-Object PSObject -Property @{
        "vSAN UUID" = $vsanObj.Uuid
        "vSAN Description" = $vsanObj.Description
        "vSAN Object Path" = $ObjObjPath
        "vSAN Friendly Name" = $ObjFriendly
        "vSAN Fault Domain ID" = $ObjfaultDomainId
        "vSAN UUID Owner" = $ObjOwner

# Get the total count of unassociated vSAN objects in the $vsanObjDetails array
$vsanObjDetailsTotal = $vsanObjDetails.Count

# Display total unassociated objects
Write-Output "Found a total of $($vsanObjDetails.Count) unassociated objects"
Sleep 3

# Initialize a counter to keep track of the processed vSAN objects
[int]$counter = 0

# Iterate through each vSAN object in $vsanObjDetails
foreach ($vsanObj_item in $vsanObjDetails) {
    # Increment the counter
    $counter = $counter + 1
    # Clear the console screen for a clean display
    # Output the current progress, showing the count and total number of vSAN objects
    Write-Output "$($counter)/$($vsanObjDetailsTotal)"
    # Display a message to indicate that the script is working on a vSAN object
    Write-Host "Working on:"
    # Iterate through each property of the vSAN object and display its name and value
    foreach ($property in $vsanObj_item.PSObject.Properties) {
        Write-Host "$($property.Name): $($property.Value)"
    # Prompt the user to confirm whether to delete the current vSAN object
    $user_objResponse = Read-Host "Delete this vSAN Object? (Y/N)"
    $user_objResponse = $user_objResponse.ToLower()
    # Check the user's response and take action accordingly
    if ($user_objResponse -eq "y") {
        # If the user confirms deletion, call the Remove-UnusedVsanObjects function
        Remove-UnusedVsanObjects -del_uuid $vsanObj_item.'vSAN UUID'
    else {
        # If the user skips deletion, log the action as "Skipped"
        Write-log -vsanObj $vsanObj_item -ObjAction "Skipped"
    # Pause execution for 2 seconds to provide a brief delay
    Sleep 2

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


Here’s an example of what you’d see when running the script:


From the script’s output, you can gather the following information:

– vSAN Friendly Name: Typically the VM’s name.
– vSAN Fault Domain ID: An internal vSAN identifier.
– vSAN UUID: An internal identifier for the unassociated object.
– vSAN Description: A description of the unassociated object.
– vSAN Object Path: The path to the unassociated object.
– vSAN UUID Owner: The hostname of the unassociated object owner. This is useful if you intend to run the objtool on the host.

For each unassociated object, the user is asked for input; there’s no “delete all” option as each object should be investigated individually. If you do wish to “delete all,” you can comment out a few lines in the code. Which ones? Ask and I’ll tell you…

And then the result:

I hope this helps! Feel free to reach out to me if there are any questions.

Leave a Reply