There might be a few flaws in your code.
You are creating a temp file for each VMX file that is found, you should only need one since it is overwritten for each VMX.
And you can remove that Temp file at the end of the loop.
I used a test to determine if the VMX file contained a DisplayName entry, that is not always the case.
You left that test out, so you can end up with an empty string in the $displayName variable.
I don't think there is a .lck file when a VM is powered off.
To have an account with the privileges "read-only + datastore.browse privileges" depends on the permissions you placed on your Datastores and VMs.
I would start with a read-only account and check which missing privileges are reported, and continue till all privileges are there.
Also note that PowerCLI requires some rather basic privileges to read the inventory.
Original Message:
Sent: Dec 18, 2024 02:25 PM
From: dbutch1976
Subject: Create a report of all unregistered VMs
Hi LucD,
With your help I think I have created a thing of beauty. I changed your script a little to only check the .vmx file for a different displayname in the event that it actually finds a matching vmx.lck file, I believe this makes the script run faster. There's just one thing that can be done to improve the script at this point, make the script capable of running with only read-only + datastore.browse privileges. Any idea if/how this could be done? Otherwise, is would "Datastore.Create" and "Datastore.Delete" be required? This script is borderline magic, tested and working with VMFS and VSAN datastores and probably any other type:
$Datastores = Get-Datastore | Where-Object {$_.name -notmatch "local" -and $_.name -notmatch "NFS"}$unregistered = @()ForEach ($datastore in $datastores) { $psds = Get-Datastore -Name $datastore New-PSDrive -Name TgtDS -Location $psds -PSProvider VimDatastore -Root '\' | Out-Null $VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -notmatch "vCLS-*" -and $_.name -notmatch "zerto*"} #$VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -eq "unregistered_IOMEGA1.vmx"} Write-Host "Starting $datastore" foreach ($VMX in $VMXS) { try { Get-VM -datastore $datastore -name $VMX.name.replace('.vmx','') -ErrorAction:Stop | Out-Null } catch { $VMname = $VMX.name.replace('.vmx','') $mismatch = Get-ChildItem -Path $VMX.PSParentpath -Filter *.lck | Where-Object {$_.name -contains "$($vmx.name).lck"} if ($mismatch -ne $null) { $tempFile = New-TemporaryFile Copy-DatastoreItem -Item $VMX.FullName -Destination $tempFile $displayname = Get-Content -Path $tempFile | where{$_ -match 'displayName'} $vmxfilename = $displayName.Split('"')[1] $result = "NAME MISMATCH - Powered on and running as $vmxfilename." Write-host "$VMname is running using a different name. Result-MISMATCH." } else{ write-host "$VMname is not found on $global:DefaultVIServers.name and is not running. Result-UNREGISTERED." $result = "UNREGISTERED." } $unregistered += [PSCustomObject] @{ Name = $vmx.Name DatastoreFullPath = $vmx.DatastoreFullPath LastWriteTime = $vmx.LastWriteTime Result = $result } } } Write-Host "Done" Remove-PSDrive -Name TgtDS}$unregistered | export-csv -Path C:\output\UnregisteredVms3.csv -NoTypeInformation
Sample output:
Name | DatastoreFullPath | LastWriteTime | Result |
unregistered_IOMEGA2.vmx | [IOMEGA] unregistered_IOMEGA2/unregistered_IOMEGA2.vmx | 2024-12-12 14:02 | UNREGISTERED. |
unregistered_IOMEGA3.vmx | [IOMEGA] unregistered_IOMEGA3/unregistered_IOMEGA3.vmx | 2024-12-12 14:03 | UNREGISTERED. |
unregistered_IOMEGA1.vmx | [IOMEGA] unregistered_IOMEGA1/unregistered_IOMEGA1.vmx | 2024-12-17 18:47 | NAME MISMATCH - Powered on and running as Unmatched_IOMEGA1. |
unregistered_VSAN2.vmx | [vsanDatastore] e6335b67-9262-facd-fedc-48210b505bbb/unregistered_VSAN2.vmx | 2024-12-12 14:05 | UNREGISTERED. |
vc7b.vmx | [vsanDatastore] 686d1a67-1204-6504-bcbc-48210b506c23/vc7b.vmx | 2024-11-05 14:48 | UNREGISTERED. |
vc7a.vmx | [vsanDatastore] 8e611a67-5c6b-ae15-634c-48210b506c23/vc7a.vmx | 2024-11-05 14:35 | UNREGISTERED. |
Deletemetest1.vmx | [vsanDatastore] 7cd74467-9424-8307-af2a-48210b506c23/Deletemetest1.vmx | 2024-11-25 15:01 | UNREGISTERED. |
unregistered_VSAN3.vmx | [vsanDatastore] 03355b67-4c56-4145-73f1-48210b505bbb/unregistered_VSAN3.vmx | 2024-12-12 14:09 | UNREGISTERED. |
unregistered_VSAN1.vmx | [vsanDatastore] 7f335b67-f9b3-5722-bf2e-8d07950ebe8e/unregistered_VSAN1.vmx | 2024-12-17 18:48 | NAME MISMATCH - Powered on and running as Unmatched_VSAN1. |
Original Message:
Sent: Dec 18, 2024 08:06 AM
From: LucD
Subject: Create a report of all unregistered VMs
You will have to read the content of the VMX file to get the DisplayName.
Something like this
$Datastores = Get-Datastore | Where-Object { $_.name -notmatch "local" -and $_.name -notmatch "NFS" }#$Datastores = Get-Datastore -name IOMEGA$unregistered = @()ForEach ($datastore in $datastores) { $psds = Get-Datastore -Name $datastore New-PSDrive -Name TgtDS -Location $psds -PSProvider VimDatastore -Root '\' | Out-Null $VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object { $_.name -notmatch "vCLS-*" -and $_.name -notmatch "zerto*" } #$VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -eq "unregistered_IOMEGA1.vmx"} Write-Host "Starting $datastore" $tempFile = New-TemporaryFile foreach ($VMX in $VMXS) { try { # Check for DisplayName Copy-DatastoreItem -Item $VMX.FullName -Destination $tempFile $displayName = Get-Content -Path $tempFile | where{$_ -match 'displayName'} if($displayName){ $vmName = $displayName.Split('"')[1] } else{ $vmName = $VMX.name.replace('.vmx', '') } Write-Host "Checking if VM $vmName is registered" Get-VM -Datastore $datastore -Name $vmName -ErrorAction:Stop | Out-Null } catch { $mismatch = Get-ChildItem -Path $VMX.PSParentpath -Filter *.lck | Where-Object { $_.name -contains "$($vmx.name).lck" } $unregistered += [PSCustomObject] @{ Name = $vmx.Name DisplayName = $vmName DatastoreFullPath = $vmx.DatastoreFullPath LastWriteTime = $vmx.LastWriteTime vCNameMismatch = $mismatch.Name } } } Write-Host "Done" Remove-Item -Path $tempFile Remove-PSDrive -Name TgtDS}$unregistered | Export-Csv -Path C:\output\UnregisteredVms2.csv -NoTypeInformation
------------------------------
Blog: lucd.info Twitter: @LucD22 Co-author PowerCLI Reference
Original Message:
Sent: Dec 18, 2024 04:47 AM
From: dbutch1976
Subject: Create a report of all unregistered VMs
That worked perfectly, thanks LucD. Now that I have a way of detecting a false positive, is there a way to then get the correct name from the .vmx file and add that to a variable? I have found the correct name of the VM within the .vmx file under "displayName".
Current script:
$Datastores = Get-Datastore | Where-Object {$_.name -notmatch "local" -and $_.name -notmatch "NFS"}#$Datastores = Get-Datastore -name IOMEGA$unregistered = @()ForEach ($datastore in $datastores) { $psds = Get-Datastore -Name $datastore New-PSDrive -Name TgtDS -Location $psds -PSProvider VimDatastore -Root '\' | Out-Null $VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -notmatch "vCLS-*" -and $_.name -notmatch "zerto*"} #$VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -eq "unregistered_IOMEGA1.vmx"} Write-Host "Starting $datastore" foreach ($VMX in $VMXS) { try { Get-VM -datastore $datastore -name $VMX.name.replace('.vmx','') -ErrorAction:Stop | Out-Null } catch { $mismatch = Get-ChildItem -Path $VMX.PSParentpath -Filter *.lck | Where-Object {$_.name -contains "$($vmx.name).lck"} $unregistered += [PSCustomObject] @{ Name = $vmx.Name DatastoreFullPath = $vmx.DatastoreFullPath LastWriteTime = $vmx.LastWriteTime vCNameMismatch = $mismatch.Name } } } Write-Host "Done" Remove-PSDrive -Name TgtDS}$unregistered | export-csv -Path C:\output\UnregisteredVms2.csv -NoTypeInformation
Output:
Name | DatastoreFullPath | LastWriteTime | vCNameMismatch |
unregistered_IOMEGA1.vmx | [IOMEGA] unregistered_IOMEGA1/unregistered_IOMEGA1.vmx | 2024-12-17 18:47 | unregistered_IOMEGA1.vmx.lck |
unregistered_IOMEGA2.vmx | [IOMEGA] unregistered_IOMEGA2/unregistered_IOMEGA2.vmx | 2024-12-12 14:02 | |
unregistered_VSAN3.vmx | [vsanDatastore] 03355b67-4c56-4145-73f1-48210b505bbb/unregistered_VSAN3.vmx | 2024-12-12 14:09 | |
unregistered_VSAN1.vmx | [vsanDatastore] 7f335b67-f9b3-5722-bf2e-8d07950ebe8e/unregistered_VSAN1.vmx | 2024-12-17 18:48 | unregistered_VSAN1.vmx.lck |
Original Message:
Sent: Dec 18, 2024 03:43 AM
From: LucD
Subject: Create a report of all unregistered VMs
Try with
$mismatch = Get-ChildItem -Path $VMX.PSParentpath -Filter *.lck | Where-Object {$_.name -contains "$($vmx.name).lck"}
------------------------------
Blog: lucd.info Twitter: @LucD22 Co-author PowerCLI Reference
Original Message:
Sent: Dec 17, 2024 07:14 PM
From: dbutch1976
Subject: Create a report of all unregistered VMs
I feel like I'm close on this but I'm stuck. I know exactly where the error is I think:
$Datastores = Get-Datastore | Where-Object {$_.name -notmatch "local" -and $_.name -notmatch "NFS"}#$Datastores = Get-Datastore -name IOMEGA$unregistered = @()ForEach ($datastore in $datastores) { $psds = Get-Datastore -Name $datastore New-PSDrive -Name TgtDS -Location $psds -PSProvider VimDatastore -Root '\' | Out-Null $VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -notmatch "vCLS-*" -and $_.name -notmatch "zerto*"} #$VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -eq "unregistered_IOMEGA1.vmx"} Write-Host "Starting $datastore" foreach ($VMX in $VMXS) { try { Get-VM -datastore $datastore -name $VMX.name.replace('.vmx','') -ErrorAction:Stop | Out-Null } catch { $mismatch = Get-ChildItem -Path $VMX.PSParentpath -Filter *.lck | Where-Object {$_.name -contains "$vmx.name.lck"} $unregistered += [PSCustomObject] @{ Name = $vmx.Name DatastoreFullPath = $vmx.DatastoreFullPath LastWriteTime = $vmx.LastWriteTime vCNameMismatch = $mismatch.Name } } } Write-Host "Done" Remove-PSDrive -Name TgtDS}$unregistered # | export-csv -Path C:\output\UnregisteredVms1.csv -NoTypeInformation
$mismatch = Get-ChildItem -Path $VMX.PSParentpath -Filter *.lck | Where-Object {$_.name -contains "$vmx.name.lck"}
How do I make the bold into $vmx.name + .lck as the extention? I've tried
"$vmx.name".lck
$vmx.name.lck
$vmx.name'.lck'
The goal is to simply add the vmx.lck file name to the output to simply indicate that the VM is powered on and running under a different name. I'll tackle figuring out the actual name from the .vmx file next.
Original Message:
Sent: Dec 17, 2024 10:16 AM
From: dbutch1976
Subject: Create a report of all unregistered VMs
Hi LucD,
The script is great, but I see a potential for false positives in the event that the VM is registered to the vCenter using a name which is different than the name as it appears in the datastore.
Is there a way to modify this line:
$VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -notmatch "vCLS-*"}
I would like to exclude all results in which there is an .nvram file located in the same folder as the .vmx file. Not sure how I can approach this, any suggestions?
*Update* after doing some additional testing, I can see that several legitimately unregistered VMs also have NVRAM files. Perhaps these were created when the VM was turned on, and are not deleted automatically. Perhaps I could check for the presence of a .vmx.lck file instead? (Although this has the disadvantage of only located powered on VMs, but it's better than nothing).
Any other suggestions would be appreciated.
Original Message:
Sent: Dec 13, 2024 04:32 PM
From: LucD
Subject: Create a report of all unregistered VMs
Look ok, great script
------------------------------
Blog: lucd.info Twitter: @LucD22 Co-author PowerCLI Reference
Original Message:
Sent: Dec 13, 2024 03:21 PM
From: dbutch1976
Subject: Create a report of all unregistered VMs
Took one final kick at this with the goal of checking all the datastores in the environment and only returning unregistered VMs. Looks like it's working and it's giving me the output I'm looking for. Changes where:
Removed $ErrorActionPreference = "Stop" since it is not required.
Removed $VMs = Get-VM since it's not required.
Added $VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -notmatch "vCLS-*"} In order to remove all vCLS VMs from the results.
Below is the final script:
Mind giving it a quick sanity check and suggesting any improvements?
$Datastores = get-datastore
$unregistered = @()
ForEach ($datastore in $datastores) {
$psds = Get-Datastore -Name $datastore
New-PSDrive -Name TgtDS -Location $psds -PSProvider VimDatastore -Root '\' | Out-Null
$VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx | Where-Object {$_.name -notmatch "vCLS-*"}
foreach ($VMX in $VMXS) {
try {
Get-VM -datastore $datastore -name $VMX.name.replace('.vmx','') -ErrorAction:Stop | Out-Null
}
catch {
$unregistered += [PSCustomObject] @{
Name = $vmx.Name
DatastoreFullPath = $vmx.DatastoreFullPath
LastWriteTime = $vmx.LastWriteTime
}
}
}
Remove-PSDrive -Name TgtDS
}
$unregistered | export-csv -Path C:\output\UnregisteredVms1.csv -NoTypeInformation
Output:
Name DatastoreFullPath LastWriteTime
---- ----------------- -------------
unregistered_IOMEGA1.vmx [IOMEGA] unregistered_IOMEGA1/unregistered_IOMEGA1.vmx 2024-12-12 2:01:48 PM
unregistered_IOMEGA2.vmx [IOMEGA] unregistered_IOMEGA2/unregistered_IOMEGA2.vmx 2024-12-12 2:02:34 PM
unregistered_IOMEGA3.vmx [IOMEGA] unregistered_IOMEGA3/unregistered_IOMEGA3.vmx 2024-12-12 2:03:00 PM
unregistered_VSAN2.vmx [vsanDatastore] e6335b67-9262-facd-fedc-48210b505bbb/unregistered_VSAN2.vmx 2024-12-12 2:05:11 PM
vc7b.vmx [vsanDatastore] 686d1a67-1204-6504-bcbc-48210b506c23/vc7b.vmx 2024-11-05 2:48:57 PM
vc7a.vmx [vsanDatastore] 8e611a67-5c6b-ae15-634c-48210b506c23/vc7a.vmx 2024-11-05 2:35:54 PM
Deletemetest1.vmx [vsanDatastore] 7cd74467-9424-8307-af2a-48210b506c23/Deletemetest1.vmx 2024-11-25 3:01:04 PM
unregistered_VSAN3.vmx [vsanDatastore] 03355b67-4c56-4145-73f1-48210b505bbb/unregistered_VSAN3.vmx 2024-12-12 2:09:57 PM
unregistered_VSAN1.vmx [vsanDatastore] 7f335b67-f9b3-5722-bf2e-8d07950ebe8e/unregistered_VSAN1.vmx 2024-12-12 2:03:31 PM
Original Message:
Sent: Dec 13, 2024 10:15 AM
From: LucD
Subject: Create a report of all unregistered VMs
Something like this
$ds = Get-Datastore -Name vsanDatastore$VMs = Get-VM#$ErrorActionPreference = "Stop"$unregistered = @()New-PSDrive -Name TgtDS -Location $ds -PSProvider VimDatastore -Root '\' | Out-Null$VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmxforeach ($VMX in $VMXS) { try { Get-VM -name $VMX.name.replace('.vmx','') -ErrorAction:Stop | Out-Null } catch { $unregistered += PSCustomObject] @{ Name = $vmx.Name DatastoreFullPath = $vmx.DatastoreFullPath LastWriteTime = $vmx.LastWriteTime } }}Remove-PSDrive -Name TgtDS$unregistered | Export-Csv -Path .\report.csv -NoTypeInformation
------------------------------
Blog: lucd.info Twitter: @LucD22 Co-author PowerCLI Reference
Original Message:
Sent: Dec 13, 2024 09:50 AM
From: dbutch1976
Subject: Create a report of all unregistered VMs
This is almost perfect:
$ds = Get-Datastore -Name vsanDatastore
$VMs = Get-VM
#$ErrorActionPreference = "Stop"
$unregistered = @()
New-PSDrive -Name TgtDS -Location $ds -PSProvider VimDatastore -Root '\' | Out-Null
$VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx
foreach ($VMX in $VMXS) {
try {
Get-VM -name $VMX.name.replace('.vmx','') -ErrorAction:Stop | Out-Null
}
catch {
$unregistered += $VMX.Name
}
}
Remove-PSDrive -Name TgtDS
$unregistered
I just need to format this data before exporting it to csv, I'm believe I need to add something like this, but I'm not sure where:
ForEach-Object {
[PSCustomObject] @{
Name = $vmx.Name
DatastoreFullPath = $vmx.DatastoreFullPath
LastWriteTime = $vmx.LastWriteTime
}
}
Original Message:
Sent: Dec 12, 2024 04:04 PM
From: dbutch1976
Subject: Create a report of all unregistered VMs
Thanks LucD,
This is getting me a complete list of all VMs, both registered and unregistered on the datastore. Can I use this to compare to a list of registered VMs and only give results for VMs it cannot find? I'm not sure how to trim the .vmx in the .name field below, after that I can add an if / else statement and only return results with Name,DatastoreFullPath,LastWriteTime
$ds = Get-Datastore -Name vsanDatastore
$VMs = Get-VM
New-PSDrive -Name TgtDS -Location $ds -PSProvider VimDatastore -Root '\' | Out-Null
$VMXS = Get-ChildItem -Path TgtDS: -Recurse -Filter *.vmx
foreach ($VMX in $VMXS) {
Get-VM -name $VMX.name(how do I trim the .vmx here?)
}
Remove-PSDrive -Name TgtDS
Original Message:
Sent: Dec 12, 2024 02:44 PM
From: LucD
Subject: Create a report of all unregistered VMs
There is a flaw in your code.
The object returned by Get-Datastore does not have a Datastore property.
So the Get-ChildItem is starting from an empty string, which explains why you see that local file.
You might try something like this
$ds = Get-Datastore -Name IOMEGANew-PSDrive -Name TgtDS -Location $ds -PSProvider VimDatastore -Root '\' | Out-NullGet-ChildItem -Path TgtDS: -Recurse -Filter *.vmxRemove-PSDrive -Name TgtDS
------------------------------
Blog: lucd.info Twitter: @LucD22 Co-author PowerCLI Reference
Original Message:
Sent: Dec 10, 2024 08:34 AM
From: dbutch1976
Subject: Create a report of all unregistered VMs
Hello,
I have found a large number of VMs which were not properly deleted from inventory and have instead been removed from inventory leaving behind many TB's of VMDKs. I would like to re-register these VMs (eventually) so that they can be put through our proper decom process.
I've found a few scripts to play around with, but many of these seem to focus on re-registering the VMs, when there are a large number of VMs I need to ensure are NOT re-registered, such as replicated VMs that should not be turned on.
For this reason I'm looking for a script I can run which simply focuses on generating a list of unregistered VMs at this point, along with relevant information such as the path to the VMX file, the last time the disk was modified. Once I've been able to generate this list in a CSV file I plan to exclude all the VMs until I get a list of only the VMs I want to re-register, then I'll import the CSV into another script and register only those VMs.
I've been playing around with this:
Get-Datastore -name IOMEGA | ForEach-Object { Get-ChildItem -Path $_.Datastore -Recurse | Where-Object { $_.Extension -eq ".vmx" -and ! (Get-VM -Name $_.BaseName -ErrorAction SilentlyContinue) }} | Select-Object *
However this isn't even working on the datastore I'm specifying (IOMEGA), instead I'm getting results from my local laptop (I don't even understand how it's finding these). Sample output from this command here:
PSPath : Microsoft.PowerShell.Core\FileSystem::C:\Users\dbutc\Documents\Virtual Machines\nest7\nest7.vmx
PSParentPath : Microsoft.PowerShell.Core\FileSystem::C:\Users\dbutc\Documents\Virtual Machines\nest7
PSChildName : nest7.vmx
PSDrive : C
PSProvider : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
Mode : -a----
VersionInfo : File: C:\Users\dbutc\Documents\Virtual Machines\nest7\nest7.vmx
InternalName:
OriginalFilename:
FileVersion:
FileDescription:
Product:
ProductVersion:
Debug: False
Patched: False
PreRelease: False
PrivateBuild: False
SpecialBuild: False
Language:
BaseName : nest7
Target : {}
LinkType :
Name : nest7.vmx
Length : 2761
DirectoryName : C:\Users\dbutc\Documents\Virtual Machines\nest7
Directory : C:\Users\dbutc\Documents\Virtual Machines\nest7
IsReadOnly : False
Exists : True
FullName : C:\Users\dbutc\Documents\Virtual Machines\nest7\nest7.vmx
Extension : .vmx
CreationTime : 2020-12-29 4:26:55 PM
CreationTimeUtc : 2020-12-29 9:26:55 PM
LastAccessTime : 2024-12-09 1:28:25 PM
LastAccessTimeUtc : 2024-12-09 6:28:25 PM
LastWriteTime : 2020-12-29 4:26:55 PM
LastWriteTimeUtc : 2020-12-29 9:26:55 PM
Attributes : Archive
How is it finding a result from my C: when I'm specifying at datastore presented to my esxi hosts?? Can this be refined to get me the information I'm looking for?