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

 Imagine two scenarios

 

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"



Popular posts from this blog

On-Prem Storage To Azure Storage - Part-2

On-Prem Storage To Azure Storage - Part-1

Secure Boot vCenter Deployment