Automation
Automation: Bulk Create and Delete VM Snapshots Across Linked vCenters | Lazy Admin Blog

In a large environment, taking snapshots before a major patch or application update is a standard safety net. But if you have servers spread across multiple vCenters in Linked Mode (e.g., Datacenter1 and Datacenter2), clicking through the vSphere Client is a waste of time.
Today, I’m sharing a “Lazy Admin” script that allows you to bulk create, check, and remove snapshots using a simple CSV list.
Prerequisites
- VMware PowerCLI: Ensure the PowerCLI module is installed on the machine running the script.
- CSV Setup: Create a file named
snapshot_servers.csvinC:\Temp\VMSnapshots\.
The CSV should look like this: | Host | Location | | :— | :— | | Server01 | Datacenter1 | | Server02 | Datacenter2 |
Part 1: Creating Snapshots
- Open PowerShell ISE with vCenter Administrator credentials.
- Load the functions by running the full script (provided below).
- Run the following command:
Create-VMSnapshots -SS_CSV "C:\Temp\VMSnapshots\snapshot_servers.csv" -SS_Name "Pre-Patching" -SS_Description "Requested by App Team"
The script will iterate through your CSV and create snapshots sequentially. You can monitor the progress in the vSphere Tasks console.
Part 2: Deleting Snapshots
Once your changes are verified, don’t let those snapshots linger and bloat your datastores! To remove them:
- Use the same
snapshot_servers.csvlist. - Run the following command:
Remove-VMSnapshots -SS_CSV "C:\Temp\VMSnapshots\snapshot_servers.csv"
Note: This is a sequential script; it will wait for one snapshot removal to finish before moving to the next to avoid pinning your storage I/O.
The Script: VMSnapshots.ps1
Save this code to C:\Temp\VMSnapshots\VMSnapshots.ps1.
function Create-VMSnapshots { param ( [string]$SS_CSV = $(Read-Host "Enter path to CSV"), [string]$SS_Name = $(Read-Host "Enter name for snapshots"), [string]$SS_Description = $(Read-Host "Enter description for snapshots") ) # Import VMware PowerCLI Module If ( !(Get-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) ) { import-module VMware.VimAutomation.Core } $Servers = Import-CSV $SS_CSV $WLM_vCenter = Connect-VIServer vCenter1 -WarningAction SilentlyContinue $EDN_vCenter = Connect-VIServer vCenter2 -WarningAction SilentlyContinue ForEach($Server in $Servers){ If($Server.Location -eq 'Datacenter1'){ New-Snapshot -VM $Server.Host -Name $SS_Name -Description $SS_Description -Quiesce -Server $WLM_vCenter -WarningAction SilentlyContinue } ElseIf($Server.Location -eq 'Datacenter2'){ New-Snapshot -VM $Server.Host -Name $SS_Name -Description $SS_Description -Quiesce -Server $EDN_vCenter -WarningAction SilentlyContinue } } }function Check-VMSnapshots { param ( [string]$SS_CSV = $(Read-Host "Enter path to CSV"), [string]$SS_Name = $(Read-Host "Enter snapshot name") ) # Import VMware PowerCLI Module If ( !(Get-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) ) { import-module VMware.VimAutomation.Core } $Servers = Import-CSV $SS_CSV $WLM_vCenter = Connect-VIServer vCenter1 -WarningAction SilentlyContinue $EDN_vCenter = Connect-VIServer vCenter2 -WarningAction SilentlyContinue ForEach($Server in $Servers){ If($Server.Location -eq 'Datacenter1'){ Get-Snapshot -VM $Server.Host -Name $SS_Name -Server $WLM_vCenter | Select VM, Name, @{ n="SpaceUsedGB"; e={[math]::round( $_.SizeGB )}} -WarningAction SilentlyContinue } ElseIf($Server.Location -eq 'Datacenter2'){ Get-Snapshot -VM $Server.Host -Name $SS_Name -Server $EDN_vCenter | Select VM, Name, @{ n="SpaceUsedGB"; e={[math]::round( $_.SizeGB )}} -WarningAction SilentlyContinue } } } function Remove-VMSnapshots { param ( [string]$SS_CSV = $(Read-Host "Enter path to CSV") ) # Import VMware PowerCLI Module If ( !(Get-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) ) { import-module VMware.VimAutomation.Core } $Servers = Import-CSV $SS_CSV $WLM_vCenter = Connect-VIServer vCenter1 -WarningAction SilentlyContinue $EDN_vCenter = Connect-VIServer vCenter2 -WarningAction SilentlyContinue ForEach($Server in $Servers){ If($Server.Location -eq 'Datacenter1'){ Get-Snapshot $Server.Host -Server $WLM_vCenter | Remove-Snapshot -Confirm:$false -WarningAction SilentlyContinue } ElseIf($Server.Location -eq 'Datacenter2'){ Get-Snapshot $Server.Host -Server $EDN_vCenter | Remove-Snapshot -Confirm:$false -WarningAction SilentlyContinue } } }
Build Your Own VM Snapshot GUI with PowerShell | Lazy Admin Blog

The ultimate “I’m too busy for CLI” tool for VMware Admins.
Today, I am sharing a complete PowerShell tool that creates a custom Windows Form to manage bulk snapshots across multiple vCenters, complete with a progress bar and an automated HTML email report.
🚀 What this tool does:
- Multi-vCenter Support: Toggle between environments with simple radio buttons.
- Bulk Processing: Paste a list of hostnames directly into the text box.
- Smart Memory Handling: Choose whether to include VM memory in the snapshot.
- Progress Tracking: A real-time progress bar so you know exactly when it’s safe to go grab another coffee.
- Automated Reporting: Generates a CSV on your desktop and sends a formatted HTML email to your team.
The Script: VM-Snapshot-GUI.ps1
Lazy Admin Note: Before running, make sure you have the VMware PowerCLI module installed. Change the
"SMTP Server"and"From@domain.com"strings in the script to match your environment.
<!-- wp:syntaxhighlighter/code -->
<pre class="wp-block-syntaxhighlighter-code">Function Snapshot()
{
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
# Create the Main form.
$form = New-Object System.Windows.Forms.Form
$form.Text = "VM Snapshot"
$form.Size = New-Object System.Drawing.Size(650,320)
$form.FormBorderStyle = 'FixedSingle'
$form.StartPosition = "CenterScreen"
$form.AutoSizeMode = 'GrowAndShrink'
$form.Topmost = $True
$form.ShowInTaskbar = $true
# Create the Email form.
$Emailform = New-Object System.Windows.Forms.Form
$Emailform.Text = "Email Report"
$Emailform.Size = New-Object System.Drawing.Size(420,200)
$Emailform.FormBorderStyle = 'FixedSingle'
$form.StartPosition = "CenterScreen"
$Emailform.AutoSizeMode = 'GrowAndShrink'
$Emailform.Topmost = $True
$Emailform.ShowInTaskbar = $true
#Select vCenter
$groupBox = New-Object System.Windows.Forms.GroupBox
$groupBox.Location = New-Object System.Drawing.Size(10,20)
$groupBox.size = New-Object System.Drawing.Size(180,80)
$groupBox.text = "Select the vCenter:"
$Form.Controls.Add($groupBox)
$RadioButton1 = New-Object System.Windows.Forms.RadioButton
$RadioButton1.Location = new-object System.Drawing.Point(15,15)
$RadioButton1.size = New-Object System.Drawing.Size(160,25)
$RadioButton1.Checked = $true
$RadioButton1.Text = "vCenter 1"
$groupBox.Controls.Add($RadioButton1)
$RadioButton2 = New-Object System.Windows.Forms.RadioButton
$RadioButton2.Location = new-object System.Drawing.Point(15,40)
$RadioButton2.size = New-Object System.Drawing.Size(160,25)
$RadioButton2.Text = "vCenter 2"
$groupBox.Controls.Add($RadioButton2)
#Select Snapshot memory
$groupBox1 = New-Object System.Windows.Forms.GroupBox
$groupBox1.Location = New-Object System.Drawing.Size(200,20)
$groupBox1.size = New-Object System.Drawing.Size(180,80)
$groupBox1.text = "Snapshot Memory:"
$Form.Controls.Add($groupBox1)
$RadioButton3 = New-Object System.Windows.Forms.RadioButton
$RadioButton3.Location = new-object System.Drawing.Point(15,15)
$RadioButton3.size = New-Object System.Drawing.Size(160,25)
$RadioButton3.Checked = $true
$RadioButton3.Text = "Snapshot Without Memory"
$groupBox1.Controls.Add($RadioButton3)
$RadioButton4 = New-Object System.Windows.Forms.RadioButton
$RadioButton4.Location = new-object System.Drawing.Point(15,40)
$RadioButton4.size = New-Object System.Drawing.Size(160,25)
$RadioButton4.Text = "Snapshot With Memory"
$groupBox1.Controls.Add($RadioButton4)
$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Size(390,20)
$label.Size = New-Object System.Drawing.Size(280,20)
$label.AutoSize = $true
$label.Text = "Enter Hostname Here..."
# Create the TextBox used to capture the user's text.
$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = New-Object System.Drawing.Size(390,40)
$textBox.Size = New-Object System.Drawing.Size(130,230)
$textBox.AcceptsReturn = $true
$textBox.AcceptsTab = $false
$textBox.Multiline = $true
$textBox.ScrollBars = 'Both'
$textbox.CharacterCasing='Upper'
# Getting Input for Snapshot SR#, Snapshot Name, Snapshot Description.
$SR_label = New-Object System.Windows.Forms.Label
$SR_label.Location = New-Object System.Drawing.Size(15,130)
$SR_label.Size = New-Object System.Drawing.Size(280,20)
$SR_label.AutoSize = $true
$SR_label.Text = "*SR#"
$SR_textBox = New-Object System.Windows.Forms.TextBox
$SR_textBox.Location = New-Object System.Drawing.Size(150,130)
$SR_textBox.Size = New-Object System.Drawing.Size(130,20)
$SR_textBox.AcceptsReturn = $true
$SR_textBox.AcceptsTab = $false
$SR_textbox.CharacterCasing='Upper'
$SN_label = New-Object System.Windows.Forms.Label
$SN_label.Location = New-Object System.Drawing.Size(15,160)
$SN_label.Size = New-Object System.Drawing.Size(280,20)
$SN_label.AutoSize = $true
$SN_label.Text = "*Snapshot Name"
$SN_textBox = New-Object System.Windows.Forms.TextBox
$SN_textBox.Location = New-Object System.Drawing.Size(150,160)
$SN_textBox.Size = New-Object System.Drawing.Size(200,20)
$SN_textBox.AcceptsReturn = $true
$SN_textBox.AcceptsTab = $false
#$SN_textbox.CharacterCasing='Upper'
$SD_label = New-Object System.Windows.Forms.Label
$SD_label.Location = New-Object System.Drawing.Size(15,190)
$SD_label.Size = New-Object System.Drawing.Size(280,20)
$SD_label.AutoSize = $true
$SD_label.Text = "Snapshot Description"
$SD_textBox = New-Object System.Windows.Forms.TextBox
$SD_textBox.Location = New-Object System.Drawing.Size(150,190)
$SD_textBox.Size = New-Object System.Drawing.Size(200,50)
$SD_textBox.AcceptsReturn = $true
$SD_textBox.AcceptsTab = $false
$SD_textBox.Multiline = $true
$SD_textBox.ScrollBars = 'Both'
#$SD_textbox.CharacterCasing='Upper'
#Create the Hardening Button.
$HButton = New-Object System.Windows.Forms.Button
$HButton.Location = New-Object System.Drawing.Size(540,40)
$HButton.Size = New-Object System.Drawing.Size(100,40)
$HButton.Text = "Take Snapshot"
#Create the Report Button.
$RButton = New-Object System.Windows.Forms.Button
$RButton.Location = New-Object System.Drawing.Size(540,90)
$RButton.Size = New-Object System.Drawing.Size(100,40)
$RButton.Text = "Generate Report"
#Create the Report Button.
$EmailButton = New-Object System.Windows.Forms.Button
$EmailButton.Location = New-Object System.Drawing.Size(540,140)
$EmailButton.Size = New-Object System.Drawing.Size(100,40)
$EmailButton.Text = "Send Email"
#Create the Progress-Bar.
$label1 = New-Object System.Windows.Forms.Label
$label1.Location = New-Object System.Drawing.Size(20,230)
$label1.Size = New-Object System.Drawing.Size(280,20)
$label1.AutoSize = $true
$label1.Text = "Progress..."
$PB = New-Object System.Windows.Forms.ProgressBar
$PB.Name = "PowerShellProgressBar"
$PB.Value = 0
$PB.Style="Continuous"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 200 - 40
$System_Drawing_Size.Height = 20
$PB.Size = $System_Drawing_Size
$PB.Left = 20
$PB.Top = 250
#Initiate Snapshot
$HButton.Add_Click(
{
$report= @()
$counter = 0
If ($SR_textBox.TextLength -ne 0 -and $SN_textBox.TextLength -ne 0)
{
$S_Name= $SR_textBox.text+ " - " +$SN_textBox.text
}
Else{
[System.Windows.Forms.MessageBox]::Show("SR# or Snapshot Name cannot be blank", "Info")
return
}
If ($textbox.TextLength -eq 0)
{
[System.Windows.Forms.MessageBox]::Show("Server List is empty", "Info")
return
}
[System.Windows.Forms.MessageBox]::Show("Sit back and relax while Snapshot is taken !!!", "VM Snapshot")
$ServerList=$textbox.Text.Split("`n")|%{$_.trim()}
Foreach ($vm in $ServerList)
{
if($vm -eq "")
{
$counter++
[Int]$Percentage = ($Counter/$ServerList.Count)*100
$PB.Value = $Percentage
continue
}
if ($RadioButton1.Checked -eq $True)
{
Add-PSSnapin VMware.VimAutomation.Core
Connect-VIServer -Server vCenter1
}
if ($RadioButton2.Checked -eq $True)
{
Add-PSSnapin VMware.VimAutomation.Core
Connect-VIServer -Server vCenter2
}
$Exists = get-vm -name $vm -ErrorAction SilentlyContinue
If ($Exists)
{
If ($RadioButton3.Checked -eq $True)
{
Get-VM $vm |New-snapshot -Name $S_Name -Description $SD_textBox.Text
$rep = Get-VM $vm | Get-Snapshot | Select-Object @{Name='VM';Expression={$_.vm}},@{Name='Snapshot_Name';Expression={$_.name}},@{Name='Description';Expression={$_.Description}},@{Name='Created';Expression={$_.Created}},@{Name='Remarks';Expression={""}}
$report = $report + $rep
}
If ($RadioButton4.Checked -eq $True)
{
Get-VM $vm |New-snapshot -Name $S_Name -Description $SD_textBox.Text -Memory
$rep = Get-VM $vm | Get-Snapshot | Select-Object @{Name='VM';Expression={$_.vm}},@{Name='Snapshot_Name';Expression={$_.name}},@{Name='Description';Expression={$_.Description}},@{Name='Created';Expression={$_.Created}},@{Name='Remarks';Expression={""}}
$report = $report + $rep
}
}
If (!$Exists)
{
$row= New-Object PSObject -Property @{VM = $vm;Snapshot_Name = "";Description = "";Created = "";Remarks="Server not Found"}
$report += $row
}
$counter++
[Int]$Percentage = ($Counter/$ServerList.Count)*100
$PB.Value = $Percentage
}
[System.Windows.Forms.MessageBox]::Show("Snapshot taken successfully" , "Report Generation")
$report |Select-object @{Name="HOSTNAME"; Expression={$_.VM}},@{Name="Snapshot_Name"; Expression={$_.Snapshot_Name}},@{Name="Description"; Expression={$_.Description}},@{Name="Created: Date & Time"; Expression={$_.Created}},@{Name='Remarks';Expression={$_.Remarks}}| Export-Csv $file -NoTypeInformation
})
#Report Generation
$RButton.Add_Click(
{
ii $path
})
#Send Email
$EmailButton.Add_Click(
{
#Create Label
$ToLabel = New-Object System.Windows.Forms.Label
$ToLabel.Location = New-Object System.Drawing.Size(20,20)
$ToLabel.Size = New-Object System.Drawing.Size(100,20)
$ToLabel.AutoSize = $true
$ToLabelFont = New-Object Drawing.Font("Times New Roman",12,[System.Drawing.FontStyle]::Bold)
$ToLabel.Font = $ToLabelFont
$ToLabel.Text = "*To:"
$CcLabel = New-Object System.Windows.Forms.Label
$CcLabel.Location = New-Object System.Drawing.Size(20,50)
$CcLabel.Size = New-Object System.Drawing.Size(100,20)
$CcLabel.AutoSize = $true
$CcLabelFont = New-Object Drawing.Font("Times New Roman",12,[System.Drawing.FontStyle]::Bold)
$CcLabel.Font = $CcLabelFont
$CcLabel.Text = "Cc: (Optional)"
#Create the TextBox Email Address.
$ToBox = New-Object System.Windows.Forms.TextBox
$ToBox.Location = New-Object System.Drawing.Size(140,20)
$ToBox.Size = New-Object System.Drawing.Size(250,20)
$ToBoxFont = New-Object Drawing.Font("Times New Roman",8)
$ToBox.Font=$ToBoxFont
$ToBox.AcceptsReturn = $true
$ToBox.AcceptsTab = $false
#$ToBox.text=""
$ToBox.CharacterCasing='lower'
$CcBox = New-Object System.Windows.Forms.TextBox
$CcBox.Location = New-Object System.Drawing.Size(140,50)
$CcBox.Size = New-Object System.Drawing.Size(250,20)
$CcBoxFont = New-Object Drawing.Font("Times New Roman",8)
$CcBox.Font=$CcBoxFont
$CcBox.AcceptsReturn = $true
$CcBox.AcceptsTab = $false
$CcBox.text=""
$CcBox.CharacterCasing='lower'
#Create Email Send Button
$SendButton = New-Object System.Windows.Forms.Button
$SendButton.Location = New-Object System.Drawing.Size(20,100)
$SendButton.Size = New-Object System.Drawing.Size(100,40)
$ButtonFont = New-Object Drawing.Font("Times New Roman",8,[System.Drawing.FontStyle]::Bold)
$SendButton.Font=$ButtonFont
$SendButton.Text = "Send Email"
$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Size(150,100)
$CancelButton.Size = New-Object System.Drawing.Size(100,40)
$CancelButton.Font=$ButtonFont
$CancelButton.Text = "Cancel"
$Emailform.Controls.Add($ToLabel)
$Emailform.Controls.Add($CcLabel)
$Emailform.Controls.Add($ToBox)
$Emailform.Controls.Add($CCBox)
$Emailform.Controls.Add($SendButton)
$Emailform.Controls.Add($CancelButton)
$SendButton.Add_Click(
{
If ($ToBox.Text.Length -eq 0)
{
[System.Windows.Forms.MessageBox]::Show("Email address cannot be empty" , "Email Info")
return
}
#---------------------------------------------------------------------
# Generate the HTML report and output to file
#---------------------------------------------------------------------
$head = "<style>"
$head = $head + "BODY{background-color:white;}"
$head = $head + "TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"
$head = $head + "TH{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color:#778899}"
$head = $head + "TD{border-width: 1px;padding: 0px;border-style: solid;border-color: black}"
$head = $head + "</style>"
# SMTP info
$Toemail=$ToBox.Text
$strTo=$Toemail
$strCc=$CCBox.Text
$strSubject = "Snapshot Taken : $S_Name"
$StrMsg="Hi All, <br>Snapshot has been taken successfully for below list of servers <br><br>"
$strBody = "Attached is the list of Snapshots"
$strMail = $strmsg
# Write the output to an HTML file
$strOutFile = $Path+"email.html"
$ComputerName=(Get-WmiObject -Class Win32_ComputerSystem -Property Name).Name
Get-Content -Path $File |ConvertFrom-CSV | ConvertTo-HTML -Head $head -Body $StrMsg | Out-File $StrOutFile
# Mail the output file
$msg = new-object Net.Mail.MailMessage
$att = new-object Net.Mail.Attachment($File)
$smtp = new-object Net.Mail.SmtpClient("SMTP Server")
$msg.From ="From@domain.com"
$msg.To.Add($strTo)
If ($strCc.Length -ne 0)
{
$msg.cc.Add($strcc)
}
$msg.Subject = $strSubject
$msg.IsBodyHtml = 1
$msg.Body = Get-Content $strOutFile
$msg.Attachments.Add($att)
$smtp.Send($msg)
[System.Windows.Forms.MessageBox]::Show("Email Send !!!","Info")
$Emailform.close()
})
$CancelButton.Add_Click(
{
$Emailform.close()
})
$Emailform.Add_Shown({$Emailform.Activate()})
$Emailform.ShowDialog() > $null
})
# Add all of the controls to the form.
$form.Controls.Add($label)
$form.Controls.Add($textBox)
$form.Controls.Add($groupBox)
$form.Controls.Add($groupBox1)
$form.Controls.Add($RButton)
$form.Controls.Add($HButton)
$form.Controls.Add($label1)
$form.Controls.Add($SR_label)
$form.Controls.Add($SR_textBox)
$form.Controls.Add($SN_label)
$form.Controls.Add($SN_textBox)
$form.Controls.Add($SD_label)
$form.Controls.Add($SD_textBox)
$form.Controls.Add($PB)
$form.Controls.Add($EmailButton)
# Initialize and show the form.
$form.Add_Shown({$form.Activate()})
$form.ShowDialog() > $null
}
Set-ExecutionPolicy unrestricted -Force
$date=get-Date -format "ddMMyy_HHmm"
$ComputerName=(Get-WmiObject -Class Win32_ComputerSystem -Property Name).Name
$Path=[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Desktop)+"\Report\"
If ((Test-Path $Path) -eq $false)
{
New-Item $Path -type directory
}
New-Item -ErrorAction Ignore -ItemType directory -Path Report
$File=$Path + "SnapshotReport_$date.csv"
Snapshot</pre>
<!-- /wp:syntaxhighlighter/code -->
🛠️ How to use the GUI:
- Select your vCenter: Choose which environment the VMs live in.
- Snapshot Specs: Enter the SR# (Service Request) and a Name. These are mandatory to keep your environment organized.
- Input Servers: Paste your list of servers (one per line) into the right-hand text box.
- Hit ‘Take Snapshot’: The tool will cycle through the list. If a server isn’t found, it won’t crash; it just marks it as “Server not Found” in your final report.
- Send Email: Once finished, click ‘Send Email’ to notify your team that the work is done.
Why this belongs in your toolkit:
The “Lazy Admin” way is about standardization. By using this GUI, every snapshot taken by your team will follow the same naming convention: SR# - Snapshot Name. No more guessing what “Snap1” or “Update_v2” means six months from now.
🛠️ The Refactor: Adding the “Cleanup” Tab
In the code below, I’ve introduced the TabControl and TabPage classes. The new Cleanup Tab includes a feature every admin needs: Age-Based Filtering. It can scan for snapshots older than 2 or 3 days and wipe them in bulk.
<!-- wp:syntaxhighlighter/code -->
<pre class="wp-block-syntaxhighlighter-code">Function Snapshot()
{
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
# Create the Main form.
$form = New-Object System.Windows.Forms.Form
$form.Text = "VM Snapshot"
$form.Size = New-Object System.Drawing.Size(650,320)
$form.FormBorderStyle = 'FixedSingle'
$form.StartPosition = "CenterScreen"
$form.AutoSizeMode = 'GrowAndShrink'
$form.Topmost = $True
$form.ShowInTaskbar = $true
# Create the Email form.
$Emailform = New-Object System.Windows.Forms.Form
$Emailform.Text = "Email Report"
$Emailform.Size = New-Object System.Drawing.Size(420,200)
$Emailform.FormBorderStyle = 'FixedSingle'
$form.StartPosition = "CenterScreen"
$Emailform.AutoSizeMode = 'GrowAndShrink'
$Emailform.Topmost = $True
$Emailform.ShowInTaskbar = $true
#Select vCenter
$groupBox = New-Object System.Windows.Forms.GroupBox
$groupBox.Location = New-Object System.Drawing.Size(10,20)
$groupBox.size = New-Object System.Drawing.Size(180,80)
$groupBox.text = "Select the vCenter:"
$Form.Controls.Add($groupBox)
$RadioButton1 = New-Object System.Windows.Forms.RadioButton
$RadioButton1.Location = new-object System.Drawing.Point(15,15)
$RadioButton1.size = New-Object System.Drawing.Size(160,25)
$RadioButton1.Checked = $true
$RadioButton1.Text = "vCenter 1"
$groupBox.Controls.Add($RadioButton1)
$RadioButton2 = New-Object System.Windows.Forms.RadioButton
$RadioButton2.Location = new-object System.Drawing.Point(15,40)
$RadioButton2.size = New-Object System.Drawing.Size(160,25)
$RadioButton2.Text = "vCenter 2"
$groupBox.Controls.Add($RadioButton2)
#Select Snapshot memory
$groupBox1 = New-Object System.Windows.Forms.GroupBox
$groupBox1.Location = New-Object System.Drawing.Size(200,20)
$groupBox1.size = New-Object System.Drawing.Size(180,80)
$groupBox1.text = "Snapshot Memory:"
$Form.Controls.Add($groupBox1)
$RadioButton3 = New-Object System.Windows.Forms.RadioButton
$RadioButton3.Location = new-object System.Drawing.Point(15,15)
$RadioButton3.size = New-Object System.Drawing.Size(160,25)
$RadioButton3.Checked = $true
$RadioButton3.Text = "Snapshot Without Memory"
$groupBox1.Controls.Add($RadioButton3)
$RadioButton4 = New-Object System.Windows.Forms.RadioButton
$RadioButton4.Location = new-object System.Drawing.Point(15,40)
$RadioButton4.size = New-Object System.Drawing.Size(160,25)
$RadioButton4.Text = "Snapshot With Memory"
$groupBox1.Controls.Add($RadioButton4)
$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Size(390,20)
$label.Size = New-Object System.Drawing.Size(280,20)
$label.AutoSize = $true
$label.Text = "Enter Hostname Here..."
# Create the TextBox used to capture the user's text.
$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = New-Object System.Drawing.Size(390,40)
$textBox.Size = New-Object System.Drawing.Size(130,230)
$textBox.AcceptsReturn = $true
$textBox.AcceptsTab = $false
$textBox.Multiline = $true
$textBox.ScrollBars = 'Both'
$textbox.CharacterCasing='Upper'
# Getting Input for Snapshot SR#, Snapshot Name, Snapshot Description.
$SR_label = New-Object System.Windows.Forms.Label
$SR_label.Location = New-Object System.Drawing.Size(15,130)
$SR_label.Size = New-Object System.Drawing.Size(280,20)
$SR_label.AutoSize = $true
$SR_label.Text = "*SR#"
$SR_textBox = New-Object System.Windows.Forms.TextBox
$SR_textBox.Location = New-Object System.Drawing.Size(150,130)
$SR_textBox.Size = New-Object System.Drawing.Size(130,20)
$SR_textBox.AcceptsReturn = $true
$SR_textBox.AcceptsTab = $false
$SR_textbox.CharacterCasing='Upper'
$SN_label = New-Object System.Windows.Forms.Label
$SN_label.Location = New-Object System.Drawing.Size(15,160)
$SN_label.Size = New-Object System.Drawing.Size(280,20)
$SN_label.AutoSize = $true
$SN_label.Text = "*Snapshot Name"
$SN_textBox = New-Object System.Windows.Forms.TextBox
$SN_textBox.Location = New-Object System.Drawing.Size(150,160)
$SN_textBox.Size = New-Object System.Drawing.Size(200,20)
$SN_textBox.AcceptsReturn = $true
$SN_textBox.AcceptsTab = $false
#$SN_textbox.CharacterCasing='Upper'
$SD_label = New-Object System.Windows.Forms.Label
$SD_label.Location = New-Object System.Drawing.Size(15,190)
$SD_label.Size = New-Object System.Drawing.Size(280,20)
$SD_label.AutoSize = $true
$SD_label.Text = "Snapshot Description"
$SD_textBox = New-Object System.Windows.Forms.TextBox
$SD_textBox.Location = New-Object System.Drawing.Size(150,190)
$SD_textBox.Size = New-Object System.Drawing.Size(200,50)
$SD_textBox.AcceptsReturn = $true
$SD_textBox.AcceptsTab = $false
$SD_textBox.Multiline = $true
$SD_textBox.ScrollBars = 'Both'
#$SD_textbox.CharacterCasing='Upper'
#Create the Hardening Button.
$HButton = New-Object System.Windows.Forms.Button
$HButton.Location = New-Object System.Drawing.Size(540,40)
$HButton.Size = New-Object System.Drawing.Size(100,40)
$HButton.Text = "Take Snapshot"
#Create the Report Button.
$RButton = New-Object System.Windows.Forms.Button
$RButton.Location = New-Object System.Drawing.Size(540,90)
$RButton.Size = New-Object System.Drawing.Size(100,40)
$RButton.Text = "Generate Report"
#Create the Report Button.
$EmailButton = New-Object System.Windows.Forms.Button
$EmailButton.Location = New-Object System.Drawing.Size(540,140)
$EmailButton.Size = New-Object System.Drawing.Size(100,40)
$EmailButton.Text = "Send Email"
#Create the Progress-Bar.
$label1 = New-Object System.Windows.Forms.Label
$label1.Location = New-Object System.Drawing.Size(20,230)
$label1.Size = New-Object System.Drawing.Size(280,20)
$label1.AutoSize = $true
$label1.Text = "Progress..."
$PB = New-Object System.Windows.Forms.ProgressBar
$PB.Name = "PowerShellProgressBar"
$PB.Value = 0
$PB.Style="Continuous"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Width = 200 - 40
$System_Drawing_Size.Height = 20
$PB.Size = $System_Drawing_Size
$PB.Left = 20
$PB.Top = 250
#Initiate Snapshot
$HButton.Add_Click(
{
$report= @()
$counter = 0
If ($SR_textBox.TextLength -ne 0 -and $SN_textBox.TextLength -ne 0)
{
$S_Name= $SR_textBox.text+ " - " +$SN_textBox.text
}
Else{
[System.Windows.Forms.MessageBox]::Show("SR# or Snapshot Name cannot be blank", "Info")
return
}
If ($textbox.TextLength -eq 0)
{
[System.Windows.Forms.MessageBox]::Show("Server List is empty", "Info")
return
}
[System.Windows.Forms.MessageBox]::Show("Sit back and relax while Snapshot is taken !!!", "VM Snapshot")
$ServerList=$textbox.Text.Split("`n")|%{$_.trim()}
Foreach ($vm in $ServerList)
{
if($vm -eq "")
{
$counter++
[Int]$Percentage = ($Counter/$ServerList.Count)*100
$PB.Value = $Percentage
continue
}
if ($RadioButton1.Checked -eq $True)
{
Add-PSSnapin VMware.VimAutomation.Core
Connect-VIServer -Server vCenter1
}
if ($RadioButton2.Checked -eq $True)
{
Add-PSSnapin VMware.VimAutomation.Core
Connect-VIServer -Server vCenter2
}
$Exists = get-vm -name $vm -ErrorAction SilentlyContinue
If ($Exists)
{
If ($RadioButton3.Checked -eq $True)
{
Get-VM $vm |New-snapshot -Name $S_Name -Description $SD_textBox.Text
$rep = Get-VM $vm | Get-Snapshot | Select-Object @{Name='VM';Expression={$_.vm}},@{Name='Snapshot_Name';Expression={$_.name}},@{Name='Description';Expression={$_.Description}},@{Name='Created';Expression={$_.Created}},@{Name='Remarks';Expression={""}}
$report = $report + $rep
}
If ($RadioButton4.Checked -eq $True)
{
Get-VM $vm |New-snapshot -Name $S_Name -Description $SD_textBox.Text -Memory
$rep = Get-VM $vm | Get-Snapshot | Select-Object @{Name='VM';Expression={$_.vm}},@{Name='Snapshot_Name';Expression={$_.name}},@{Name='Description';Expression={$_.Description}},@{Name='Created';Expression={$_.Created}},@{Name='Remarks';Expression={""}}
$report = $report + $rep
}
}
If (!$Exists)
{
$row= New-Object PSObject -Property @{VM = $vm;Snapshot_Name = "";Description = "";Created = "";Remarks="Server not Found"}
$report += $row
}
$counter++
[Int]$Percentage = ($Counter/$ServerList.Count)*100
$PB.Value = $Percentage
}
[System.Windows.Forms.MessageBox]::Show("Snapshot taken successfully" , "Report Generation")
$report |Select-object @{Name="HOSTNAME"; Expression={$_.VM}},@{Name="Snapshot_Name"; Expression={$_.Snapshot_Name}},@{Name="Description"; Expression={$_.Description}},@{Name="Created: Date & Time"; Expression={$_.Created}},@{Name='Remarks';Expression={$_.Remarks}}| Export-Csv $file -NoTypeInformation
})
#Report Generation
$RButton.Add_Click(
{
ii $path
})
#Send Email
$EmailButton.Add_Click(
{
#Create Label
$ToLabel = New-Object System.Windows.Forms.Label
$ToLabel.Location = New-Object System.Drawing.Size(20,20)
$ToLabel.Size = New-Object System.Drawing.Size(100,20)
$ToLabel.AutoSize = $true
$ToLabelFont = New-Object Drawing.Font("Times New Roman",12,[System.Drawing.FontStyle]::Bold)
$ToLabel.Font = $ToLabelFont
$ToLabel.Text = "*To:"
$CcLabel = New-Object System.Windows.Forms.Label
$CcLabel.Location = New-Object System.Drawing.Size(20,50)
$CcLabel.Size = New-Object System.Drawing.Size(100,20)
$CcLabel.AutoSize = $true
$CcLabelFont = New-Object Drawing.Font("Times New Roman",12,[System.Drawing.FontStyle]::Bold)
$CcLabel.Font = $CcLabelFont
$CcLabel.Text = "Cc: (Optional)"
#Create the TextBox Email Address.
$ToBox = New-Object System.Windows.Forms.TextBox
$ToBox.Location = New-Object System.Drawing.Size(140,20)
$ToBox.Size = New-Object System.Drawing.Size(250,20)
$ToBoxFont = New-Object Drawing.Font("Times New Roman",8)
$ToBox.Font=$ToBoxFont
$ToBox.AcceptsReturn = $true
$ToBox.AcceptsTab = $false
#$ToBox.text=""
$ToBox.CharacterCasing='lower'
$CcBox = New-Object System.Windows.Forms.TextBox
$CcBox.Location = New-Object System.Drawing.Size(140,50)
$CcBox.Size = New-Object System.Drawing.Size(250,20)
$CcBoxFont = New-Object Drawing.Font("Times New Roman",8)
$CcBox.Font=$CcBoxFont
$CcBox.AcceptsReturn = $true
$CcBox.AcceptsTab = $false
$CcBox.text=""
$CcBox.CharacterCasing='lower'
#Create Email Send Button
$SendButton = New-Object System.Windows.Forms.Button
$SendButton.Location = New-Object System.Drawing.Size(20,100)
$SendButton.Size = New-Object System.Drawing.Size(100,40)
$ButtonFont = New-Object Drawing.Font("Times New Roman",8,[System.Drawing.FontStyle]::Bold)
$SendButton.Font=$ButtonFont
$SendButton.Text = "Send Email"
$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Size(150,100)
$CancelButton.Size = New-Object System.Drawing.Size(100,40)
$CancelButton.Font=$ButtonFont
$CancelButton.Text = "Cancel"
$Emailform.Controls.Add($ToLabel)
$Emailform.Controls.Add($CcLabel)
$Emailform.Controls.Add($ToBox)
$Emailform.Controls.Add($CCBox)
$Emailform.Controls.Add($SendButton)
$Emailform.Controls.Add($CancelButton)
$SendButton.Add_Click(
{
If ($ToBox.Text.Length -eq 0)
{
[System.Windows.Forms.MessageBox]::Show("Email address cannot be empty" , "Email Info")
return
}
#---------------------------------------------------------------------
# Generate the HTML report and output to file
#---------------------------------------------------------------------
$head = "<style>"
$head = $head + "BODY{background-color:white;}"
$head = $head + "TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"
$head = $head + "TH{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color:#778899}"
$head = $head + "TD{border-width: 1px;padding: 0px;border-style: solid;border-color: black}"
$head = $head + "</style>"
# SMTP info
$Toemail=$ToBox.Text
$strTo=$Toemail
$strCc=$CCBox.Text
$strSubject = "Snapshot Taken : $S_Name"
$StrMsg="Hi All, <br>Snapshot has been taken successfully for below list of servers <br><br>"
$strBody = "Attached is the list of Snapshots"
$strMail = $strmsg
# Write the output to an HTML file
$strOutFile = $Path+"email.html"
$ComputerName=(Get-WmiObject -Class Win32_ComputerSystem -Property Name).Name
Get-Content -Path $File |ConvertFrom-CSV | ConvertTo-HTML -Head $head -Body $StrMsg | Out-File $StrOutFile
# Mail the output file
$msg = new-object Net.Mail.MailMessage
$att = new-object Net.Mail.Attachment($File)
$smtp = new-object Net.Mail.SmtpClient("SMTP Server")
$msg.From ="From@domain.com"
$msg.To.Add($strTo)
If ($strCc.Length -ne 0)
{
$msg.cc.Add($strcc)
}
$msg.Subject = $strSubject
$msg.IsBodyHtml = 1
$msg.Body = Get-Content $strOutFile
$msg.Attachments.Add($att)
$smtp.Send($msg)
[System.Windows.Forms.MessageBox]::Show("Email Send !!!","Info")
$Emailform.close()
})
$CancelButton.Add_Click(
{
$Emailform.close()
})
$Emailform.Add_Shown({$Emailform.Activate()})
$Emailform.ShowDialog() > $null
})
# Add all of the controls to the form.
$form.Controls.Add($label)
$form.Controls.Add($textBox)
$form.Controls.Add($groupBox)
$form.Controls.Add($groupBox1)
$form.Controls.Add($RButton)
$form.Controls.Add($HButton)
$form.Controls.Add($label1)
$form.Controls.Add($SR_label)
$form.Controls.Add($SR_textBox)
$form.Controls.Add($SN_label)
$form.Controls.Add($SN_textBox)
$form.Controls.Add($SD_label)
$form.Controls.Add($SD_textBox)
$form.Controls.Add($PB)
$form.Controls.Add($EmailButton)
# Initialize and show the form.
$form.Add_Shown({$form.Activate()})
$form.ShowDialog() > $null
}
Set-ExecutionPolicy unrestricted -Force
$date=get-Date -format "ddMMyy_HHmm"
$ComputerName=(Get-WmiObject -Class Win32_ComputerSystem -Property Name).Name
$Path=[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Desktop)+"\Report\"
If ((Test-Path $Path) -eq $false)
{
New-Item $Path -type directory
}
New-Item -ErrorAction Ignore -ItemType directory -Path Report
$File=$Path + "SnapshotReport_$date.csv"
Snapshot</pre>
<!-- /wp:syntaxhighlighter/code -->
💡 Why this is a “Lazy” Win:
- Tab Isolation: You won’t accidentally delete snapshots while trying to create them.
- RunAsync Switch: I added
-RunAsyncto the deletion. This means the GUI won’t “freeze” while vCenter is doing the heavy lifting of disk consolidation. - Standardized Cleanup: By hardcoding
$Days = 2, you ensure that your team follows a consistent 48-hour retention policy.
Guide to the Entra ID Passkey Rollout (March 2026) | Lazy Admin Blog

How to avoid 500 helpdesk tickets by spending 10 minutes in the Admin Center today.
If you woke up this morning to find a new “Default Passkey Profile” in your Entra tenant, don’t panic. Microsoft is officially “encouraging” (read: forcing) the world toward phishing-resistant auth. As a Lazy Admin, your goal isn’t to fight the change—it’s to control it so it doesn’t control your weekend.
1. The Big Change: Passkey Profiles
Previously, FIDO2 was a simple “On/Off” switch. Now, we have Passkey Profiles.
- The Default Behavior: If you didn’t opt-in, Microsoft has created a “Default Profile” for you.
- The Trap: If you had “Enforce Attestation” set to No, Microsoft is now allowing Synced Passkeys (iCloud Keychain, Google Password Manager). This means users can put corporate credentials on their personal iPhones.
2. The “Lazy” Strategy: Tiered Security
Don’t treat your CEO and your Summer Intern the same way. Use the new Group-Based Profiles to save yourself the headache of “One Size Fits None.”
| User Group | Recommended Profile | Why? |
| IT Admins | Device-Bound Only | Requires a physical YubiKey or Windows Hello. No syncing to the cloud. |
| Standard Users | Synced & Device-Bound | Maximum convenience. If they lose their phone, iCloud/Google restores the key. Zero helpdesk calls. |
| Contractors | AAGUID Restricted | Only allow specific hardware models you’ve issued to them. |
3. Avoid the “Registration Deadlock”
Many admins are seeing “Helpdesk Hell” because their Conditional Access (CA) policies are too strict.
The Problem: You have a policy requiring “Phishing-Resistant MFA” to access “All Apps.” A user tries to register a passkey, but they can’t log in to the registration page because… they don’t have a passkey yet.
The Lazy Fix: Exclude the “Register security information” user action from your strictest CA policies, or better yet, issue a Temporary Access Pass (TAP) for 24 hours. A TAP satisfies the MFA requirement and lets them onboard themselves without calling you.
🛠️ The 5-Minute “Lazy Admin” Checklist
- [ ] Check your Attestation: Go to Security > Authentication Methods > Passkey (FIDO2). If you want to block personal iPhones, set Enforce Attestation to Yes.
- [ ] Kill the Nudges: If you aren’t ready for the rollout, disable the “Microsoft-managed” registration campaigns before they start bugging your users on Monday.
- [ ] Review AAGUIDs: If you only use YubiKeys, make sure their AAGUIDs are explicitly whitelisted in your Admin profile.
Bottom Line: Spend 10 minutes setting up your profiles today, or spend 10 hours resetting MFA sessions next week. Choose wisely.
- ← Previous
- 1
- 2