Save to My DOJO
Table of contents
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
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 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 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.
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.
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 n corresponds to the nth object in the array.
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.
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″]
Not a DOJO Member yet?
Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!
7 thoughts on "How to find orphaned vSphere VMs using PowerCLI"
Hello
thank for your script
Is it possible with multiple Datastore?
Regards
Yvan
Hi Yvan,
As it is, no, it can handle only one datastore at a time. However, it shouldn’t be that difficult to modify it to have it loop across all available datastores.
regards
Jason
Hi
Thanks for your answer
may help me for the loop 🙂
Yvan