Cleanup Stale Files on VMWare Datastores – PowerCLI

In large VMWare environments, it’s quite common to have random files leftover on VMFS datastores. Sometimes this is explainable: maybe an admin removes a VM from inventory instead of deleting it from disk and then forgets about it. Other times I’ve seen sVmotion leave entire VMDK files behind after a migration or if a migration fails midway through. Regardless of how it happens, it can be impractical to manually clean this up in a large environment. In order to better identify these leftover disk hogs, I use a PowerCLI function to look for unused directories which I run each month and have the results emailed to myself for review. The function scans the top level directories on a datastore and will consider a directory stale if non of the files within have been modified in the past 30 days (default value)

Since this can take a very long time to run, it outputs what datastore/directory it is currently looking at so you know it isn’t hung up. The function will return an array of custom PS objects that contain the Directory Name with the name of the Datastore that directory lives on for each stale directory identified. It will NOT attempt to delete anything, it is simply for reporting. It works by creating a VimDatastore PSDrive to each datastore and then using the basic Get-ChildItem cmdlet to browse around the files/directories.

You can run it with no parameters and it will use 30 days as the default for StaleAfterDays and run against all datastores returned by Get-Datastore. Or you can ignore local disks by passing SkipNonSharedDatastores and if you are connected to a vCenter instance you can target a specific data center by passing the TargetDatacenter argument.

Get-StaleDatastoreDirs

function Get-StaleDatastoreDirs {
<#
.SYNOPSIS
Get a list of datastore directories where all files are inactive for more than $StaleAfterDays days 
.DESCRIPTION
Get-StaleDatastoreDirs traverses top level datastore directories looking for any directory where all
the files within haven't been written to for over $StaleAfterDays days.  The default is >30 days
.EXAMPLE
Get-StaleDatastoreDirs

DESCRIPTION
-----------
This command will run against all datastores and use a default value of 30 for $StaleAfterDays

.EXAMPLE
Get-StaleDatastoreDirs -StaleAfterDays 60 -TargetDatacenter "My Datacenter" -SkipNonSharedDatastores

DESCRIPTION
-----------
This command will run against datastores in the specified datacenter and return any that have over 60
days of inactivity.  It will also skip any non-shared datastores
#>

    [CmdletBinding()] param (
        [Parameter(Mandatory=$false)] [Int]$StaleAfterDays = 30,
        [Parameter(Mandatory=$false)] [Switch]$SkipNonSharedDatastores,
        [Parameter(Mandatory=$false)] [String]$TargetDatacenter
    )

    process {

        $current = Get-Date
        $listOfStaleDirs = @()
        
        try {
            if( $SkipNonSharedDatastores ) {
                if( $TargetDatacenter ) {
                    $dsList = Get-Datastore -Location (Get-Datacenter $TargetDatacenter -ErrorAction Stop) -ErrorAction Stop | ?{$_.ExtensionData.Summary.MultipleHostAccess}
                }
                else {
                    $dsList = Get-Datastore -ErrorAction Stop | ?{$_.ExtensionData.Summary.MultipleHostAccess}
                }
            }
            else {
                if( $TargetDatacenter ) {
                    $dsList = Get-Datastore -Location (Get-Datacenter $TargetDatacenter -ErrorAction Stop) -ErrorAction Stop
                }
                else {
                    $dsList = Get-Datastore -ErrorAction Stop
                }
            }
            $dsList = $dsList | sort Name
        }
        catch {
            Write-Host $_ -ForegroundColor Magenta
            return
        }
        
        foreach ($datastore in $dsList) {

            Write-Host "Scanning datastore:" $datastore.Name
            New-PSDrive -Location $datastore -Name ds -PSProvider VimDatastore -Root '\' | Out-Null
            
            [array]$dirs = Get-ChildItem -Path 'ds:\' | ?{$_.Name -ne ".vSphere-HA" -and $_.ItemType -eq "Folder"} | sort Name
            
            foreach ($dir in $dirs) {
                Write-Host "`tScanning directory:" $dir.Name
                $numOfStaleFiles = 0
                [array]$subfiles = Get-ChildItem -Path $dir.FullName -Recurse | ?{$_.ItemType -ne "Folder"}
                
                #check to see if current file has been written to in $staleAfterDays num of days
                foreach ($file in $subfiles) {
                    if ( ($current.Subtract($file.LastWriteTime)).TotalDays -gt $StaleAfterDays ) {
                        $numOfStaleFiles++
                    }
                }
                
                #if all files in directory haven't been written to in $staleAfterDays num of days
                if($subfiles.length -eq $numOfStaleFiles) {
                    Write-Host "`tFound directory stale for over $StaleAfterDays days:" $dir.Name -ForegroundColor Cyan
                    $custDirObj = New-Object PSObject -Property @{
                        DatastoreName = $datastore.Name
                        DirectoryName = $dir.Name
                    }
                    $listOfStaleDirs += $custDirObj
                }
            }
            Remove-PSDrive ds
        }

        return $listOfStaleDirs
    }
}