<#
***************************************************************************************************
VMWARE POWERCLI AND PURE STORAGE POWERSHELL SDK MUST BE INSTALLED ON THE MACHINE THIS IS RUNNING ON
***************************************************************************************************
For info, refer to www.codyhosterman.com
*******Disclaimer:******************************************************
This scripts are offered "as is" with no warranty. While this
scripts is tested and working in my environment, it is recommended that you test
this script in a test lab before using in a production environment. Everyone can
use the scripts/commands provided here without any written permission but I
will not be liable for any damage or loss to the system.
************************************************************************
This script will identify Pure Storage FlashArray volumes and issue UNMAP against them. The script uses the best practice
recommendation block count of 1% of the free capacity of the datastore. All operations are logged to a file and also
output to the screen. REST API calls to the array before and after UNMAP will report on how much (if any) space has been reclaimed.
This can be run directly from PowerCLI or from a standard PowerShell prompt. PowerCLI must be installed on the local host regardless.
Supports:
-PowerShell 3.0 or later
-Pure Storage PowerShell SDK 1.5 or later
-PowerCLI 6.3 Release 1+
-REST API 1.4 and later
-Purity 4.1 and later
-FlashArray 400 Series and //m
-vCenter 5.5 and later
-Each FlashArray datastore must be present to at least one ESXi version 5.5 or later host or it will not be reclaimed
#>
#Create log folder if non-existent
write-host "Please choose a directory to store the script log"
function ChooseFolder([string]$Message, [string]$InitialDirectory)
{
$app = New-Object -ComObject Shell.Application
$folder = $app.BrowseForFolder(0, $Message, 0, $InitialDirectory)
$selectedDirectory = $folder.Self.Path
return $selectedDirectory
}
$logfolder = ChooseFolder -Message "Please select a log file directory" -InitialDirectory 'MyComputer'
$logfile = $logfolder + '\' + (Get-Date -Format o |ForEach-Object {$_ -Replace ':', '.'}) + "unmapresults.txt"
write-host "Script result log can be found at $logfile" -ForegroundColor Green
#Configure optional Log Insight target
$useloginsight = read-host "Would you like to send the UNMAP results to a Log Insight instance (y/n)"
while (($useloginsight -ine "y") -and ($useloginsight -ine "n"))
{
write-host "Invalid entry, please enter y or n"
$useloginsight = read-host "Would you like to send the UNMAP results to a Log Insight instance (y/n)"
}
if ($useloginsight -ieq "y")
{
$loginsightserver = read-host "Enter in the FQDN or IP of Log Insight"
$loginsightagentID = read-host "Enter a Log Insight Agent ID"
add-content $logfile ('Results will be sent to the following Log Insight instance ' + $loginsightserver + ' with the UUID of ' + $loginsightagentID)
}
elseif ($useloginsight -ieq "n")
{
add-content $logfile ('Log Insight will not be used for external logging')
}
#Import PowerCLI. Requires PowerCLI version 6.3 or later. Will fail here if PowerCLI is not installed
if ( !(Get-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) ) {
if (Test-Path “C:\Program Files (x86)\VMware\Infrastructure\PowerCLI\Scripts\Initialize-PowerCLIEnvironment.ps1”)
{
. “C:\Program Files (x86)\VMware\Infrastructure\PowerCLI\Scripts\Initialize-PowerCLIEnvironment.ps1” |out-null
}
elseif (Test-Path “C:\Program Files (x86)\VMware\Infrastructure\vSphere PowerCLI\Scripts\Initialize-PowerCLIEnvironment.ps1”)
{
. “C:\Program Files (x86)\VMware\Infrastructure\vSphere PowerCLI\Scripts\Initialize-PowerCLIEnvironment.ps1” |out-null
}
if ( !(Get-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) )
{
write-host ("PowerCLI not found. Please verify installation and retry.") -BackgroundColor Red
write-host "Terminating Script" -BackgroundColor Red
add-content $logfile ("PowerCLI not found. Please verify installation and retry.")
add-content $logfile "Get it here: https://my.vmware.com/web/vmware/details?downloadGroup=PCLI650R1&productId=614"
add-content $logfile "Terminating Script"
return
}
}
#Set
Set-PowerCLIConfiguration -invalidcertificateaction "ignore" -confirm:$false |out-null
Set-PowerCLIConfiguration -Scope Session -WebOperationTimeoutSeconds -1 -confirm:$false |out-null
if ( !(Get-Module -ListAvailable -Name PureStoragePowerShellSDK -ErrorAction SilentlyContinue) ) {
write-host ("FlashArray PowerShell SDK not found. Please verify installation and retry.") -BackgroundColor Red
write-host "Get it here: https://github.com/PureStorage-Connect/PowerShellSDK"
write-host "Terminating Script" -BackgroundColor Red
add-content $logfile ("FlashArray PowerShell SDK not found. Please verify installation and retry.")
add-content $logfile "Get it here: https://github.com/PureStorage-Connect/PowerShellSDK"
add-content $logfile "Terminating Script"
return
}
write-host ' __________________________'
write-host ' /++++++++++++++++++++++++++\'
write-host ' /++++++++++++++++++++++++++++\'
write-host ' /++++++++++++++++++++++++++++++\'
write-host ' /++++++++++++++++++++++++++++++++\'
write-host ' /++++++++++++++++++++++++++++++++++\'
write-host ' /++++++++++++/----------\++++++++++++\'
write-host ' /++++++++++++/ \++++++++++++\'
write-host ' /++++++++++++/ \++++++++++++\'
write-host ' /++++++++++++/ \++++++++++++\'
write-host ' /++++++++++++/ \++++++++++++\'
write-host ' \++++++++++++\ /++++++++++++/'
write-host ' \++++++++++++\ /++++++++++++/'
write-host ' \++++++++++++\ /++++++++++++/'
write-host ' \++++++++++++\ /++++++++++++/'
write-host ' \++++++++++++\ /++++++++++++/'
write-host ' \++++++++++++\'
write-host ' \++++++++++++\'
write-host ' \++++++++++++\'
write-host ' \++++++++++++\'
write-host ' \------------\'
write-host 'Pure Storage FlashArray VMware ESXi UNMAP Script v5.0'
write-host '----------------------------------------------------------------------------------------------------'
$FAcount = 0
$inputOK = $false
do
{
try
{
[int]$FAcount = Read-Host "How many FlashArrays do you need to enter (enter a number)"
$inputOK = $true
}
catch
{
Write-Host -ForegroundColor red "INVALID INPUT! Please enter a numeric value."
}
}
until ($inputOK)
$flasharrays = @()
for ($i=0;$i -lt $FAcount;$i++)
{
$flasharray = read-host "Please enter a FlashArray IP or FQDN"
$flasharrays += $flasharray
}
$Creds = $Host.ui.PromptForCredential("FlashArray Credentials", "Please enter your FlashArray username and password.", "","")
#Connect to FlashArray via REST
$purevolumes=@()
$purevol=$null
$EndPoint= @()
$arraysnlist = @()
<#
Connect to FlashArray via REST with the SDK
Assumes the same credentials are in use for every FlashArray
#>
foreach ($flasharray in $flasharrays)
{
try
{
$tempArray = (New-PfaArray -EndPoint $flasharray -Credentials $Creds -IgnoreCertificateError -ErrorAction stop)
$EndPoint += $temparray
$purevolumes += Get-PfaVolumes -Array $tempArray
$arraySN = Get-PfaArrayAttributes -Array $tempArray
if ($arraySN.id[0] -eq "0")
{
$arraySN = $arraySN.id.Substring(1)
$arraySN = $arraySN.substring(0,19)
}
else
{
$arraySN = $arraySN.id.substring(0,18)
}
$arraySN = $arraySN -replace '-',''
$arraysnlist += $arraySN
add-content $logfile "FlashArray shortened serial is $($arraySN)"
}
catch
{
add-content $logfile ""
add-content $logfile ("Connection to FlashArray " + $flasharray + " failed. Please check credentials or IP/FQDN")
add-content $logfile $Error[0]
add-content $logfile "Terminating Script"
return
}
}
add-content $logfile '----------------------------------------------------------------------------------------------------'
add-content $logfile 'Connected to the following FlashArray(s):'
add-content $logfile $flasharrays
add-content $logfile '----------------------------------------------------------------------------------------------------'
write-host ""
$vcenter = read-host "Please enter a vCenter IP or FQDN"
$newcreds = Read-host "Re-use the FlashArray credentials for vCenter? (y/n)"
while (($newcreds -ine "y") -and ($newcreds -ine "n"))
{
write-host "Invalid entry, please enter y or n"
$newcreds = Read-host "Re-use the FlashArray credentials for vCenter? (y/n)"
}
if ($newcreds -ieq "n")
{
$Creds = $Host.ui.PromptForCredential("vCenter Credentials", "Please enter your vCenter username and password.", "","")
}
try
{
connect-viserver -Server $vcenter -Credential $Creds -ErrorAction Stop |out-null
add-content $logfile ('Connected to the following vCenter:')
add-content $logfile $vcenter
add-content $logfile '----------------------------------------------------------------------------------------------------'
}
catch
{
write-host "Failed to connect to vCenter" -BackgroundColor Red
write-host $vcenter
write-host $Error[0]
write-host "Terminating Script" -BackgroundColor Red
add-content $logfile "Failed to connect to vCenter"
add-content $logfile $vcenter
add-content $logfile $Error[0]
add-content $logfile "Terminating Script"
return
}
write-host ""
#A function to make REST Calls to Log Insight
function logInsightRestCall
{
$restvmfs = [ordered]@{
name = "Datastore"
content = $datastore.Name
}
$restarray = [ordered]@{
name = "FlashArray"
content = $endpoint[$arraychoice].endpoint
}
$restvol = [ordered]@{
name = "FlashArrayvol"
content = $purevol.name
}
$restunmap = [ordered]@{
name = "ReclaimedSpaceGB"
content = $reclaimedvirtualspace
}
$esxhost = [ordered]@{
name = "ESXihost"
content = $esxchosen[$i].name
}
$devicenaa = [ordered]@{
name = "SCSINaa"
content = $lun
}
$fields = @($restvmfs,$restarray,$restvol,$restunmap,$esxhost,$devicenaa)
$restcall = @{
messages = ([Object[]]($messages = [ordered]@{
text = ("Completed an UNMAP operation on the VMFS volume named " + $datastore.Name + " that is on the FlashArray named " + $endpoint[$arraychoice].endpoint + ".")
fields = ([Object[]]$fields)
}))
} |convertto-json -Depth 4
$resturl = ("http://" + $loginsightserver + ":9000/api/v1/messages/ingest/" + $loginsightagentID)
add-content $logfile ""
if($i=0){add-content $logfile ("Posting results to Log Insight server: " + $loginsightserver)}
try
{
$response = Invoke-RestMethod $resturl -Method Post -Body $restcall -ContentType 'application/json' -ErrorAction stop
if($i=0){add-content $logfile "REST Call to Log Insight server successful"}
}
catch
{
add-content $logfile "REST Call failed to Log Insight server"
add-content $logfile $error[0]
add-content $logfile $resturl
}
}
$arrayspacestart = @()
foreach ($flasharray in $endpoint)
{
$arrayspacestart += Get-PfaArraySpaceMetrics -array $flasharray
}
#Gather VMFS Datastores and identify how many are Pure Storage volumes
$reclaimeddatastores = @()
$virtualspace = @()
$physicalspace = @()
$esxchosen = @()
$expectedreturns = @()
$datastores = get-datastore
add-content $logfile 'Found the following datastores:'
add-content $logfile $datastores
add-content $logfile '----------------------------------------------------------------------------------------------------'
#Starting UNMAP Process on datastores
write-host "Please be patient--this process can take a long time"
$purevol = $null
foreach ($datastore in $datastores)
{
add-content $logfile (get-date)
add-content $logfile ('The datastore named ' + $datastore + ' is being examined')
$esx = $datastore | get-vmhost | where-object {($_.version -like '5.5.*') -or ($_.version -like '6.*')}| where-object {($_.ConnectionState -eq 'Connected')} |Select-Object -last 1
$unmapconfig = ""
if ($datastore.ExtensionData.Info.Vmfs.majorVersion -eq 6)
{
$esxcli=get-esxcli -VMHost $esx -v2
add-content $logfile ("The datastore named " + $datastore.name + " is VMFS version 6. Checking Automatic UNMAP configuration...")
$unmapargs = $esxcli.storage.vmfs.reclaim.config.get.createargs()
$unmapargs.volumelabel = $datastore.name
$unmapconfig = $esxcli.storage.vmfs.reclaim.config.get.invoke($unmapargs)
}
if ($datastore.Type -ne 'VMFS')
{
add-content $logfile ('This volume is not a VMFS volume, it is of type ' + $datastore.Type + ' and cannot be reclaimed. Skipping...')
add-content $logfile ''
add-content $logfile '----------------------------------------------------------------------------------------------------'
}
elseif ($esx.count -eq 0)
{
add-content $logfile ('This datastore has no 5.5 or later hosts to run UNMAP from. Skipping...')
add-content $logfile ''
add-content $logfile '----------------------------------------------------------------------------------------------------'
}
elseif ($unmapconfig.ReclaimPriority -eq "low")
{
add-content $logfile ('This VMFS has Automatic UNMAP enabled. No need to run a manual reclaim. Skipping...')
add-content $logfile ''
add-content $logfile '----------------------------------------------------------------------------------------------------'
}
else
{
$lun = $datastore.ExtensionData.Info.Vmfs.Extent.DiskName |select-object -unique
if ($lun.count -eq 1)
{
add-content $logfile ("The UUID for this volume is " + $datastore.ExtensionData.Info.Vmfs.Extent.DiskName)
$esxcli=get-esxcli -VMHost $esx -v2
if ($lun -like 'naa.624a9370*')
{
$volserial = ($lun.ToUpper()).substring(12)
$purevol = $purevolumes | where-object { $_.serial -eq $volserial }
if ($purevol.name -eq $null)
{
add-content $logfile 'ERROR: This volume has not been found. Please make sure that all of the FlashArrays presented to this vCenter are entered into this script.'
add-content $logfile ''
add-content $logfile '----------------------------------------------------------------------------------------------------'
continue
}
else
{
for($i=0; $i -lt $arraysnlist.count; $i++)
{
if ($arraysnlist[$i] -eq ($volserial.substring(0,16)))
{
$arraychoice = $i
}
}
$arrayname = Get-PfaArrayAttributes -array $EndPoint[$arraychoice]
add-content $logfile ('The volume is on the FlashArray ' + $arrayname.array_name)
add-content $logfile ('This datastore is a Pure Storage volume named ' + $purevol.name)
add-content $logfile ''
add-content $logfile ('The ESXi named ' + $esx + ' will run the UNMAP/reclaim operation')
add-content $logfile ''
$volinfo = Get-PfaVolumeSpaceMetrics -Array $EndPoint[$arraychoice] -VolumeName $purevol.name
$usedvolcap = ((1 - $volinfo.thin_provisioning)*$volinfo.size)/1024/1024/1024
$virtualspace += '{0:N0}' -f ($usedvolcap)
$physicalspace += '{0:N0}' -f ($volinfo.volumes/1024/1024/1024)
$usedspace = $datastore.CapacityGB - $datastore.FreeSpaceGB
$deadspace = '{0:N0}' -f ($usedvolcap - $usedspace)
if ($deadspace -lt 0)
{
$deadspace = 0
}
add-content $logfile ('The current used space of this VMFS is ' + ('{0:N0}' -f ($usedspace)) + " GB")
add-content $logfile ('The current used virtual space for its FlashArray volume is approximately ' + ('{0:N0}' -f ($usedvolcap)) + " GB")
$reclaimable = ('{0:N0}' -f ($deadspace))
if ($reclaimable -like "-*")
{
$reclaimable = 0
}
$expectedreturns += $reclaimable
add-content $logfile ('The minimum reclaimable virtual space for this FlashArray volume is ' + $reclaimable + ' GB')
#Calculating optimal block count. If VMFS is 75% full or more the count must be 200 MB only. Ideal block count is 1% of free space of the VMFS in MB
if ((1 - $datastore.FreeSpaceMB/$datastore.CapacityMB) -ge .75)
{
$blockcount = 200
add-content $logfile 'The volume is 75% or more full so the block count is overridden to 200 MB. This will slow down the reclaim dramatically'
add-content $logfile 'It is recommended to either free up space on the volume or increase the capacity so it is less than 75% full'
add-content $logfile ("The block count in MB will be " + $blockcount)
}
else
{
$blockcount = [math]::floor($datastore.FreeSpaceMB * .008)
add-content $logfile ("The maximum allowed block count for this datastore is " + $blockcount)
}
$unmapargs = $esxcli.storage.vmfs.unmap.createargs()
$unmapargs.volumelabel = $datastore.Name
$unmapargs.reclaimunit = $blockcount
try
{
$reclaimeddatastores += $datastore
$esxchosen += $esx
write-host ("Running UNMAP on VMFS named " + $datastore.Name + "...")
$esxcli.storage.vmfs.unmap.invoke($unmapargs) |out-null
}
catch
{
add-content $logfile "Failed to complete UNMAP to this volume. Most common cause is a PowerCLI timeout which means UNMAP will continue to completion in the background for this VMFS."
add-content $logfile $Error[0]
add-content $logfile "Moving to the next volume"
continue
}
add-content $logfile ''
add-content $logfile '----------------------------------------------------------------------------------------------------'
}
}
else
{
add-content $logfile ('The volume is not a FlashArray device, skipping the UNMAP operation')
add-content $logfile ''
add-content $logfile '----------------------------------------------------------------------------------------------------'
continue
}
}
elseif ($lun.count -gt 1)
{
add-content $logfile ('The volume spans more than one SCSI device, skipping UNMAP operation')
add-content $logfile ''
add-content $logfile '----------------------------------------------------------------------------------------------------'
continue
}
}
}
$arrayspaceend = @()
$arraychanges = @()
$finaldatastores = @()
$totalreclaimedvirtualspace = 0
start-sleep 120
for ($i=0;$i -lt $reclaimeddatastores.count;$i++)
{
$lun = $reclaimeddatastores[$i].ExtensionData.Info.Vmfs.Extent.DiskName |select-object -unique
$volserial = ($lun.ToUpper()).substring(12)
$purevol = $purevolumes | where-object { $_.serial -eq $volserial }
for($a=0; $a -lt $arraysnlist.count; $a++)
{
if ($arraysnlist[$a] -eq ($volserial.substring(0,16)))
{
$arraychoice = $a
}
}
$volinfo = Get-PfaVolumeSpaceMetrics -Array $EndPoint[$arraychoice] -VolumeName $purevol.name
$usedvolcap = '{0:N0}' -f (((1 - $volinfo.thin_provisioning)*$volinfo.size)/1024/1024/1024)
$newphysicalspace = '{0:N0}' -f ($volinfo.volumes/1024/1024/1024)
$reclaimedvirtualspace = $virtualspace[$i] - $usedvolcap
$reclaimedphysicalspace = $physicalspace[$i] - $newphysicalspace
$totalreclaimedvirtualspace += $reclaimedvirtualspace
if ($reclaimedvirtualspace -like "-*")
{
$reclaimedvirtualspace = 0
}
if ($reclaimedphysicalspace -like "-*")
{
$reclaimedphysicalspace = 0
}
$finaldatastores += New-Object psobject -Property @{Datastore=$($reclaimeddatastores[$i].name);Volume=$($purevol.name);ExpectedMinimumVirtualSpaceGBReclaimed=$($expectedreturns[$i]);ActualVirtualSpaceGBReclaimed=$($reclaimedvirtualspace);ActualPhysicalSpaceGBReclaimed=$($reclaimedphysicalspace)}
if ($useloginsight -ieq "y"){logInsightRestCall}
}
for ($i=0;$i -lt $endpoint.count;$i++)
{
$arrayspaceend += Get-PfaArraySpaceMetrics -array $endpoint[$i]
$physicalspacedifference = ($arrayspacestart[$i].volumes - $arrayspaceend[$i].volumes)/1024/1024/1024
if ($physicalspacedifference -like "-*")
{
$physicalspacedifference = 0
}
if ($totalreclaimedvirtualspace[$i] -like "-*")
{
$virtualspacedifference = 0
}
else
{
$virtualspacedifference = $totalreclaimedvirtualspace[$i]
}
$arraychanges += New-Object psobject -Property @{FlashArray=$($arrayspaceend[$i].hostname);VirtualSpaceGBReclaimed=$('{0:N0}' -f ($virtualspacedifference));PhysicalSpaceGBReclaimed=$('{0:N0}' -f ($physicalspacedifference))}
}
add-content $logfile "FlashArray-level Reclamation Statistics:"
write-host "FlashArray-level Reclamation Statistics:"
write-host ($arraychanges |ft -autosize -Property FlashArray,VirtualSpaceGBReclaimed,PhysicalSpaceGBReclaimed | Out-String )
$arraychanges|ft -autosize -Property FlashArray,VirtualSpaceGBReclaimed,PhysicalSpaceGBReclaimed | Out-File -FilePath $logfile -Append -Encoding ASCII
add-content $logfile "Volume-level Reclamation Statistics:"
write-host "Volume-level Reclamation Statistics:"
write-host ($finaldatastores |ft -autosize -Property Datastore,Volume,ExpectedMinimumVirtualSpaceGBReclaimed,ActualVirtualSpaceGBReclaimed,ActualPhysicalSpaceGBReclaimed | Out-String )
$finaldatastores|ft -autosize -Property Datastore,Volume,ExpectedMinimumVirtualSpaceGBReclaimed,ActualVirtualSpaceGBReclaimed,ActualPhysicalSpaceGBReclaimed | Out-File -FilePath $logfile -Append -Encoding ASCII
add-content $logfile ("Space reclaim operation for all FlashArray VMFS volumes is complete.")
add-content $logfile ""
#disconnecting sessions
add-content $logfile ("Disconnecting vCenter and FlashArray sessions")
disconnect-viserver -Server $vcenter -confirm:$false
foreach ($flasharray in $endpoint)
{
Disconnect-PfaArray -Array $flasharray
}