Vmware - Automating Older Snapshots Deletion
Automating snapshot deletions using PowerShell. will be updating this blog soon....
will eloborate in details in coming days.
collecting data
|
Removal logic First Scenario 1. VM-1 has
three snapshots. 1.1 first
snapshot with random name and description 1.2 second
snapshot has CHG pattern either in name or description 1.3 third
snapshot with random name and description if the vm-1
snapshot that is older than 18 hours and has CHG pattern in name or
description but i don't
want to delete any snapshot in this scenario but report in html that vm-1 has
multiple snapshot even it
matches the condition but due to multiple vm its just reports it not delete Second
Scenario 2. VM-2 has
only one snapshot snapshot has
CHG pattern and also match the 18 hour deadline as awell then it
should delete the snapshot |
|
# Snapshot
Cleanup Script # Parameters:
Please modify the below requirements as per your need. $vCenterServer
= "YOUR VCENTER" $hoursOld =
18 # Age threshold in hours $dryRun =
$false # True=Runs in simulation mode (no deletion), False=Deletes snapshots
per logic $deleteCHGSnapshots
= $true # True=Check CHGXXXXX pattern in snapshot name/description,
False=Ignore CHG pattern $vmFilter =
"*" $location =
@("ausmdc1", "aussdc2") # Add more locations here $reportPath =
"D:\Veeam\Snapshot_Deletion\Report\SnapshotCleanupReport_$(Get-Date
-Format 'yyyy-MM-dd_HH-mm-ss').html" $logPath =
"D:\Veeam\Snapshot_Deletion\Report\SnapshotCleanup_$(Get-Date -Format
'yyyy-MM-dd_HH-mm-ss').log" $emailTo =
@("YOUR EMAIL") $emailFrom =
"YOUR SMTP EMAIL" $emailSMTPServer
= "YOUR SMTP SERVER" $batchSize =
2 # Configurable batch size for
snapshot deletion to avoid throttling $retentionDays
= 7 # Log Retention policy: Delete
logs and reports older than this number of days $excludeVMs =
$null # @("AUSMDC1MGT002", "AUSSDC1WEB002") # Exclusion list for VMs to skip # Function to
write to log file function
Write-Log { param ( [string]$Level = "INFO", [string]$Message ) $timestamp = Get-Date -Format
"yyyy-MM-dd HH:mm:ss" $logMessage = "[$timestamp] [$Level]
$Message" Write-Host $logMessage Add-Content -Path $logPath -Value
$logMessage } # Function to
clean up old logs and reports based on retention policy function
Invoke-RetentionPolicy { param ( [string]$Directory, [int]$Days ) $cutoffDate = (Get-Date).AddDays(-$Days) $oldFiles = Get-ChildItem -Path
$Directory -File | Where-Object { $_.LastWriteTime -lt $cutoffDate } foreach ($file in $oldFiles) { try { Remove-Item -Path $file.FullName
-Force -ErrorAction Stop Write-Log -Level "INFO"
-Message "Deleted old file: $($file.FullName) as per retention
policy" } catch { Write-Log -Level
"ERROR" -Message "Failed to delete old file:
$($file.FullName): $($_.Exception.Message)" } } } # Function to
generate HTML report with updated color-coding and snapshot description function
Generate-HTMLReport { param ( [array]$ReportData, [int]$TotalVMs, [int]$TotalSnapshots, [int]$DeletedSnapshots, [int]$SkippedSnapshots, [int]$NoSnapshots, [int]$WouldDeleteSnapshots ) Write-Log -Level "INFO"
-Message "Generating HTML report with: TotalVMs=$TotalVMs,
TotalSnapshots=$TotalSnapshots, DeletedSnapshots=$DeletedSnapshots,
WouldDeleteSnapshots=$WouldDeleteSnapshots, SkippedSnapshots=$SkippedSnapshots,
NoSnapshots=$NoSnapshots" # Ensure non-negative values and validate
chart data $DeletedSnapshots = [math]::Max(0,
$DeletedSnapshots) $WouldDeleteSnapshots = [math]::Max(0,
$WouldDeleteSnapshots) $SkippedSnapshots = [math]::Max(0,
$SkippedSnapshots) $NoSnapshots = [math]::Max(0,
$NoSnapshots) # Chart data validation: If all values
are zero, log a warning if ($DeletedSnapshots -eq 0 -and
$WouldDeleteSnapshots -eq 0 -and $SkippedSnapshots -eq 0) { Write-Log -Level "WARNING"
-Message "All chart data values are zero; pie chart will be empty" } $htmlContent = @" <!DOCTYPE
html> <html
lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport"
content="width=device-width, initial-scale=1.0"> <title>Snapshot Cleanup
Report</title> <script
src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script> <script
src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0/dist/chartjs-plugin-datalabels.min.js"></script> <script
src="https://cdn.tailwindcss.com"></script> </head> <body
class="bg-gray-100 font-sans"> <div class="container mx-auto
p-6"> <h1 class="text-3xl font-bold
text-center text-gray-800 mb-6">vCenter Snapshot Cleanup
Report</h1> <p class="text-center
text-gray-600 mb-4">Generated on $(Get-Date -Format 'yyyy-MM-dd
HH:mm:ss')</p> <!-- Summary Section --> <div class="bg-white
shadow-md rounded-lg p-6 mb-6"> <h2 class="text-2xl
font-semibold text-gray-700 mb-4">Summary</h2> <div class="grid
grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4"> <div
class="bg-blue-100 p-4 rounded-lg text-center"> <p class="text-lg
font-medium text-blue-800">Total VMs Processed</p> <p
class="text-2xl font-bold text-blue-900">$TotalVMs</p> </div> <div
class="bg-green-100 p-4 rounded-lg text-center"> <p class="text-lg
font-medium text-green-800">Total Snapshots Found</p> <p
class="text-2xl font-bold
text-green-900">$TotalSnapshots</p> </div> <div
class="bg-red-100 p-4 rounded-lg text-center"> <p class="text-lg
font-medium text-red-800">Snapshots Deleted</p> <p
class="text-2xl font-bold
text-red-900">$DeletedSnapshots</p> </div> <div
class="bg-blue-100 p-4 rounded-lg text-center"> <p class="text-lg
font-medium text-blue-800">Would Delete (Dry Run)</p> <p
class="text-2xl font-bold
text-blue-900">$WouldDeleteSnapshots</p> </div> <div
class="bg-yellow-100 p-4 rounded-lg text-center"> <p class="text-lg
font-medium text-yellow-800">Dry Run</p> <p
class="text-2xl font-bold
text-yellow-900">$($dryRun.ToString())</p> </div> </div> </div> <!-- Pie
Chart --> <div class="bg-white
shadow-md rounded-lg p-6 mb-6"> <h2 class="text-2xl
font-semibold text-gray-700 mb-4">Snapshot Distribution</h2> <canvas
id="snapshotChart" class="w-full max-w-lg
mx-auto"></canvas> <p id="chartError"
class="text-red-600 text-center hidden">Failed to load chart.
Please check the browser console for details.</p> </div> <!-- Snapshot Details Table --> <div class="bg-white
shadow-md rounded-lg p-6"> <h2 class="text-2xl
font-semibold text-gray-700 mb-4">Snapshot Details</h2> <div
class="overflow-x-auto"> <table class="w-full
table-auto border-collapse"> <thead> <tr
class="bg-gray-200"> <th
class="px-4 py-2 text-left text-gray-600">VM Name</th> <th
class="px-4 py-2 text-left text-gray-600">Snapshot
Name</th> <th
class="px-4 py-2 text-left text-gray-600">Snapshot
Description</th> <th
class="px-4 py-2 text-left text-gray-600">Age (Hours)</th> <th
class="px-4 py-2 text-left text-gray-600">Size (GB)</th> <th
class="px-4 py-2 text-left text-gray-600">Action</th> <th
class="px-4 py-2 text-left text-gray-600">Reason</th> </tr> </thead> <tbody> $($ReportData
| ForEach-Object { $actionColor = switch ($_.Action) { "Deleted" {
"text-green-800 font-bold bg-green-100" } "Skipped" { if ($_.Reason -eq "VM has
multiple snapshots") { "text-orange-800 font-bold
bg-orange-100" } else {
"text-yellow-600" } } "No Snapshots" {
"text-gray-600" } "Failed" {
"text-red-600" } "Would Delete (Dry Run)" {
"text-blue-800 font-bold bg-blue-100" } default { "text-gray-600" } } $vmNameClass = if ($_.Action -in
@("Would Delete (Dry Run)", "Deleted")) {
"bg-blue-100 font-bold" } elseif ($_.Action -eq
"Skipped" -and $_.Reason -eq "VM has multiple snapshots")
{ "bg-orange-100 font-bold" } else { "" } "<tr> <td class='border px-4 py-2
$vmNameClass'>$($_.VMName)</td> <td class='border px-4
py-2'>$($_.SnapshotName)</td> <td class='border px-4
py-2'>$($_.Description)</td> <td class='border px-4
py-2'>$($_.Age)</td> <td class='border px-4
py-2'>$($_.SizeGB)</td> <td class='border px-4 py-2
$actionColor'>$($_.Action)</td> <td class='border px-4
py-2'>$($_.Reason)</td> </tr>" } |
Out-String) </tbody> </table> </div> </div> <script>
document.addEventListener('DOMContentLoaded', function() { const ctx =
document.getElementById('snapshotChart').getContext('2d'); const chartError =
document.getElementById('chartError'); const chartData =
[$DeletedSnapshots, $WouldDeleteSnapshots, $SkippedSnapshots]; console.log('Chart data:',
chartData); // Check if Chart.js is loaded if (typeof Chart === 'undefined')
{ console.error('Chart.js not
loaded');
chartError.classList.remove('hidden'); return; } // Check if canvas context is
valid if (!ctx) { console.error('Canvas context
not found');
chartError.classList.remove('hidden'); return; } try { // Register the datalabels
plugin
Chart.register(ChartDataLabels); new Chart(ctx, { type: 'pie', data: { labels: ['Deleted',
'Would Delete (Dry Run)', 'Skipped'], datasets: [{ data: chartData, backgroundColor:
['#34D399', '#3B82F6', '#FBBF24'], borderColor:
['#FFFFFF'], borderWidth: 1 }] }, options: { responsive: true, plugins: { legend: {
position: 'top' }, title: { display:
true, text: 'Snapshot Distribution' }, datalabels: { color:
'#FFFFFF', font: { weight:
'bold', size: 18 }, formatter:
(value, context) => { return
value > 0 ? value : ''; // Only show non-zero values }, anchor:
'center', align:
'center' }, tooltip: { enabled: true
// Keep hover tooltips enabled } } } }); console.log('Chart
initialized successfully'); } catch (error) { console.error('Error
initializing chart:', error);
chartError.classList.remove('hidden'); } }); </script> </div> </body> </html> "@ $htmlContent | Out-File -FilePath
$reportPath -Encoding UTF8 Write-Log -Level "INFO"
-Message "Generated HTML report at $reportPath" return $reportPath } # Function to
delete snapshots function
Delete-VMSnapshots { param ( [array]$VMs, [int]$HoursOld, [bool]$DryRun, [bool]$DeleteCHGSnapshots, [int]$BatchSize ) $reportData = @() $totalSnapshots = 0 $deletedSnapshots = 0 $wouldDeleteSnapshots = 0 $skippedSnapshots = 0 $noSnapshots = 0 $vmSnapshotSizes = @() # Process each VM foreach ($vm in $VMs) { try { Write-Log -Level "INFO"
-Message "Processing VM: $($vm.Name)" $snapshots = Get-Snapshot -VM $vm
-ErrorAction Stop if (-not $snapshots) { Write-Log -Level
"INFO" -Message "No snapshots found for VM '$($vm.Name)'" $reportData +=
[PSCustomObject]@{ VMName = $vm.Name SnapshotName =
"N/A" Description = "N/A" Age = "N/A" SizeGB = "N/A" Action = "No Snapshots" Reason = "No snapshots found" } $noSnapshots++ continue } $totalSnapshots +=
$snapshots.Count $totalSizeMB = 0 $eligibleSnapshots = @() # Evaluate snapshots for
eligibility foreach ($snapshot in $snapshots)
{ $snapshotAge = ((Get-Date) -
$snapshot.Created).TotalHours $sizeMB = if ($null -ne
$snapshot.SizeMB -and $snapshot.SizeMB -ge 0) {
[math]::Round($snapshot.SizeMB, 2) } else { Write-Log -Level
"WARNING" -Message "Invalid or missing size for snapshot
'$($snapshot.Name)' on VM '$($vm.Name)'. Setting size to 0 MB." 0 } $sizeGB =
[math]::Round($sizeMB / 1024, 2) $chgMatchName =
$DeleteCHGSnapshots -and $snapshot.Name -match "CHG[0-9]+" $chgMatchDesc =
$DeleteCHGSnapshots -and $snapshot.Description -match "CHG[0-9]+" $chgMatch = $chgMatchName -or
$chgMatchDesc $chgMatchSource = if
($chgMatchName -and $chgMatchDesc) { "name and description" } elseif
($chgMatchName) { "name" } elseif
($chgMatchDesc) { "description" } else {
"none" } Write-Log -Level
"DEBUG" -Message "Snapshot '$($snapshot.Name)' for VM
'$($vm.Name)' Description: '$($snapshot.Description)', CHG Match in Name:
$chgMatchName, CHG Match in Description: $chgMatchDesc, Age: $snapshotAge
hours, SizeMB: $sizeMB" if ($snapshotAge -gt
$HoursOld -and (!$DeleteCHGSnapshots -or $chgMatch)) { $eligibleSnapshots +=
[PSCustomObject]@{ Snapshot = $snapshot Name = $snapshot.Name Description = $snapshot.Description Age = [math]::Round($snapshotAge, 2) SizeMB = $sizeMB SizeGB = $sizeGB ChgMatchSource =
$chgMatchSource } $totalSizeMB += $sizeMB } else { $reason = if
($snapshotAge -le $HoursOld -and $DeleteCHGSnapshots -and -not $chgMatch) { "Age <=
$HoursOld hours and no CHG pattern in name or description" } elseif ($snapshotAge
-le $HoursOld) { "Age <=
$HoursOld hours" } elseif
($DeleteCHGSnapshots -and -not $chgMatch) { "No CHG pattern
in name or description" } else { "Unknown
reason" } Write-Log -Level
"INFO" -Message "Snapshot '$($snapshot.Name)' for VM
'$($vm.Name)' is $snapshotAge hours old, CHG pattern in $chgMatchSource, not
deleting ($reason)" $reportData +=
[PSCustomObject]@{ VMName = $vm.Name SnapshotName =
$snapshot.Name Description = $snapshot.Description Age = [math]::Round($snapshotAge, 2) SizeGB = $sizeGB Action = "Skipped" Reason = $reason } $skippedSnapshots++ } } # Apply modified deletion logic if ($eligibleSnapshots) { if ($snapshots.Count -gt 1) { # Scenario 1: VM has
multiple snapshots, skip deletion foreach ($snapData in
$eligibleSnapshots) { $snapshotName =
$snapData.Name $snapshotDescription
= $snapData.Description $snapshotAge =
$snapData.Age $sizeGB =
$snapData.SizeGB $chgMatchSource =
$snapData.ChgMatchSource Write-Log -Level
"INFO" -Message "Snapshot '$snapshotName' for VM '$($vm.Name)'
is eligible but skipped due to multiple snapshots (Age: $snapshotAge hours,
CHG pattern in $chgMatchSource)" $reportData +=
[PSCustomObject]@{ VMName = $vm.Name SnapshotName =
$snapshotName Description = $snapshotDescription Age = $snapshotAge SizeGB = $sizeGB Action = "Skipped" Reason = "VM has multiple
snapshots" } $skippedSnapshots++ } } else { # Scenario 2: VM has one
snapshot, process for deletion $vmSnapshotSizes +=
[PSCustomObject]@{ VMName = $vm.Name TotalSizeMB = $totalSizeMB Snapshots = $eligibleSnapshots } } } } catch { Write-Log -Level
"ERROR" -Message "Failed to process VM '$($vm.Name)':
$($_.Exception.Message)" $reportData += [PSCustomObject]@{ VMName = $vm.Name SnapshotName =
"N/A" Description = "N/A" Age = "N/A" SizeGB = "N/A" Action = "Failed" Reason = "Failed to process:
$($_.Exception.Message)" } } } # Identify VM with smallest total
snapshot size if ($vmSnapshotSizes) { $smallestVM = $vmSnapshotSizes |
Sort-Object TotalSizeMB | Select-Object -First 1 Write-Log -Level "INFO"
-Message "VM with smallest total snapshot size: '$($smallestVM.VMName)'
($($smallestVM.TotalSizeMB) MB)" } else { Write-Log -Level "INFO"
-Message "No VMs with eligible snapshots found" } # Sort VMs by total snapshot size $vmSnapshotSizes = $vmSnapshotSizes |
Sort-Object TotalSizeMB # Process snapshots for deletion in
batches of up to $BatchSize foreach ($vmData in $vmSnapshotSizes) { $vmName = $vmData.VMName $eligibleSnapshots =
$vmData.Snapshots | Sort-Object SizeMB # Sort by size (smallest first) $snapshotBatches = @() $batch = @() # Create batches of up to $BatchSize
snapshots foreach ($snapData in
$eligibleSnapshots) { $batch += $snapData if ($batch.Count -ge $BatchSize)
{ $snapshotBatches += ,$batch $batch = @() } } if ($batch) { $snapshotBatches += ,$batch } # Process each batch foreach ($batch in $snapshotBatches)
{ foreach ($snapData in $batch) { $snapshot =
$snapData.Snapshot $snapshotName =
$snapData.Name $snapshotDescription =
$snapData.Description $snapshotAge = $snapData.Age $sizeMB = $snapData.SizeMB $sizeGB = $snapData.SizeGB $chgMatchSource =
$snapData.ChgMatchSource if ($DryRun) { Write-Log -Level
"INFO" -Message "Snapshot '$snapshotName' for VM '$vmName' is
eligible for deletion (Age: $snapshotAge hours, CHG pattern in
$chgMatchSource)" $reportData +=
[PSCustomObject]@{ VMName = $vmName SnapshotName =
$snapshotName Description = $snapshotDescription Age = $snapshotAge SizeGB = $sizeGB Action = "Would Delete (Dry Run)" Reason = "Older than $HoursOld
hours" + $(if ($DeleteCHGSnapshots) { " and CHG pattern in
$chgMatchSource" } else { "" }) } $wouldDeleteSnapshots++ } else { try { Write-Log -Level
"INFO" -Message "Deleting snapshot '$snapshotName' for VM
'$vmName' (Age: $snapshotAge hours, SizeMB: $sizeMB)" Remove-Snapshot
-Snapshot $snapshot -Confirm:$false -ErrorAction Stop Write-Log -Level
"INFO" -Message "Successfully deleted snapshot '$snapshotName'
for VM '$vmName'" $reportData +=
[PSCustomObject]@{ VMName = $vmName SnapshotName =
$snapshotName Description = $snapshotDescription Age = $snapshotAge SizeGB = $sizeGB Action = "Deleted" Reason = "Snapshot deleted
successfully" } $deletedSnapshots++ } catch { Write-Log -Level
"ERROR" -Message "Failed to delete snapshot '$snapshotName'
for VM '$vmName': $($_.Exception.Message)" $reportData +=
[PSCustomObject]@{ VMName = $vmName SnapshotName =
$snapshotName Description = $snapshotDescription Age = $snapshotAge SizeGB = $sizeGB Action = "Failed" Reason = "Failed to delete:
$($_.Exception.Message)" } } } } if (-not $DryRun -and $batch) { Write-Log -Level
"INFO" -Message "Waiting 10 seconds after processing batch for
VM '$vmName'" Start-Sleep -Seconds 10 } } } return @{ ReportData = $reportData TotalSnapshots = $totalSnapshots DeletedSnapshots = $deletedSnapshots WouldDeleteSnapshots =
$wouldDeleteSnapshots SkippedSnapshots = $skippedSnapshots NoSnapshots = $noSnapshots } } # Main script try { # Import VMware PowerCLI module Import-Module VMware.VimAutomation.Core
-ErrorAction Stop Write-Log -Level "INFO"
-Message "Imported VMware PowerCLI module" # Connect to vCenter try { Write-Log -Level "INFO"
-Message "Connecting to vCenter server: $vCenterServer" Connect-VIServer -Server
$vCenterServer -ErrorAction Stop Write-Log -Level "INFO"
-Message "Successfully connected to vCenter" } catch { Write-Log -Level "ERROR"
-Message "Failed to connect to vCenter: $($_.Exception.Message)" exit } # Get all VMs, excluding those in
$excludeVMs $vms = Get-VM -Location $location -Name
$vmFilter -ErrorAction SilentlyContinue | Where-Object { $_.Name -notin
$excludeVMs } Write-Log -Level "DEBUG"
-Message "Found $($vms.Count) VMs after exclusions: $($vms.Name -join ',
')" # Process snapshots $result = Delete-VMSnapshots -VMs $vms
-HoursOld $hoursOld -DryRun $dryRun -DeleteCHGSnapshots $deleteCHGSnapshots
-BatchSize $batchSize $reportData = $result.ReportData $totalVMs = $vms.Count $totalSnapshots = $result.TotalSnapshots $deletedSnapshots =
$result.DeletedSnapshots $wouldDeleteSnapshots =
$result.WouldDeleteSnapshots $skippedSnapshots =
$result.SkippedSnapshots $noSnapshots = $result.NoSnapshots # Enhance log summary to include
"Would Delete (Dry Run)" entries $wouldDeleteVMs = $reportData |
Where-Object { $_.Action -eq "Would Delete (Dry Run)" } |
Select-Object -Unique VMName, SnapshotName, Description, Age, SizeGB, Reason if ($wouldDeleteVMs) { Write-Log -Level "INFO"
-Message "VMs with snapshots marked for deletion (Dry Run):" foreach ($entry in $wouldDeleteVMs) { Write-Log -Level "INFO"
-Message "Would Delete (Dry Run): VM '$($entry.VMName)' Snapshot
'$($entry.SnapshotName)' (Description: '$($entry.Description)', Age:
$($entry.Age) hours, Size: $($entry.SizeGB) GB, Reason:
$($entry.Reason))" } } else { Write-Log -Level "INFO"
-Message "No VMs with snapshots marked for deletion (Dry Run)" } # Generate HTML report $reportFile = Generate-HTMLReport
-ReportData $reportData -TotalVMs $totalVMs -TotalSnapshots $totalSnapshots
-DeletedSnapshots $deletedSnapshots -SkippedSnapshots $skippedSnapshots
-NoSnapshots $noSnapshots -WouldDeleteSnapshots $wouldDeleteSnapshots # Prepare summary for log and email $summary = @" Snapshot
Cleanup Summary: Total VMs
processed: $totalVMs Total
snapshots found: $totalSnapshots Snapshots
deleted: $deletedSnapshots Would delete
(dry run): $wouldDeleteSnapshots Dry Run:
$dryRun Delete CHG
Snapshots: $deleteCHGSnapshots HTML Report:
$reportFile Log File:
$logPath Details: $($reportData
| ForEach-Object { "$($_.Action): VM '$($_.VMName)' Snapshot
'$($_.SnapshotName)' (Description: '$($_.Description)', Age: $($_.Age) hours,
Size: $($_.SizeGB) GB, Reason: $($_.Reason))" } | Out-String) "@ Write-Log -Level "INFO"
-Message $summary # Send email notification try { $emailBody = "Snapshot Cleanup
Report generated on $(Get-Date -Format 'yyyy-MM-dd
HH:mm:ss').<br><br>$($summary -replace "`n",
"<br>")" Send-MailMessage -To $emailTo -From
$emailFrom -Subject "vCenter Snapshot Cleanup Report" -Body
$emailBody -BodyAsHtml -SmtpServer $emailSMTPServer -Attachments $reportFile
-ErrorAction Stop Write-Log -Level "INFO"
-Message "Email notification sent to $($emailTo -join ', ') with report
attachment" } catch { Write-Log -Level "ERROR"
-Message "Failed to send email: $($_.Exception.Message)" } # Apply retention policy to logs and
reports $reportDir =
"D:\Veeam\Snapshot_Deletion\Report" Invoke-RetentionPolicy -Directory
$reportDir -Days $retentionDays } catch { Write-Log -Level "ERROR"
-Message "Script failed: $($_.Exception.Message)" } finally { if (Get-VIServer -Server $vCenterServer
-ErrorAction SilentlyContinue) { Disconnect-VIServer -Server
$vCenterServer -Confirm:$false Write-Log -Level "INFO"
-Message "Disconnected from vCenter server" } } Write-Log
-Level "INFO" -Message "Snapshot cleanup script
completed" |