How to find orphaned vSphere VMs using PowerCLI

 

 

 

In this post, I covered how to rename a VM folder such that it matches that given to the VM which makes it easier to find the VM’s folder (or vice-versa) in datastore browser. As I had explained then, the issue is easily fixed by storage vMotioning the VM, a fact we can easily leverage using PowerCLI. This is particularly useful when you have a considerable number of mismatches to fix. I also hinted at the possibility of writing another PowerCLI script this time one that finds and fixes what are known as orphaned virtual machines. In short, an orphaned VM is one that has still exists in the vCenter Server database but is no longer recognized by the ESXi host where it was hosted.

Without further ado, let’s dive in today’s post which explores a PowerCLI based solution to this problem.

 

An Overview


The script I have in mind is loosely based on the following pseudo-code.

Connect to a vCenter Server or ESXi host
Generate a list of all registered vms and templates stored on a user chosen datastore
Generate a recursive list of folders and files on the datastore starting from root

 For each folder in the list
 Do
  If folder holds a vmx file then

   Truncate '.vmx' or '.vmtx' from the filename, returning a string possibly matching the owning vm's name
   Copy the vmx file to a local folder
   Parse the vmx file and extract the value set for displayName
   
   If displayName exists in the list of registered vms then
     registered=true else registered=false
   End if

   If displayName = truncated vmx file name then
     nameMatch = true else nameMatch=false  
   End if 

   Write results to console and to a logfile

  End if
 End do
Figure 1 - The <em>displayName</em> property as listed in a vmx file

Figure 1 – The displayName property as listed in a vmx file

Testing …


I’m using the environment shown in Figure 2 to test the script as I put it together. There are 5 VMs in total present on the same datastore against which I’ll be running the script. Two of the VMs, Pluto and Saturn are correctly registered and have matching folder names. A third VM called No Dice is correctly registered as well but its folder is called Jupiter.  The Not Registered VM, as the name implies, is not registered in the inventory but it does have a matching folder name. Finally, there’s A which is registered template with a matching folder name inside another folder.  These are all cases I’ll be testing to verify if the script works properly.

Figure 2 - Test environment

Figure 2 – Test environment

 

Figure 3 shows a snippet of the finalized script’s output when it is run against the test environment. The color legend is as follows;

  • GREEN – Registered VMs with matching folder names.
  • RED – VMs with a mismatched folder name.
  • CYAN – Registered but orphaned VMs.

The information displayed under each column represent the following;

  • Type – Indicates whether the resource is a virtual machine {VM} or a template {Tmpl}.
  • VM Name – The resource name as displayed in vSphere client or programmatically.
  • VMX Filename – The .vmx or .vmtx file name copied over to local storage from the corresponding datastore folder.
  • Folder Path [DS Name] – The resource folder path on the datastore. The datastore name is omitted from the path but is included in the column name.
  • Match? – Indicates whether the resource name matches the name of the holding datastore folder.
  • Reg? – Indicates whether the resource is registered or not.
  • ESXi Host – Displays the host where the VM is registered (Works only for vms).

There probably are other test cases I might have missed, so if you’re testing this against larger environments give me a shout if you find any. Also note that VMs residing under nested folders will be marked as having a mismatched folder name even though the VM name matches.

Figure 3 - The finalized script's output

Figure 3 – The finalized script’s output

 

Figure 4 shows the generated output file. Though comma delimited, technically it is not a csv file but it can, regardless, be read with Excel or similar for filtering and what not.

Figure 4 - Output file generated by script

Figure 4 – Output file generated by script

 

Dissecting the script


The vars section

The first block of the script is reserved for variable declaration. The only part that needs some explaining is the following. It’s really more of a PowerShell thing but worth mentioning nevertheless.

$col1="{0,-3}"
...
$col7="{6,-16}"
$colSetup="$col1 $col2 $col3 $col4 $col5 $col6"

When write-host is used to re-direct a script’s output to screen, the -f parameter provides for a better screen layout. Here’s a simple example to demonstrate what I mean.

write-host ("{0,-15} {1,-15} {2,-10}" -f "Name","Surname","DOB")

Each pair of values enclosed in the curly brackets, represents a column number and the corresponding column width. The ‘-‘ character is used to left justify the output. Any values you want outputted to screen, must be put right after the -f parameter. Make sure that you have as many values as there are columns otherwise the cmdlet will throw an error.

Figure 5 - Using write-host -f to format output

Figure 5 – Using write-host -f to format output

 

The datastore browsing section

The first 2 lines of the code snippet shown next are used to retrieve details about the datastore specified by $DSName via a View object. The 3rd line, while seemingly redundant, is required to ensure that the datastore name returned is correct since it is case-sensitive when used with the Copy-DatastoreItem cmdlet.

$DSObj  = Get-Datastore -name $DSName
$DSView = get-view $DSObj.id
$DSName = $DSObj.name

The next block of code instantiates an object of type HostDatastoreBrowserSearchSpec. We use the method or task SearchDatastoreSubFolders to perform a search on the datastore starting from its root. The complete list of folders and files contained therein is represented by $searchRes.

$searchSpec = New-Object VMware.Vim.HostDatastoreBrowserSearchSpec
$DSBrowser = get-view $DSView.browser
$RootPath = ("[" + $DSView.summary.Name +"]") 
$searchRes = $DSBrowser.SearchDatastoreSubFolders($RootPath, $searchSpec)

Dumping the contents of $searchRes to screen gives you something similar to Figure 6. You can see the path to every folder found on the datastore and its content (yet more objects) listed under the File column. Since this is basically an array of objects, we can list the files under each folder simply by invoking $searches[n].files as shown in the bottom part of Fig. 6 where corresponds to the nth object in the array.

Figure 6 - Querying a File object for a list files under a folder

Figure 6 – Querying a File object for a list files under a folder

 

The loop block

The loop block consists of a foreach loop which inspects each object stored in $searchRes. The first few lines are reserved to variable declarations and some string manipulation. In particular, the next line of code is used to split the VM’s folder path so we can extract the VM folder name which we can use to compare values.

$VMFolder = (($folder.FolderPath.Split("]").trimstart())[1]).trimend('/')

This one, on the other hand, returns the path of the vmx file, if one exists.

$VMXFile = ($folder.file | where {$_.Path -like "*.vmx"}).Path

An if-then statement verifies that a vmx file has indeed been found. If true, the vmx file is copied over to a local folder specified by $VMXFolder using the Copy-DataStoreItem cmdlet. This can be a painfully slow process especially if you have a significant number of VMs. Also, as the data channel is encrypted, extra baggage slows down the file-copy process. I have not found a better way to extract the displayname value from the vmx file which is why I copy vmx files over to a local folder.

Once the vmx file is transferred, it is read and parsed to extract the displayName value which is saved in $owningVM. This value corresponds to the VM name listed in vSphere client irrespective of the folder name on the datastore. Using these two pieces of information, we can map a folder name to a VM and determine if the VM is registered in the inventory or not. This is accomplished as follows;

$owningVM = ((get-content -path ($VMXFolder + "/" + $VMXfile) -ErrorAction SilentlyContinue | 
   Where-Object {$_ -match "displayName"}).split(""""))[1]

if (($vms -contains $owningVM) -or ($templates -contains $owningVM)) {$registered = "Yes"} 
 else {$registered="No"; $col="Cyan"}

The rest of the code in the loop block takes care of formatting and displaying the output to screen as well as writing it to file using Out-File. Here’s one nifty trick you can use to neatly display values that exceeds the column width;

if ($owningVM.Length -ge 30) {$owningVM = (($owningVM)[0..26] -join "") + "..."}

In the above example, the string is truncated down to the first 27 characters which are then joined together using the -join operator with no defined separator. Remember that a string is essentially an object (in most languages) i.e. an array of characters. We append “…” as padding so it fits the column nicely as can be seen in Figure 7.

Figure 7 - Constraining output to the exact column width

Figure 7 – Constraining output to the exact column width

 

And here’s a video of the script in action …

 

Conclusion


So there you have it, one more compelling reason why you should learn PowerCLI. The script as it is, simply reports back on what it finds and does not take any remedial action. You can however easily adapt it to prompt users to add a VM back to the inventory and delete redundant folders.

Admittedly, there are probably shorter and smarter ways to achieve the same result however the whole idea, well most of it,  is to come up with examples that emphasize the versatility and flexibility provided by both PowerShell or PowerCLI, something I hopefully achieved through this script.

For further posts on PowerCLI, have a look at the complete list of articles you’ll find on this blog.

 

The Script


# find-unregistered-vm-folders.ps1 - A PowerCLI script that finds and lists folders associated with unregistered vms
# or templates on a datastore. The script also lists those datastore folders whose names do not match those of the
# owning vm or template. The script generates a comma delimited log file in addition to writing to console.
# by Jason Fenech (14/09/16)
#--------------------------------------------------------------------------------------------------------------------
# Un-comment the 2 lines below if running script using PowerShell (not PowerCLI)
#
# Import-Module VMware.VimAutomation.Core -ErrorAction SilentlyContinue
# Import-Module VMware.VimAutomation.Storage -ErrorAction SilentlyContinue

#--------------------------------------------------------------------------------------------------------------------
#Change as required
#--------------------------------------------------------------------------------------------------------------------
 $vCenter="xxx.xxx.xxx.xxx"
 $user="user"
 $pass="password"
 $DSName="datastore-name"
 $VMXFolder = "C:\vmx"
 $VMXLogFile = $VMXFolder + "\vms-on-" + $DSName + "-" + (get-date -Format ddMMyy-hhmmss) + ".csv"
 $horLine = "----------------------------------------------------------------------------------------------------------------------------------------"

#Pre-defined column widths for write-host -f statements
 $col1="{0,-3}"
 $col2="{1,-5}"
 $col3="{2,-30}"
 $col4="{3,-30}"
 $col5="{4,-40}"
 $col6="{5,-6}"
 $col7="{6,-4}"
 $col8="{7,-16}"
 $colSetup="$col1 $col2 $col3 $col4 $col5 $col6 $col7 $col8"
#--------------------------------------------------------------------------------------------------------------------

#Connect to vCenter Server
 try{
 Disconnect-VIServer -force -Confirm:$false -ErrorAction SilentlyContinue 
 Connect-VIServer -Server $vCenter -User $user -Password $pass -ErrorAction Stop | Out-Null
 }
 catch{
 Write-Host "Failed to connect to vCenter Server $vCenter"
 exit #Exit script on error 
 } 
#--------------------------------------------------------------------------------------------------------------------
 clear 

 #If datastore name is specified incorrectly by the user, terminate
  try {$DSObj = Get-Datastore -name $DSName -ErrorAction Stop} 
   catch {Write-Host "Invalid datastore name" ; exit}
 
 #Get datastore view using id
  $DSView = Get-View $DSObj.id
 
 #Name is case-sensitive hence the need to retrieve the name even though specified by user
  $DSName = $DSObj.name
 
 #Fetch a list of folders and files present on the datastore
  $searchSpec = New-Object VMware.Vim.HostDatastoreBrowserSearchSpec
  $DSBrowser = get-view $DSView.browser
  $RootPath = ("[" + $DSView.summary.Name +"]")
  $searchRes = $DSBrowser.SearchDatastoreSubFolders($RootPath, $searchSpec)
 
 #Object Counter
  $s=0; 

 #Get a list of vms and templates residing on the datastore
  $vms=(get-vm * -Datastore $DSObj).Name
  $templates=(get-template * -Datastore $DSObj).Name 

 #Write header to log file
 ("#,Type,VM_Name,VMX_Filename,VM_Folder,Name_Match?,Is_VM_Registered,ESXi_Host") | 
   Out-File -FilePath $VMXLogFile -Append
 
 #Write table header row to console
  Write-Host "Browsing datastore $DSObj ...`n"
  Write-Host $horLine
  Write-Host ($colSetup -f "#", "Type", "VM Name", "VMX Filename", "Folder Path [$DSName]","Match?" , "Reg?", "ESXi Host")  -ForegroundColor white
  Write-Host $horLine
 
 #Recursively check every folder under the DS' root for vmx files.
  foreach ($folder in $searchRes)
  {
    $type = $null      #Template or vm?
    $VMXFile = $null   #Stores vmx/vmtx filename
    $registered = "No" #Is the vm registered?
    $nameMatch = "No"  #Does the folder name match that of the vm?
    $col = "Green"     #Default console color
 
    $DCName = $DSObj.Datacenter.Name
    $VMFolder = (($folder.FolderPath.Split("]").trimstart())[1]).trimend('/') 
    $VMXFile = ($folder.file | where {$_.Path -like "*.vmx" -or $_.Path -like "*.vmtx"}).Path  #vmtx is for templates
    $VMPath = ($DSName + "/" + $VMFolder)
    $fileToCopy = ("vmstore:/" + $DCName + "/" + $VMPath + "/" + $VMXFile)
 
 #Assuming vmx file exists ...
  if ($VMXFile -ne $null)
   {
    $s++
 
   #Extract VM name from the vmx file name. We will compare this to the value returned by displayName
    if ($VMXFile.contains(".vmx")){$prevVMName = $VMXFile.TrimEnd(".vmx"); $type="VM"} #VM
     elseif ($VMXFile.contains(".vmtx")){$prevVMName = $VMXFile.TrimEnd(".vmtx"); $type="Tmpl"} #Template
 
   #Copy vmx file to a local folder
    copy-DatastoreItem $fileToCopy $vmxFolder -ErrorAction SilentlyContinue
 
   #Extract the current vm name from the VMX file as well as the host name
    Try
    {
     $owningVM = ((get-content -path ($VMXFolder + "/" + $VMXfile) -ErrorAction SilentlyContinue | 
     Where-Object {$_ -match "displayName"}).split(""""))[1]
     
     if ( $type.equals("VM")){$vmHost = (Get-VM -Name $owningVM -ErrorAction SilentlyContinue).vmhost}
      else {$vmHost = (Get-template -Name $owningVM -ErrorAction SilentlyContinue).vmhost}
 
     if ($vmHost -eq $null) {$vmHost="n/a"}
    }
    Catch 
    {
     $owningVM="Error retrieving ..."
     $vmHost="Error ..."
    } 
 
  #If the vm specified in the VMX file is found in the list of vms or templates, mark it as registered
   if (($vms -contains $owningVM) -or ($templates -contains $owningVM)) {$registered = "Yes"} else {$col="Red"}
 
  #Check folder name. Set $nameMatch to true if no conflict found
   if ($prevVMName.equals($owningVM) -and $prevVMName.equals($VMFolder)){$nameMatch="Yes"} else {$col="Red"};
 
  #Highlight unregistered vms in cyan
   if ($registered.Equals("No")){$col="Cyan"}
 
 #Update Logfile
   ($s.ToString() + "," + $type + "," + $owningVM + "," + $VMXFile + "," + $VMFolder + "," + $nameMatch + "," + $registered + "," + $vmHost) | 
    Out-File -FilePath $VMXLogFile -Append 
 
 #Truncate strings if they do not fit the respective coloumn width
  if ($owningVM.Length -ge 30) {$owningVM = (($owningVM)[0..26] -join "") + "..."}
  if ($VMXfile.Length -ge 30) {$VMXfile = (($VMXfile)[0..26] -join "") + "..."}
  if ($VMFolder.Length -ge 40) {$VMFolder = (($VMFolder)[0..36] -join "") + "..."}
 
 #Write to console
  write-host ($colSetup -f $s.ToString() , $type , $owningVM , $VMXFile, $VMFolder, $nameMatch, $registered, $vmHost) -ForegroundColor $col
   } 
  }
 
 Write-Host $horLine
 Disconnect-VIServer -force -Confirm:$false -ErrorAction SilentlyContinue 

 

[the_ad id=”4738″][the_ad id=”4796″]

Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

3 thoughts on "How to find orphaned vSphere VMs using PowerCLI"

Leave a comment

Your email address will not be published. Required fields are marked *