How to Export Folder and Share Permissions to CSV via PowerShell

Posted on Updated on

Auditing file share permissions is a critical task for security and compliance. While the Windows GUI allows you to view permissions one folder at a time, it is impossible to get a “big picture” view without automation.

By using the Get-Acl (Access Control List) cmdlet in PowerShell, you can recursively scan a directory and export every user and group permission to a clean CSV file for review in Excel.


The PowerShell Script

Save the following code as ExportFolderPermissions.ps1. Before running it, ensure you update the $FolderPath and the output path for the CSV file.

PowerShell
# Define the source path (Local folder or UNC Share)
$FolderPath = dir -Directory -Path "\\ServerName\SharePath" -Recurse -Force
$Report = @()
Foreach ($Folder in $FolderPath) {
# Fetch the Access Control List for the current folder
$Acl = Get-Acl -Path $Folder.FullName
foreach ($Access in $acl.Access) {
# Create an ordered object for each permission entry
$Properties = [ordered]@{
'FolderName' = $Folder.FullName
'AD Group or User' = $Access.IdentityReference
'Permissions' = $Access.FileSystemRights
'Inherited' = $Access.IsInherited
}
$Report += New-Object -TypeName PSObject -Property $Properties
}
}
# Export the final report to CSV
$Report | Export-Csv -Path "C:\Temp\FolderPermissions.csv" -NoTypeInformation

How the Script Works

  1. dir -Recurse: This command crawls through every subfolder within your target directory. The -Directory switch ensures we only look at folders, not individual files (which would make the report massive).
  2. Get-Acl: This retrieves the security descriptor for the folder, including who has access and what specific rights they have (Read, Write, Full Control, etc.).
  3. PSObject: We bundle the folder name, user identity, and rights into a custom object so that Export-Csv can easily format them into columns.
  4. IdentityReference: This shows you the exact AD Group or User name assigned to that folder.

💡 Lazy Admin Tips

  • Run as Admin: You must run PowerShell as an Administrator and have “Read Permissions” rights on the target folders, or the script will return “Access Denied” errors.
  • Performance: Scanning thousands of subfolders over a slow network link can take time. If you have a massive file server, run the script locally on the server itself rather than over a mapped drive.
  • Filter Results: Once you open the CSV in Excel, use Filters to quickly find “Everyone” or “Anonymous” permissions, or to see which folders have inheritance disabled.

#PowerShell #SysAdmin #ActiveDirectory #SecurityAudit #WindowsServer #ITPro #Coding #LazyAdmin #CyberSecurity #TechTips

Mapping Your AD: VBScript to List OUs in Parent-Child Order | Lazy Admin Blog

Posted on Updated on

When you’re managing a complex Active Directory environment, getting a clear “birds-eye view” of your structure is essential. While the Active Directory Users & Computers (dsa.msc) snap-in is great for manual navigation, sometimes you need a flat text output that preserves the visual hierarchy of your Organizational Units (OUs).

The following VBScript crawls your LDAP directory and mirrors the parent-child nesting you see in your GUI tools.


📜 The Script: ListAllOUs_ParentChild.vbs

Copy the code below and save it as ListAllOUs_ParentChild.vbs.

VBScript
Option Explicit
Const ADS_SCOPE_SUBTREE = 2
Dim ObjConn, ObjRS, ObjRootDSE
Dim StrSQL, StrDomName, ObjOU
' Get the local domain name
Set ObjRootDSE = GetObject("LDAP://RootDSE")
StrDomName = Trim(ObjRootDSE.Get("DefaultNamingContext"))
Set ObjRootDSE = Nothing
' SQL Query to find OUs (Excluding Domain Controllers)
StrSQL = "Select Name, ADsPath From 'LDAP://" & StrDomName & "' Where ObjectCategory = 'OrganizationalUnit' And Name <> 'Domain Controllers'"
Set ObjConn = CreateObject("ADODB.Connection")
ObjConn.Provider = "ADsDSOObject"
ObjConn.Open "Active Directory Provider"
Set ObjRS = CreateObject("ADODB.Recordset")
ObjRS.Open StrSQL, ObjConn
If Not ObjRS.EOF Then
ObjRS.MoveLast: ObjRS.MoveFirst
WScript.Echo vbNullString
WScript.Echo "Total OU: " & Trim(ObjRS.RecordCount)
WScript.Echo "==================="
WScript.Echo vbNullString
While Not ObjRS.EOF
Set ObjOU = GetObject(Trim(ObjRS.Fields("ADsPath").Value))
' Check if it's a top-level Parent OU
If StrComp(Right(Trim(ObjOU.Parent), Len(Trim(ObjOU.Parent)) - 7), StrDomName, VbTextCompare) = 0 Then
WScript.Echo "Parent OU: " & Trim(ObjRS.Fields("Name").Value)
GetChild(ObjOU)
End If
ObjRS.MoveNext
Set ObjOU = Nothing
Wend
End If
ObjRS.Close: Set ObjRS = Nothing
ObjConn.Close: Set ObjConn = Nothing
' Subroutine to find first-level children
Private Sub GetChild(ThisObject)
Dim ObjChild
For Each ObjChild In ThisObject
If StrComp(Trim(ObjChild.Class), "OrganizationalUnit", VbTextCompare) = 0 Then
WScript.Echo vbTab & ">> Child OU: " & Right(Trim(ObjChild.Name), Len(Trim(ObjChild.Name)) - 3)
GetGrandChild (ObjChild.ADsPath)
End If
Next
End Sub
' Recursive subroutine to find all nested children
Private Sub GetGrandChild (ThisADsPath)
Dim ObjGrand, ObjItem
Set ObjGrand = GetObject(ThisADsPath)
For Each ObjItem In ObjGrand
If StrComp(Trim(ObjItem.Class), "OrganizationalUnit", VbTextCompare) = 0 Then
WScript.Echo vbTab & vbTab & ">> Child OU: " & Right(Trim(ObjItem.Name), Len(Trim(ObjItem.Name)) - 3)
GetGrandChild Trim(ObjItem.ADsPath)
End If
Next
Set ObjGrand = Nothing
End Sub

🚀 How to Execute

To run this script correctly and avoid “Windows Script Host” popup boxes for every line, you must use the command-line engine (CScript).

Example Command: CScript /NoLogo ListAllOUs_ParentChild.vbs

Output Preview:

Parent OU: Sales

Child OU: North_Region

Child OU: South_Region

>> Child OU: Retail_Stores

#ActiveDirectory #SysAdmin #WindowsServer #Automation #VBScript #ITAdmin #LazyAdmin #LDAP #DirectoryServices #InfrastructureAsCode #Scripting #ADUC

Deep Audit: Listing Nested Active Directory Group Members via VBScript | Lazy Admin Blog

Posted on Updated on

Have you ever looked at a “Domain Admins” group and thought it looked suspiciously small? The culprit is usually nesting. Standard AD queries often fail to “recurse,” meaning they show you the subgroup but not the people inside it.

This script, ListGroupMembers_IncludingNested.vbs, uses a recursive function to dive into every sub-group and extract the actual users, ensuring your security audits are 100% accurate.

The Script: How it Works

The script utilizes a Dictionary Object to keep track of groups it has already scanned. This is a critical “Lazy Admin” safety feature—it prevents the script from getting stuck in an infinite loop if two groups are members of each other.

Usage Instructions

  1. Copy the code below into Notepad.
  2. Edit the StrGroupName variable to match your target group.
  3. Save the file as ListGroupMembers.vbs.
  4. Run it from the command prompt using cscript ListGroupMembers.vbs.
VBScript
' -- Save as ListGroupMembers_IncludingNested.vbs
Option Explicit
Dim ObjRootDSE, ObjConn, ObjRS, ObjCustom
Dim StrDomainName, StrGroupName, StrSQL, StrGroupDN, StrEmptySpace
Set ObjRootDSE = GetObject("LDAP://RootDSE")
StrDomainName = Trim(ObjRootDSE.Get("DefaultNamingContext"))
' -- Edit the line below with your Group Name
StrGroupName = "YourGroupNameHere"
StrSQL = "Select ADsPath From 'LDAP://" & StrDomainName & "' Where ObjectCategory = 'Group' AND Name = '" & StrGroupName & "'"
Set ObjConn = CreateObject("ADODB.Connection")
ObjConn.Provider = "ADsDSOObject": ObjConn.Open "Active Directory Provider"
Set ObjRS = ObjConn.Execute(StrSQL)
If ObjRS.EOF Then
WScript.Echo "Group not found: " & StrGroupName
Else
StrGroupDN = Trim(ObjRS.Fields("ADsPath").Value)
Set ObjCustom = CreateObject("Scripting.Dictionary")
GetAllNestedMembers StrGroupDN, " ", ObjCustom
End If

Why VBScript in 2026?

While PowerShell is the modern standard, many legacy environments and automated scheduled tasks still rely on VBScript because it requires zero execution policy changes and runs natively on every Windows machine since Server 2000. It is the “Old Reliable” of the AD world.

Key Features of this Script

  • Recursive Discovery: It doesn’t just stop at the first layer.
  • Class Identification: Clearly marks if a member is a User, Computer, or another Group.
  • Loop Protection: Uses the Scripting.Dictionary to escape circular nesting traps.

#ActiveDirectory #WindowsServer #CyberSecurity #SysAdmin #ITAudit #VBScript #Automation #LazyAdmin #TechArchive

The Ultimate Server Audit: Deep-Dive Inventory to Excel (VBScript) | Lazy Admin Blog

Posted on Updated on

If you are facing a massive compliance audit or a data center migration, “basic” info isn’t enough. You need to know exactly what is under the hood: What roles are active? How much disk space is actually left? What random software was installed three years ago?

This VBScript is a one-stop-shop. It checks network connectivity and then scrapes WMI and the Registry to build a massive, multi-column Excel report.

What this Script Collects:

  • Hardware: Manufacturer, Model, CPU Type, and RAM (converted to GB).
  • OS Details: Version, Caption, and the exact Installation Date.
  • Storage: Total Size vs. Free Space (with a Red-Alert highlight if space is < 20%).
  • Network: DHCP status, IP, Subnet, and Gateway.
  • Software & Roles: Every Windows Server Role/Feature and every application listed in the Registry’s Uninstall key (including version and install date).

Preparation

  1. Directory: Create C:\Temp on your local machine.
  2. Input: Create a file named ServerList.txt in C:\Temp with your server names (one per line).
  3. Excel: Ensure Microsoft Excel is installed.

The Script: Server_Inventory.vbs

VBScript
' Save as Server_Inventory.vbs in C:\Temp
' lazyadminblog.com - Ultimate Inventory Script
On Error Resume Next
dtmDate = Date
strMonth = Month(Date)
strDay = Day(Date)
strYear = Right(Year(Date),2)
strFileName = "C:\Temp\ServerInventory_" & strMonth & "-" & strDay & "-" & strYear & ".xls"
Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.Workbooks.Add
Set fso1 = CreateObject("Scripting.FileSystemObject")
Set pcfile = fso1.OpenTextFile("C:\Temp\ServerList.txt",1)
Wscript.Echo "Audit in progress... Please wait!"
'--- Setup Header Row ---
Sub SetupHeader(col, text)
objExcel.Cells(1, col).Value = text
objExcel.Cells(1, col).Font.Colorindex = 2
objExcel.Cells(1, col).Font.Bold = True
objExcel.Cells(1, col).Interior.ColorIndex = 23
objExcel.Cells(1, col).Alignment = -4108 ' Center
End Sub
SetupHeader 1, "Computer Name"
SetupHeader 2, "Manufacturer"
SetupHeader 3, "Model"
SetupHeader 4, "RAM (GB)"
SetupHeader 5, "Operating System"
SetupHeader 6, "Installed Date"
SetupHeader 7, "Processor"
SetupHeader 8, "Drive"
SetupHeader 9, "Drive Size (GB)"
SetupHeader 10, "Free Space (GB)"
SetupHeader 11, "Adapter Description"
SetupHeader 12, "DHCP Enabled"
SetupHeader 13, "IP Address"
SetupHeader 14, "Subnet"
SetupHeader 15, "Gateway"
SetupHeader 16, "Roles & Features"
SetupHeader 17, "Installed Software"
SetupHeader 18, "Install Date"
SetupHeader 19, "Version"
SetupHeader 20, "Size"
y = 2
Do While Not pcfile.AtEndOfStream
computerName = pcfile.ReadLine
Err.Clear
Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & computerName & "\root\cimv2")
If Err.Number = 0 Then
' Fetch Queries
Set colSettings = objWMIService.ExecQuery("SELECT * FROM Win32_ComputerSystem")
Set colOSSettings = objWMIService.ExecQuery("SELECT * FROM Win32_OperatingSystem")
Set colProcSettings = objWMIService.ExecQuery("SELECT * FROM Win32_Processor")
Set colDiskSettings = objWMIService.ExecQuery("Select * from Win32_LogicalDisk Where DriveType=3")
Set colAdapters = objWMIService.ExecQuery("SELECT * FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = True")
For Each objComputer In colSettings
strManufacturer = objComputer.Manufacturer
strModel = objComputer.Model
strRAM = FormatNumber((objComputer.TotalPhysicalMemory / (1024^3)), 2)
For Each objOS In colOSSettings
strOS = objOS.Caption
OSinstDate = CDate(Mid(objOS.InstallDate,1,4)+"/"+Mid(objOS.InstallDate,5,2)+"/"+Mid(objOS.InstallDate,7,2))
For Each objProc In colProcSettings
strProc = objProc.Name
' Populate Static Info
objExcel.Cells(y, 1).Value = computerName
objExcel.Cells(y, 2).Value = strManufacturer
objExcel.Cells(y, 3).Value = strModel
objExcel.Cells(y, 4).Value = strRAM
objExcel.Cells(y, 5).Value = strOS
objExcel.Cells(y, 6).Value = OSinstDate
objExcel.Cells(y, 7).Value = strProc
' Drive Logic
a = y
For Each objDisk In colDiskSettings
objExcel.Cells(a, 8).Value = objDisk.DeviceID
sz = objDisk.Size / (1024^3)
fr = objDisk.FreeSpace / (1024^3)
objExcel.Cells(a, 9).Value = FormatNumber(sz, 2)
objExcel.Cells(a, 10).Value = FormatNumber(fr, 2)
If fr < (sz * 0.2) Then objExcel.Cells(a, 10).Interior.ColorIndex = 3 ' Low Space Alert
a = a + 1
Next
' Network Logic
b = y
For Each objAdapter In colAdapters
objExcel.Cells(b, 11).Value = objAdapter.Description
objExcel.Cells(b, 12).Value = objAdapter.DHCPEnabled
If Not IsNull(objAdapter.IPAddress) Then objExcel.Cells(b, 13).Value = objAdapter.IPAddress(0)
If Not IsNull(objAdapter.IPSubnet) Then objExcel.Cells(b, 14).Value = objAdapter.IPSubnet(0)
If Not IsNull(objAdapter.DefaultIPGateway) Then objExcel.Cells(b, 15).Value = objAdapter.DefaultIPGateway(0)
b = b + 1
Next
' Roles & Features
x = y
Set colRoleFeatures = objWMIService.ExecQuery("Select * from Win32_ServerFeature")
If colRoleFeatures.Count > 0 Then
For Each objRole In colRoleFeatures
objExcel.Cells(x, 16).Value = objRole.Name
x = x + 1
Next
Else
objExcel.Cells(x, 16).Value = "None Found"
End If
' Software Registry Scan
s = y
Const HKLM = &H80000002
strKey = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\"
Set objReg = GetObject("winmgmts://" & computerName & "/root/default:StdRegProv")
objReg.EnumKey HKLM, strKey, arrSubkeys
For Each strSubkey In arrSubkeys
objReg.GetStringValue HKLM, strKey & strSubkey, "DisplayName", strVal1
If strVal1 <> "" Then
objExcel.Cells(s, 17).Value = strVal1
objReg.GetStringValue HKLM, strKey & strSubkey, "InstallDate", strVal2
objExcel.Cells(s, 18).Value = strVal2
objReg.GetDWORDValue HKLM, strKey & strSubkey, "VersionMajor", vMaj
objReg.GetDWORDValue HKLM, strKey & strSubkey, "VersionMinor", vMin
objExcel.Cells(s, 19).Value = vMaj & "." & vMin
s = s + 1
End If
Next
' Advance Row to next available empty spot
y = a
If b > y Then y = b
If x > y Then y = x
If s > y Then y = s
y = y + 1 ' Buffer line
Next
Next
Next
Else
objExcel.Cells(y, 1).Value = computerName
objExcel.Cells(y, 2).Value = "OFFLINE"
objExcel.Cells(y, 2).Interior.ColorIndex = 3
y = y + 1
End If
Loop
' Final Formatting
For col = 1 To 20: objExcel.Columns(col).AutoFit: Next
objExcel.ActiveWorkbook.SaveAs strFileName
Wscript.Echo "Complete! Report saved to " & strFileName

Why it’s a Game Changer

  • The “Red Flag” Feature: It automatically highlights any disk with less than 20% free space in Red. This instantly tells you which servers need urgent cleanup.
  • Software Archeology: Most scripts skip software lists because they are messy. This script pulls directly from the Uninstall registry keys, capturing even the apps that don’t show up in standard WMI queries.
  • Intelligent Row Management: Because software, roles, and disks all have different counts, the script calculates the “max row” used for each server and jumps to the next clear space for the next machine.

Stop Hunting for Web Servers: How to Auto-Discover Every IIS Instance in Your Domain | Lazy Admin Blog

Posted on Updated on

IIS Discovery

Have you ever been asked for a list of every active web server in your environment, only to realize your documentation is six months out of date? You could check your DNS records manually, or you could let PowerShell do the detective work for you.

This script scans your Active Directory for Windows Servers, checks if the World Wide Web Publishing Service (W3SVC) is actually running, and then pulls a deep-profile of the hardware, OS, and network configuration for every active hit.

The Setup

  1. Create the workspace: Create a folder at C:\Temp\ServersRunningIIS.
  2. Prepare the list: The script will automatically generate a list of all Windows Servers from AD, but ensure you have the Active Directory PowerShell module installed.
  3. Run with Privileges: Since the script uses WMI to query remote system info (RAM, OS Version, etc.), run your PowerShell ISE or Console as a Domain Admin.

The PowerShell Script

PowerShell
# Script: IIS Server Discovery & Profiler
# Location: lazyadminblog.com
# Purpose: Identify active IIS nodes and collect hardware/OS specs
Import-Module ActiveDirectory
# 1. Harvest all Windows Servers from AD
Write-Host "Gathering server list from Active Directory..." -ForegroundColor Cyan
$servers = Get-ADComputer -Filter {operatingsystem -Like "Windows server*"} | Select-Object -ExpandProperty Name
$servers | Out-File "C:\Temp\ServersRunningIIS\serverlist.txt"
# 2. Load the list for processing
$serversall = Get-Content "C:\Temp\ServersRunningIIS\serverlist.txt"
Start-Transcript -Path "C:\Temp\ServersRunningIIS\log_output.txt" -Append
foreach($vm in $serversall) {
try {
# Check if IIS Service (W3SVC) exists and is running
$iis = Get-WmiObject Win32_Service -ComputerName $vm -Filter "name='W3SVC'" -ErrorAction SilentlyContinue
if($iis.State -eq "Running") {
Write-Host "FOUND: IIS is active on $vm" -BackgroundColor DarkBlue -ForegroundColor DarkYellow
# Collect Network Info
$ipinfo = Get-WmiObject Win32_NetworkAdapterConfiguration -ComputerName $vm |
Where-Object {$_.IPEnabled -eq $true -and $_.IPAddress -like "1*"} | Select-Object -First 1
# Collect Hardware Info
$hwinfo = Get-WmiObject Win32_Computersystem -ComputerName $vm
# Collect OS Info
$osinfo = Get-WmiObject Win32_OperatingSystem -ComputerName $vm
# Flattening data for CSV-style output
$allinfo = "$($hwinfo.Name);$($hwinfo.Domain);$($ipinfo.IPAddress);$($ipinfo.IPSubnet);$($ipinfo.DefaultIPGateway);$($hwinfo.TotalPhysicalMemory);$($hwinfo.Manufacturer);$($hwinfo.Model);$($osinfo.Caption);$($osinfo.OSArchitecture);$($osinfo.ServicePackMajorVersion);$($osinfo.SystemDrive);$($osinfo.Version)"
# Save results to our 'Running' list
$allinfo | Out-File "C:\Temp\ServersRunningIIS\RunningWebServers.txt" -Append
}
}
catch {
Write-Host "Could not connect to $vm" -ForegroundColor Red
}
}
Stop-Transcript
Write-Host "Audit Complete! Check C:\Temp\ServersRunningIIS\RunningWebServers.txt" -ForegroundColor Green

What’s inside the report?

The output file (RunningWebServers.txt) uses a semicolon (;) delimiter, making it easy to import into Excel. It captures:

  • Network: IP Address, Subnet, and Gateway.
  • Hardware: Manufacturer, Model, RAM, and Domain membership.
  • Software: OS Version, Architecture (x64/x86), and System Drive.

Lazy Admin Tip

If you want to open the results immediately in Excel, just rename the output file from .txt to .csv and use the “Text to Columns” feature in Excel with the semicolon as the separator!

From Zero to Complete IP Inventory in 5 Seconds: The Multi-Host VBScript | Lazy Admin Blog

Posted on Updated on

Manually documenting IP addresses, MACs, and DNS settings is the definition of “busy work.” This VBScript automates the entire process. It reads a list of servers from a text file, queries each one via WMI, and builds a professional Excel report in real-time.

How to Use This Script

  1. Prepare the Input: Create a text file (e.g., servers.txt) and list your hostnames or IP addresses, one per line.
  2. Save the Script: Save the code below as IPAddressInventory.vbs.
  3. Run: Double-click the .vbs file. When prompted, provide the full path to your text file (e.g., C:\Scripts\servers.txt).
  4. Requirement: You must have Microsoft Excel installed on the machine where you run the script.

The VBScript Code

VBScript

' Save as IPAddressInventory.vbs
' Input: Text file with Hostnames/IPs
' Output: Excel Spreadsheet (IP_Addresses.xlsx)
On Error Resume Next
Const FOR_READING = 1
'--- File Input ---
strSrvListFile = InputBox ("Please enter the server list file path OR UNC file path" & vbCrLf & "Eg: C:\Scripts\server.txt" & vbCrLf & "Eg: \\servername\scripts\server.txt","File Input location")
Set objFSO = CreateObject ("Scripting.FileSystemObject")
Set objReadFile = objFSO.OpenTextFile (strSrvListFile, FOR_READING)
'--- File Output ---
strOutput = objfso.GetParentFolderName(strSrvListFile) &"\"
'--- Error Handling ---
If Err.Number <> 0 Then
WScript.Echo "Please Enter a Valid file Name"
Err.Clear
WScript.Quit
End If
'--- Excel Object Creation ---
Set objExcel = CreateObject ("Excel.application")
objExcel.Visible = True
Set objWorkbook = objExcel.Workbooks.Add()
Set objWorksheet = objWorkbook.Worksheets("Sheet1")
x = 1
y = 1
'--- Define Headers ---
objWorksheet.Cells (x, y).value = "S.No"
objWorksheet.Cells (x, y+1).value = "Server Name"
objWorksheet.Cells (x, y+2).value = "Description"
objWorksheet.Cells (x, y+3).value = "IP_Address"
objWorksheet.Cells (x, y+4).value = "Subnet"
objWorksheet.Cells (x, y+5).value = "MACAddress"
objWorksheet.Cells (x, y+6).value = "Gateway"
objWorksheet.Cells (x, y+7).value = "Preffered DNS"
objWorksheet.Cells (x, y+8).value = "Primary DNS"
objWorksheet.Cells (x, y+9).value = "Secondary DNS"
objWorksheet.Cells (x, y+10).value = "Additional DNS 1"
objWorksheet.Cells (x, y+11).value = "Additional DNS 2"
objWorksheet.Cells (x, y+12).value = "WINS Primary"
objWorksheet.Cells (x, y+13).value = "WINS Secondary"
objWorksheet.Cells (x, y+14).value = "DNS Suffix"
objWorksheet.Cells (x, y+15).value = "DNS Suffix Order"
objWorksheet.Cells (x, y+16).value = "Remarks"
s = 1
Do Until objReadFile.AtEndOfStream
k = 0
arrComputer = objReadFile.ReadLine
strServer = Split (arrComputer, ",")
objWorksheet.Cells (x+1, y).value = s
objWorksheet.Cells (x+1, y+1).value = strServer(k)
Set objWMIService = GetObject ("winmgmts:" & "!\\" & strServer(k) & "\root\cimv2")
'--- Query Network Information ---
If Err.Number = 0 Then
WScript.Echo strServer(k) &": Inventoring"
Set colAdapters = objWMIService.ExecQuery("Select * from Win32_NetworkAdapterConfiguration Where IPEnabled = True")
For Each objAdapter in colAdapters
objWorksheet.Cells(x+1, y+2).Value = objAdapter.Description
' IP Address Logic
If Not IsNull(objAdapter.IPAddress) Then
For i = LBound(objAdapter.IPAddress) To UBound(objAdapter.IPAddress)
If Not InStr(objAdapter.IPAddress(i),":") > "0" Then
objWorksheet.Cells(x+1, y+3).Value = objAdapter.IPAddress(i)
End If
Next
End If
' Subnet Logic
If Not IsNull(objAdapter.IPSubnet) Then
For i = LBound(objAdapter.IPSubnet) To UBound(objAdapter.IPSubnet)
If objAdapter.IPSubnet(i)<> "64" Then
objWorksheet.Cells(x+1, y+4).Value = objAdapter.IPSubnet(i)
End If
Next
End If
objWorksheet.Cells(x+1, y+5).Value = objAdapter.MACAddress
' Gateway Logic
If IsNull(objAdapter.DefaultIPGateway) Then
objWorksheet.Cells(x+1, y+6).Value = "Gateway Not Set"
Else
For i = LBound(objAdapter.DefaultIPGateway) To UBound(objAdapter.DefaultIPGateway)
objWorksheet.Cells(x+1, y+6).Value = objAdapter.DefaultIPGateway(i)
Next
End If
' DNS Logic
If IsNull(objAdapter.DNSServerSearchOrder) Then
objworksheet.Cells(x+1, y+7).Value = "DNS Not Set"
Else
For i = LBound(objAdapter.DNSServerSearchOrder) To UBound(objAdapter.DNSServerSearchOrder)
objWorksheet.Cells(x+1, y+7).Value = objAdapter.DNSServerSearchOrder(i)
y = y + 1
Next
End If
y = 1
objWorksheet.Cells(x+1, y+12).Value = objAdapter.WINSPrimaryServer
objWorksheet.Cells(x+1, y+13).Value = objAdapter.WINSSecondaryServer
objWorksheet.Cells(x+1, y+14).Value = objAdapter.DNSDomain
' Suffix Logic
If IsNull(objAdapter.DNSDomainSuffixSearchOrder) Then
objworksheet.Cells(x+1, y+14).Value = "Suffix Order NA"
Else
For i = LBound(objAdapter.DNSDomainSuffixSearchOrder) To UBound(objAdapter.DNSDomainSuffixSearchOrder)
objWorksheet.Cells(x+1, y+15).Value = objAdapter.DNSDomainSuffixSearchOrder(i)
x = x + 1
Next
x = x - 1
End If
x = x + 1
WScript.Echo strServer(k) &": Completed"
Next
Else
' Error Handling for Offline Servers
objWorksheet.Cells(x+1, y+16).Value = Err.Number & "_" & Err.Description
WScript.Echo strServer(k) &": "& Err.Description
Err.Clear
x = x + 1
End If
s = s + 1
Loop
'--- Formatting and Saving ---
Set objRange = objWorksheet.UsedRange
objRange.EntireColumn.Autofit()
objExcel.ActiveWorkbook.Saveas strOutput & "IP_Addresses.xlsx"
MsgBox "Operation Completed Successfully " ,,"IP Address"

Key Features of the Script

  • Automatic Excel Formatting: It uses UsedRange.Autofit() to ensure the data is readable as soon as the file opens.
  • WMI Integration: It queries the Win32_NetworkAdapterConfiguration class directly from the remote machine.
  • Multi-Adapter Support: If a server has multiple enabled NICs, the script loops through each to capture all configurations.
  • Remark Logging: If a machine is unreachable, the error code and description are written directly into the Excel “Remarks” column so you know which servers to check manually.

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

Posted on Updated on

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.csv in C:\Temp\VMSnapshots\.

The CSV should look like this: | Host | Location | | :— | :— | | Server01 | Datacenter1 | | Server02 | Datacenter2 |


Part 1: Creating Snapshots

  1. Open PowerShell ISE with vCenter Administrator credentials.
  2. Load the functions by running the full script (provided below).
  3. Run the following command:
PowerShell
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:

  1. Use the same snapshot_servers.csv list.
  2. Run the following command:
PowerShell
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.

PowerShell
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
}
}
}

The Architect’s Guide to Windows 12: AI, CorePC, and the Infrastructure Pivot | Lazy Admin Blog

Posted on Updated on

The era of the “monolithic OS” is officially ending. General users will enjoy the “Floating Taskbar” and AI-driven search. Infrastructure architects need to focus on two structural pillars: CorePC and NPU-driven compute.

1. The CorePC Transformation: State-Separated Architecture

For decades, Windows has been a “monolithic” block of code where system files, drivers, and user data were loosely intertwined. Windows 12 introduces CorePC, a modular architecture built on State Separation.

What is State Separation?

CorePC breaks the OS into isolated, specialized partitions. This design philosophy comes from mobile operating systems like iOS and Android. It is adapted for the complexity of the PC.

  • The System Partition: A read-only, digitally signed, and immutable image provided by Microsoft. It is isolated from everything else.
  • The Application Layer: Apps are containerized. They can interact with system files but cannot modify them, preventing “registry rot” and unauthorized system changes.
  • The User State: The only mutable partition where user profiles and local data reside.

💡 Architect’s Insight: The Death of “WinRot”

Practical Application: In a traditional enterprise, a corrupted system file often requires a full re-image. With State Separation, the OS can perform an Atomic Update. It swaps the entire read-only system partition for a fresh one in the background. For a help desk, this means “Reset this PC” takes seconds rather than hours. User data remains completely untouched. It lives on a separate logical “state.”


2. The NPU Requirement: 40+ TOPS or Bust

If your 2026 hardware budget doesn’t prioritize the NPU (Neural Processing Unit), your fleet will be obsolete on delivery.

Understanding TOPS (Trillions of Operations Per Second)

TOPS is the “horsepower” rating for an NPU. Think of it as the RPM for your AI engine. CPUs are great at logic, and GPUs excel at graphics. NPUs are specialized silicon designed to handle the trillions of matrix multiplications required by AI models. They achieve this without draining the battery.

  • The Threshold: Microsoft has set a benchmark of 40+ TOPS.
  • Why it matters: Windows 12 uses a Neural Index for Recall and Semantic Search. This allows users to find a file by describing it (e.g., “Find the blue sustainability slide from last meeting”) rather than remembering a filename.
  • The Hardware Gate: To handle this locally (for privacy and speed), dedicated silicon is required. Current leaders include the Snapdragon X Elite, Intel Core Ultra, and AMD Ryzen AI series.

💡 Architect’s Insight: VDI and the “AI Gap”

The Real-World Scenario: If you are a VDI architect, Windows 12 presents a challenge. Most hypervisors do not yet support NPU passthrough. Running Windows 12 in a VM without NPU offloading means features like Recall will either be disabled. Alternatively, they will tax the server CPUs to the point of instability. Strategy: Shift non-NPU-capable legacy endpoints to Windows 365 (Cloud PC). This offloads the AI compute to Microsoft’s Azure hardware. Older thin clients can “run” Windows 12 features they couldn’t handle locally.


3. Implementation Roadmap: 2026 Action Plan

Phase 1: The “NPU-Ready” Audit

Stop purchasing “standard” laptops. 16GB RAM is now the absolute minimum for AI-native workloads. If you use 8GB, it will lead to significant performance bottlenecks because local models will swap to disk.

Phase 2: AI Data Governance

Windows 12 will “see” and “index” local content via Smart Recall.

  • Action: You must define Intune/GPO policies to govern what is indexed. You don’t want the OS indexing sensitive PII or passwords that might appear on-screen during a session. Microsoft has built exclusion logic for credential-related content, but enterprise-grade filtering is still a requirement.

❓ Frequently Asked Questions (FAQ)

  • Will my legacy Win32 apps still work? Yes. Windows 12 uses a Win32 Container to run classic apps. However, kernel-mode drivers (like old VPN clients) may need modernization to support the new state-separated driver model.
  • Is Windows 12 mandatory? Technically, no. Windows 11 continues to receive updates. Windows 10 is reaching the end of its Extended Security Update (ESU) lifecycle. Therefore, adopting the modular architecture of Windows 12 is the only long-term path for security compliance.
  • What about privacy with “Recall”? All Recall indexing and AI processing occur on-device. No screenshots or semantic data are sent to the cloud. Access is protected by Windows Hello (biometrics).

🏁 Summary: Key Takeaways for the Busy Architect

  1. Modular OS: Windows 12 uses CorePC for faster, safer updates and near-instant recovery.
  2. Silicon-First: A 40+ TOPS NPU is mandatory for the full “AI PC” experience.
  3. VDI Pivot: Use Windows 365 to bridge the gap for legacy hardware that lacks local AI silicon.

What’s your strategy for the NPU transition? Are you leaning toward a hardware refresh or a shift to Cloud PCs?

Share your thoughts in the comments. Let us know if you want a follow-up post on Intune policies for Smart Recall governance!

Build Your Own VM Snapshot GUI with PowerShell | Lazy Admin Blog

Posted on Updated on

Build Your Own VM Snapshot GUI with PowerShell

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 &amp; 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 = "&lt;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 + "&lt;/style>"

            # SMTP info
            $Toemail=$ToBox.Text
            $strTo=$Toemail
            $strCc=$CCBox.Text
            $strSubject = "Snapshot Taken : $S_Name"
            $StrMsg="Hi All, &lt;br>Snapshot has been taken successfully for below list of servers &lt;br>&lt;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:

  1. Select your vCenter: Choose which environment the VMs live in.
  2. Snapshot Specs: Enter the SR# (Service Request) and a Name. These are mandatory to keep your environment organized.
  3. Input Servers: Paste your list of servers (one per line) into the right-hand text box.
  4. 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.
  5. 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 &amp; 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 = "&lt;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 + "&lt;/style>"

            # SMTP info
            $Toemail=$ToBox.Text
            $strTo=$Toemail
            $strCc=$CCBox.Text
            $strSubject = "Snapshot Taken : $S_Name"
            $StrMsg="Hi All, &lt;br>Snapshot has been taken successfully for below list of servers &lt;br>&lt;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:

  1. Tab Isolation: You won’t accidentally delete snapshots while trying to create them.
  2. RunAsync Switch: I added -RunAsync to the deletion. This means the GUI won’t “freeze” while vCenter is doing the heavy lifting of disk consolidation.
  3. Standardized Cleanup: By hardcoding $Days = 2, you ensure that your team follows a consistent 48-hour retention policy.