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.

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!

Azure Alert: Default Outbound Access Ends March 31st 2026 | Lazy Admin Blog

Posted on Updated on

Is your “Internet-less” VM about to lose its connection? Here is the fix.

For years, Azure allowed Virtual Machines without an explicit outbound connection (like a Public IP or NAT Gateway) to “cheat” and access the internet using a default, hidden IP. That ends on March 31st 2026. If you haven’t transitioned your architecture, your updates will fail, your scripts will break, and your apps will go dark.

1. What exactly is changing?

Microsoft is moving toward a “Secure by Default” model. The “Default Outbound Access” (which was essentially a random IP assigned by Azure) is being retired. From now on, you must explicitly define how a VM talks to the outside world.

2. The Three “Lazy Admin” Solutions

You have three ways to fix this before the deadline. Choose the one that fits your budget and security needs:

Option A: The NAT Gateway (Recommended)

This is the most scalable way. You associate a NAT Gateway with your Subnet. All VMs in that subnet will share one (or more) static Public IPs for outbound traffic.

  • Pro: Extremely reliable and handles thousands of concurrent sessions.
  • Con: There is a small hourly cost + data processing fee.

Option B: Assign a Public IP to the VM

The simplest “Quick Fix.” Give the VM its own Standard Public IP.

  • Pro: Immediate fix for a single server.
  • Con: Itโ€™s a security risk (opens a door into the VM) and gets expensive if you have 50 VMs.

Option C: Use a Load Balancer

If you already use an Azure Load Balancer, you can configure Outbound Rules.

  • Pro: Professional, enterprise-grade setup.
  • Con: More complex to configure if you’ve never done it before.

3. How to find your “At Risk” VMs

Don’t wait for March 31st 2026 to find out what’s broken. Run this PowerShell snippet to find VMs that might be relying on default outbound access:

# Find VMs without a Public IP in a specific Resource Group
$VMs = Get-AzVM -ResourceGroupName "YourRGName"
foreach ($vm in $VMs) {
$nic = Get-AzNetworkInterface -ResourceId $vm.NetworkProfile.NetworkInterfaces[0].Id
if ($null -eq $nic.IpConfigurations.PublicIpAddress) {
Write-Host "Warning: $($vm.Name) has no Public IP and may rely on Default Outbound Access!" -ForegroundColor Yellow
}
}

๐Ÿ›ก๏ธ Lazy Admin Verdict:

If you have more than 3 VMs, deploy a NAT Gateway. Itโ€™s the “Set and Forget” solution that ensures you won’t get a 2 AM call on April 1st when your servers can’t reach Windows Update.

M365 E7: The “Super SKU” is Here (And it Costs $99) | Lazy Admin Blog

Posted on Updated on

Is the new ‘Frontier Suite’ a lazy admin’s dream or a budget nightmare?

After 11 years of E5 being the king of the mountain, Microsoft has officially announced its successor: Microsoft 365 E7. Launching May 1, 2026, this isn’t just a minor updateโ€”itโ€™s a $99/month powerhouse designed for an era where AI agents are treated like actual employees.

1. Whatโ€™s inside the E7 Box?

If youโ€™ve been “nickel and dimed” by add-on licenses for the last two years, E7 is Microsoftโ€™s way of saying “Fine, hereโ€™s everything.”

  • Microsoft 365 Copilot (Wave 3): No more $30 add-on. Itโ€™s baked in, including the new “Coworker” mode developed with Anthropic.
  • Agent 365: This is the big one. A brand-new control plane to manage, secure, and govern AI agents across your tenant.
  • Microsoft Entra Suite: You get the full identity stack, including Private Access (ZTNA) and Internet Access (SSE), which were previously separate costs.
  • Advanced Security: Enhanced features for Defender, Intune, and Purview specifically tuned for “Agentic AI” (AI that actually performs tasks, not just answers questions).

2. The $99 Math: Is it worth it?

At first glance, $99 per user per month sounds like a typo. But letโ€™s look at the “Lazy Admin” math:

ComponentStandalone Cost (Est.)
M365 E5$60 (post-July 2026 hike)
M365 Copilot$30
Agent 365$15
Entra Suite Add-on$12
Total Value$117/month

By moving to E7, youโ€™re saving about $18 per user and, more importantly, you stop managing four different license renewals. That is the definition of working smarter.

3. The “Agentic” Shift

Why do we need E7? Because in 2026, agents are becoming “Frontier Workers.” Microsoftโ€™s new stance is that AI agents need their own identities. Under E7, your automated agents get their own Entra ID, mailbox, and Teams access so they can attend meetings and file reports just like a human. E7 provides the governance layer to make sure these agents don’t go rogue and start emailing your CEO the companyโ€™s secrets.


๐Ÿ“Š Microsoft 365 License Comparison: E3 vs. E5 vs. E7

Feature CategoryM365 E3M365 E5M365 E7 (Frontier)
Monthly Cost~$36.00~$57.00$99.00
Core ProductivityFull Apps + TeamsFull Apps + TeamsFull Apps + Teams
SecurityBasic (Entra ID P1)Advanced (Entra ID P2)Autonomous (P3)
ComplianceCore eDiscoveryInner Risk + PrivaAgentic Governance
AI IntegrationAdd-on RequiredAdd-on RequiredNative Copilot Wave 3
Specialized ToolingNonePower BI ProAgent 365 (Suite)
Threat ProtectionDefender for EndpointDefender XDR FullQuantum Defender
Endpoint MgmtIntune (Basic)Intune (Plan 2)Autopilot Frontie

๐Ÿ›ก๏ธ Lazy Admin Verdict:

  • Upgrade to E7 if: You already have 50%+ Copilot adoption and youโ€™re starting to build custom AI agents in Copilot Studio.
  • Stay on E5 if: Youโ€™re still fighting with users to turn on MFA and haven’t touched AI yet.

๐Ÿ“š References & Further Reading

Guide to the Entra ID Passkey Rollout (March 2026) | Lazy Admin Blog

Posted on Updated on

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 GroupRecommended ProfileWhy?
IT AdminsDevice-Bound OnlyRequires a physical YubiKey or Windows Hello. No syncing to the cloud.
Standard UsersSynced & Device-BoundMaximum convenience. If they lose their phone, iCloud/Google restores the key. Zero helpdesk calls.
ContractorsAAGUID RestrictedOnly 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

  1. [ ] Check your Attestation: Go to Security > Authentication Methods > Passkey (FIDO2). If you want to block personal iPhones, set Enforce Attestation to Yes.
  2. [ ] 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.
  3. [ ] 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.

Setting Up Microsoft Entra Connect (Step-by-Step) | Lazy Admin Blog

Posted on Updated on

Why do manual user management when you can let a sync engine do the heavy lifting?

If youโ€™re still manually creating users in both on-premises Active Directory and the Microsoft 365 portal, stop. Youโ€™re working too hard. Microsoft Entra Connect (formerly Azure AD Connect) is the “bridge” that syncs your local identities to the cloud. Set it up once, and your users get one identity for everything.

1. The “Pre-Flight” Checklist (Don’t skip this!)

The biggest mistake admins make is running the installer before the environment is ready. To be truly “lazy,” do the prep work so the installation doesn’t fail midway.

  • Server: A domain-joined Windows Server 2016 or later (2022 is recommended).
  • Hardware: Minimum 4GB RAM and a 70GB hard drive.
  • Permissions: * Local: You need to be a Local Admin on the sync server.
    • On-Prem: An Enterprise Admin account for the initial setup.
    • Cloud: A Global Administrator or Hybrid Identity Administrator account in Entra ID.
  • Software: .NET Framework 4.7.2 or higher and TLS 1.2 enabled.

Pro Tip: Run the Microsoft IdFix tool first. It finds duplicate emails and weird characters in your AD that would otherwise break the sync.


2. Step-by-Step Installation

Download the latest version of the Entra Connect MSI here.

Step A: The Express Route

  1. Launch AzureADConnect.msi.
  2. Agree to the terms and click Use Express Settings. (Note: Use “Custom” only if you have multiple forests or need specific attribute filtering).
  3. Connect to Entra ID: Enter your Cloud Admin credentials.
  4. Connect to AD DS: Enter your Enterprise Admin credentials.
  5. Entra ID Sign-in: Ensure your UPN suffixes match. If your local domain is corp.local but your email is lazyadminblog.com, you need to add lazyadminblog.com as a UPN suffix in AD.

Step B: The “Staging Mode” Safety Net

Before you hit install, youโ€™ll see a checkbox for “Start the synchronization process when configuration completes.” If you are replacing an old server or are nervous about what will happen to your 5,000 users, check the “Enable staging mode” box. This allows the server to calculate the sync results without actually exporting anything to the cloud. You can “peek” at the results before going live.


3. Post-Setup: The “Lazy” Health Check

Once installed, the sync runs every 30 minutes by default. You don’t need to babysit it, but you should know how to check it:

  • The Desktop Tool: Open the Synchronization Service Manager to see a green “Success” status for every run.
  • The PowerShell Way: To force a sync right now (because youโ€™re too impatient for the 30-minute window), run:PowerShellStart-ADSyncSyncCycle -PolicyType Delta

4. Troubleshooting Common “Gotchas”

  • “Top-level domain not verified”: You forgot to add your domain (e.g., https://www.google.com/search?q=myblog.com) to the Entra ID portal.
  • “Object Synchronization Triggered Deletion”: By default, Entra Connect won’t delete more than 500 objects at once. This is a safety feature to stop you from accidentally wiping your cloud directory. If you intended to delete them, you’ll need to disable the export deletion threshold.

The “Lazy Admin” Sync Monitor Script

Copy and save this as Monitor-EntraSync.ps1 on your sync server.

# --- CONFIGURATION ---
$SMTPServer = "smtp.yourrelay.com"
$From = "EntraAlert@lazyadminblog.com"
$To = "you@yourcompany.com"
$Subject = "โš ๏ธ ALERT: Entra ID Sync Failure on $(hostname)"
# --- THE LOGIC ---
# Import the AdSync module (usually already loaded on the server)
Import-Module ADSync
# Get the statistics of the very last sync run
$LastRun = Get-ADSyncRunProfileResult | Sort-Object StartDateTime -Descending | Select-Object -First 1
# Check if the result was NOT 'success'
if ($LastRun.Result -ne "success") {
    $Body = @"
    The last Entra ID Sync cycle failed!
    
    Server: $(hostname)
    Run Profile: $($LastRun.RunProfileName)
    End Time: $($LastRun.EndDateTime)
    Result: $($LastRun.Result)
    
    Please log in to the Synchronization Service Manager to investigate.
"@
    # Send the alert
    Send-MailMessage -SmtpServer $SMTPServer -From $From -To $To -Subject $Subject -Body $Body -Priority High
}

๐Ÿ› ๏ธ How to set it up (The Lazy Way)

To make this fully automated, follow these steps:

  1. Create a Scheduled Task: Open Task Scheduler on your Entra Connect server.
  2. Trigger: Set it to run every hour (or every 30 minutes to match your sync cycle).
  3. Action: * Program/script:powershell.exe
    • Add arguments: -ExecutionPolicy Bypass -File "C:\Scripts\Monitor-EntraSync.ps1"
  4. Security Options: Run it as SYSTEM or a Service Account that has local admin rights so it can access the ADSync module.

Why this is better than “Default” monitoring:

  • No Noise: You only get an email if there is an actual problem.
  • Proactive: You’ll likely know the sync is broken before your users start complaining that their new passwords aren’t working.
  • Zero Cost: No need for expensive third-party monitoring tools for a single-server task.

References & Further Reading

The Robocopy Masterclass: Faster Migrations with Multi-Threading | Lazy Admin Blog

Posted on Updated on

Why use File Explorer when you can use a 128-thread engine?

If you are still using “Drag and Drop” to move terabytes of data, stop. Not only is it slow, but if the network blips for half a second, the whole process fails. Robocopy is built to survive network hiccups and, since Windows 7, it has a “Turbo” button called Multi-threading (/MT).

1. The “Standard” Lazy Migration Command

If you just want to move a folder and make sure all the permissions (ACLs) stay intact, this is your go-to string:

ROBOCOPY C:\sourcefolder C:\destinationfolder /E /COPY:DATS /LOG+:C:\temp\Robocopy_logs.txt

  • /E: Copies all subfolders, even the empty ones.
  • /COPY:DATS: This is the magic. It copies Data, Attributes, Timestamps, and Security (NTFS ACLs).
  • /LOG+: Appends the results to a text file. Always log your copies. If something is missing later, the log is your evidence.

2. Going “Turbo” with Multi-Threading (/MT)

By default, Windows copies files one by one (Serial). With the /MT switch, you can open up to 128 “lanes” of traffic.

  • Default: /MT:8 (8 files at once)
  • Aggressive: /MT:32 (Sweet spot for most servers)
  • Extreme: /MT:128 (Use this for thousands of tiny files)

Pro Tip: /MT is not compatible with /IPG (which slows down copies to save bandwidth) or /EFSRAW. If you want speed, stick to /MT.


3. Real-World Examples: Migrating Mapped Drives

When migrating large volumes (like a mapped Y: or Z: drive), you want to use the /MIR (Mirror) switch. This makes the destination an exact clone of the source.

Example: Migrating a Production Volume

ROBOCOPY Y:\lfvolumes\DEFAULT E:\MIGRATE_PRD\E_Drive /MIR /MT:24 /LOG+:D:\Logs\Edrive.txt

Example: Copying a Single Giant SQL Backup (.BAK)

ROBOCOPY T:\ E:\Migrations\ db_backup.BAK /ZB /J /LOG+:D:\Logs\backup.txt

  • /ZB: Restartable mode (if the network drops, it picks up where it left off).
  • /J: Unbuffered I/O (Recommended for huge files like database backups).

4. The “Cheat Sheet” of Switches

SwitchWhat it does
/MIRMirroring. Deletes files in destination that no longer exist in source.
/ZRestartable Mode. Survives network glitches.
/R:nNumber of retries (Default is 1 millionโ€”set this to 3 or 5 instead!).
/W:nWait time between retries (Default is 30 seconds).
/FFTUse this if copying to a Linux NAS to avoid timestamp issues.

๐Ÿ›ก๏ธ Lazy Admin Warning:

Be careful with /MIR. If you accidentally point it at an empty source folder, it will “Mirror” that emptiness by deleting everything in your destination. Always test with the /L (List Only) switch first to see what would happen without actually doing it.

Monitoring Disk Command Aborts on ESXi: Identifying Storage Overload | Lazy Admin Blog

Posted on Updated on

When your storage subsystem is severely overloaded, it cannot process commands within the acceptable timeframe defined by the Guest Operating System. The result? Disk Command Aborts. For Windows VMs, this usually triggers after 60 seconds of silence from the storage array.

Aborted commands are a critical red flag indicating that your storage hardware is overwhelmed and unable to meet the host’s performance expectations. Monitoring this parameter is essential for proactive datacenter management.

Here is how you can track these aborts using two primary methods: the vSphere Client and esxtop.


๐Ÿ’ป Method 1: vSphere Client (Graphical Interface)

This method provides a visual, historical look at command aborts across your infrastructure.

  1. Navigate to Hosts and Clusters.
  2. Select the object you want to monitor (Host or Cluster).
  3. Click on the Monitor tab, then Performance, and select Advanced.
  4. Click Chart Options.
  5. Switch the metric grouping to Disk.
  6. Select Commands aborted from the list of measurements.
  7. Click OK.

๐Ÿ› ๏ธ Method 2: esxtop (Command Line Interface)

For real-time, granular troubleshooting, esxtop is the definitive tool. It monitors the ABRTS/s (Aborts per Second) field, specifically tracking SCSI aborts.

Steps to Configure esxtop for Aborts:

  1. Open Putty and log in to your ESXi host via SSH.
  2. Type esxtop and press Enter.
  3. Type u to switch to the Disk Device view.
  4. Type f to change the field settings.
  5. Type L to select Error stats.
  6. Press Enter, then press W to save these settings for future sessions.

You will now see the ABRTS/s column. This number represents the SCSI commands aborted by the guest VM during the 1-second collection interval.


๐Ÿ“ˆ Thresholds and Interpretation

If you are deploying a monitoring tool, the critical threshold for ABRTS/s is 1. A value of 1 or higher means SCSI commands are actively being aborted by the guest OS because the storage is not responding.

What is Ideal?

In an ideal scenario, ABRTS/s should always be 0.

What is Real-World?

In a busy production environment, you may see this value fluctuate between 0 and 0.xx. This occurs during “peak hours”โ€”for instance, when multiple servers on the host are running disk-intensive backup operations simultaneously, leading to temporary storage saturation. However, any consistent spike above 1 requires immediate investigation into path failures, array congestion, or complete storage unresponsiveness.

How to install and configure Multipathing I/O on a computer running Windows Server 2008

Posted on Updated on

To install Multipath I/O

  1. Open Server Manager.To open Server Manager, click Start, point to Administrative Tools, and then click Server Manager.
  2. In the Features area, click Add Features.
  3. On the Select Features page of the Add Features Wizard, select Multipath I/O, and then click Next.
  4. On the Confirm Installation Selections page, click Install.
  5. When installation has completed, click Close.

To install Multipath I/O on a computer by using the Server Manager command line, complete the following steps.

To install Multipath I/O by using a command line

  1. Open a Command Prompt window with elevated privileges.Right-click the Command Prompt object on the Start menu, and then click Run as administrator.
  2. Type the following, and press ENTER.ย ServerManagerCmd.exe -install Multipath-IO
  3. When installation has completed, you can verify that Multipath I/O has installed by entering the following command and reviewing the query results in the command window. Multipath I/O should show in the list of installed packages.ย ServerManagerCmd.exe -query

IC347745[1]

Removing Multipath I/O

To remove Multipath I/O, complete the following steps.

To remove Multipath I/O

  1. Open Server Manager.To open Server Manager, click Start, point to Administrative Tools, and then click Server Manager.
  2. In the Features area, click Remove Features.
  3. On the Select Features page of the Add Features Wizard, select Multipath I/O, and then click Next.
  4. On the Confirm Installation Selections page, click Install.
  5. When installation has completed, click Close.

To remove Multipath I/O by using the Server Manager command line, complete the following steps.

To remove Multipath I/O by using a command line

  1. Open a Command Prompt window with elevated privileges.Right-click the Command Prompt object on the Start menu, and then click Run as administrator.
  2. Type the following, and press ENTER.ย ServerManagerCmd.exe -remove Multipath-IO
  3. When removal has completed, you can verify that Multipath I/O was removed by entering the following command and reviewing the query results in the command window. Multipath I/O should not be in the list of installed packages.ย ServerManagerCmd.exe -query
To claim an iSCSI-attached device for use with MPIO

Open the MPIO control panel, and then click the Discover Multi-Paths tab.
  1. Select the Add support for iSCSI devices check box, and then click Add. When prompted to restart the computer, click Yes.
  2. When the computer restarts, the MPIO Devices tab lists the additional hardware ID โ€œMSFT2005iSCSIBusType_0x9.โ€ When this hardware ID is listed, all iSCSI bus attached devices will be claimed by the Microsoft DSM.

Help and support service not running in Windows 2003

Posted on Updated on

To fix this:

  • Open a Command Prompt
  • Run the following commands
  • %SystemDrive%
  • CD %windir%\PCHealth\HelpCtr\Binaries
  • start /w helpsvc /svchost netsvcs /regserver /install

The service should install and start automatically.

How to Clean up the WinSxS Directory on Windows Server 2008 R2

Posted on Updated on

Prior to this we need to install Disk Cleanup on Windows 2008. Disk Cleanup is not installed by default on Windows Server 2008 R2. It is instead a component installed with the Desktop Experience feature.

To install Disk Cleanup without reboot the server – How to install Disk Cleanup without reboot the server in Window 2008

Now download the appropriate package and install the on the system.

Operating system Update
All supported x86-based versions of Windows 7
All supported x64-based versions of Windows 7
ย ย DownloadDownload
All supported x64-based versions of Windows Server 2008 R2
ย ย DownloadDownload

Looking at my Windows 2008 R2 Server with SP1 installed, according to Windows Explorer, the size of my Windows/WinSxS directory is as follows:

pic1[1]

The size of the WinSxS directory will vary by server. Some of you will have smaller WinSxS directories, some larger.

Installing the update is just like installing any other update. Just download and double-click on the .msu file:

pic2[1]

 

Now we need to run the disk cleanup wizard. Disk Cleanup option can be found under Startย –> All Programsย –> Accessoriesย –> System Tools: or go to run and type ‘cleanmgr’ to launch it.

pic8[1]

On launch, Disk Cleanup prompts for the drive you want to clean up, default drive will be C: drive

pic9[1]

After clicking Ok, a scan is performed:

pic10[1]

Several options are provided for cleanup, including a new option for Windows Update Cleanup:

pic11[1]

If you didnโ€™t launch Disk Cleanup as Administrator, at this point, youโ€™ll need to take a couple extra steps. Youโ€™ll need to click on the Clean up system files button.

3348.diskcleanup3[1]

The actual cleanup occurs during the next reboot. After the reboot, taking a look at the WinSxS directory, it has shrunk to the following:

pic12[1]

 

How to change default snapshot location in VMware ESXi 5

Posted on Updated on

Defaulty the snapshots which are taken for any virtual machine are stored with their parent in the same directory or storage. Sometimes you may run out of space and you might not be able to take anymore snapshots so in that case you can always use some other location for the storage of snapshots.

snapshot
These are the required steps to be taken toย change the default locations of all the snapshots .

NOTE: Please ensure that the vm you are working on is powered OFF.

Right Click the vm and selectย Edit Settings
Click onย Optionsย from the top TAB, selectย Generalย and open theย Configuration parameters

Add a new row with the following details

snapshot.redoNotWithParent

Save this parameter with a value “true” as shown below

Now open the CLI of the host where the vm is located

Go to the vm’s parent directory where all the vm files are stored and open the main .vmx file

As in my case

# cd /vmfs/volumes/53652b45-90f342h4-v3r3-s5dw676h5674/Windows2003
# vi Windows2003.vmx

Now add this line anywhere in the .vmx file with the path location where you want your snapshots to be stored

workingDir = “/vmfs/volumes/54332bf4-gd3bf353-g45b-g2ft353b5545/snapshots”

Save the file and exit

Now you need to reload this vm to make the changes take affect.
# vim-cmd vmsvc/getallvms | grepย Windows2003
13ย Windows2003 [iSCSI-Datastore15] Windows2003/Windows2003 win2003ย vmx-07
Here 13ย is the vm id which you can find out using the above command
# vim-cmd vmsvc/reloadย 13
Now when you take snapshots the snapshot files and vm swap files will be created in a different location.

How to redirect vm’s swap file

In case you do not want vm swap file to be redirected to another location and you want it to the same parent directory.
Add an extra parameter in the Configuration Parameter option shown above
sched.swap.dir=”<path_to_vm_directory>”
For example
/vmfs/volumes/54332bf4-gd3bf353-g45b-g2ft353b5545/vmswap

Save the settings and exit. Now each time you take snapshot the snapshot files and vm swap files will be saved at specified different location.

 

Installing Disk Cleanup In Windows 2008 Without Rebooting The Server

Posted on Updated on

The Disk Cleanup executable file cleanmgr.exe and the associated Disk Cleanup button are not present in Windows Serverยฎ 2008 or in Windows Serverยฎ 2008 R2 by default.ย This is by design, as the Disk Cleanup button is part of the Desktop Experience feature. In order to have Disk Cleanup button appear on a diskโ€™s Properties dialog, you will need to install the Desktop Experience feature.

So in order to use cleanmgr.exe youโ€™ll need to copy two files that are already present on the server, cleanmgr.exe and cleanmgr.exe.mui. Use the following table to locate the files for your operating system.

Windows Server 2008 R2 64 bit

C:\Windows\winsxs\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.1.7600.16385_none_c9392808773cd7da\cleanmgr.exe

Windows Server 2008 R2ย 64-bit

C:\Windows\winsxs\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.1.7600.16385_en-us_b9cb6194b257cc63\cleanmgr.exe.mui

Windows Server 2008 64-bit

C:\Windows\winsxs\amd64_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.0.6001.18000_en-us_b9f50b71510436f2\cleanmgr.exe.mui

Windows Server 2008 64-bit

C:\Windows\winsxs\amd64_microsoft-windows-cleanmgr_31bf3856ad364e35_6.0.6001.18000_none_c962d1e515e94269\cleanmgr.exe.mui

Windows Server 2008 32-bit

C:\Windows\winsxs\x86_microsoft-windows-cleanmgr.resources_31bf3856ad364e35_6.0.6001.18000_en-us_5dd66fed98a6c5bc\cleanmgr.exe.mui

Windows Server 2008 32-bit

C:\Windows\winsxs\x86_microsoft-windows-cleanmgr_31bf3856ad364e35_6.0.6001.18000_none_6d4436615d8bd133\cleanmgr.exe
Once youโ€™ve located the files move them to the following locations:

1. Cleanmgr.exe should go in %systemroot%\System32

2. Cleanmgr.exe.mui should go in %systemroot%\System32\en-US

You can now launch the Disk cleanup tool by running Cleanmgr.exe from the command promptย or by clicking Start and typing Cleanmgr into the Search bar.

 

Restart the domain controller in Directory Services Restore Mode locally

Posted on Updated on

If you have physical access to a domain controller, you can restart the domain controller in Directory Services Restore Mode locally. Restarting in Directory Services Restore Mode takes the domain controller offline. In this mode, the server is not functioning as a domain controller.

When you start Windowsย Serverย 2003 in Directory Services Restore Mode, the local Administrator account is authenticated by the local Security Accounts Manager (SAM) database. Therefore, logging on requires that you use the local administrator password, not an Activeย Directory domain password. This password is set during Activeย Directory installation when you provide the password for Directory Services Restore Mode.

Administrative credentials

To perform this procedure, you must provide the Administrator password for Directory Services Restore Mode.

To restart the domain controller in Directory Services Restore Mode locally

  1. Restart the domain controller.
  2. When the screen for selecting an operating system appears, press F8.
  3. On the Windows Advanced Options menu, select Directory Services Restore Mode.
  4. When you are prompted, log on as the local administrator.

Change the static IP address of a domain controller

Posted on Updated on

Administrative Credentials

To perform this procedure, you must be a member of the Domain Admins group in the domain of the domain controller whose IP address you are changing.

To change the static IP address of a domain controller

  1. Log on locally (also known as interactively) to the system console of the domain controller whose IP address you want to change. If you are not able to log on to the domain controller by using the domain, you may have to start the domain controller in Directory Services Restore Mode (DSRM). For more information, see Restart the domain controller in Directory Services Restore Mode locally (https://lazyadminblog.com/2015/04/11/restart-the-domain-controller-in-directory-services-restore-mode-locally/).

On the desktop, right-clickย My Network Places, and then clickย Properties.

  1. In theNetwork Connectionsย dialog box, right-clickย Local Area Connection, and then clickย Properties.
  2. In theLocal Area Connection Propertiesย dialog box, double-clickย Internet Protocol (TCP/IP).
  3. In theInternet Protocol (TCP/IP) Propertiesย dialog box, in theย IP addressย box, type the new address.
  4. In theSubnet maskย box, type the subnet mask.
  5. In theDefault gatewayย box, type the default gateway.
  6. In thePreferred DNS serverย box, type the address of the DNS server that this computer contacts.
  7. In theAlternate DNS serverย box, type the address of the DNS server that this computer contacts if the preferred server is unavailable.
  8. If this domain controller uses WINS servers, clickAdvancedย and then, in theย Advanced TCP/IP Settingsย dialog box, click theย WINS
  9. If an address in the list is no longer appropriate, click the address, and then clickEdit.
  10. In theTCP/IP WINS Serverย dialog box, type the new address, and then clickย OK.
  11. Repeat steps 11 and 12 for all addresses that need to be changed, and then clickOKย twice to close theย TCP/IP WINS Serverย dialog box and theย Advanced TCP/IP Settingsย dialog box.
  12. ClickOKย to close theย Internet Protocol (TCP/IP) Propertiesย dialog box.

After you change the IP address of a domain controller, you should run theย ipconfig /registerdnsย command to register the host record andย dcdiag /fixย command to ensure that service records are appropriately registered with DNS. For more information, see Dcdiag Overview and subordinate topics for additional information about the Dcdiag tool (https://lazyadminblog.com/2015/04/11/dcdiag-overview/).

Changing the IP settings of a server does not affect the share resources or shared permissions on that server, if the name resolution structure DNS and WINS settings are correctly configured. However, if network drives or passive connections (connections that are made manually from a command prompt or run line) are mapped using the IP address, an update is required. For example, if a client computer has G: drive mapped using the following commandย net use g: \\192.168.0.199\dataย and the IP address of the server that hosts the Data shared folder is changed from 192.168.0.199 to 192.168.1.200, the new G: drive mapping command should be changed toย net use g: \\192.168.1.200\data. A better solution would be to ensure that DNS name resolution is working properly and to use the server name, as opposed to the IP address, in the command. For example, if the server name is DC1, the command to map a G: drive to the Data share on the server isย net use g: \\dc1\data. It changes only if the server name changes; it is not affected if the IP address of the server changes.

Using esxtop to identify storage performance issues for ESX / ESXi (multiple versions) (1008205)

Posted on Updated on

The interactive esxtop utility can be used to provide I/O metrics over various devices attached to a VMware ESX host.

Configuring monitoring using esxtop

ย To monitor storage performance per HBA:

  1. Start esxtop by typing esxtop at the command line.
  2. Press d to switch to disk view (HBA mode).
  3. To view the entire Device name, press SHIFT + L and enter 36 in Change the name field size.
  4. Press f to modify the fields that are displayed.
  5. Press b, c, d, e, h, and j to toggle the fields and press Enter.
  6. Pressย s and thenย 2ย to alter the update time to every 2 seconds and press Enter.
  7. See Analyzing esxtop columns for a description of relevant columns. For more information, see Interpreting esxtop Statistics.

Note: These options are available only in VMware ESX 3.5 and later.

To monitor storage performance on a per-LUN basis:

  1. Start esxtop by typing esxtop from the command line.
  2. Press u to switch to disk view (LUN mode).
  3. Press f to modify the fields that are displayed.
  4. Pressย b, c, f, and h to toggle the fields and press Enter.
  5. Press s and then 2 to alter the update time to every 2 seconds and press Enter.
  6. See Analyzing esxtop columns for a description of relevant columns. For more information, see Interpreting esxtop Statistics.

To increase the width of the device field in esxtop to show the complete naa id:

  1. Start esxtop by typing esxtop at the command line.
  2. Press u to switch to the disk device display.
  3. Press Lย to change the name field size.Note: Ensure to use uppercase L.
  4. Enter the value 36 to display the complete naa identifier.

To monitor storage performance on a per-virtual machine basis:

  1. Start esxtop by typing esxtop at the command line.
  2. Type v to switch to disk view (virtual machine mode).
  3. Press f to modify the fields that are displayed.
  4. Pressย b, d, e, h, and j to toggle the fields and press Enter.
  5. Press s and then 2 to alter the update time to every 2 seconds and press Enter.
  6. See Analyzing esxtop columns for a description of relevant columns. For more information, see Interpreting esxtop Statistics.

 

Analyzing esxtop columns

Refer to this table for relevant columns and descriptions of these values:

Column ย Description
CMDS/s This is the total amount of commands per second and includes IOPS (Input/Output Operations Per Second) and other SCSI commands such as SCSI reservations, locks, vendor string requests, unit attention commands etc.ย being sent to or coming from the device or virtual machine being monitored.In most cases, CMDS/s = IOPS unless there are a lot of metadata operations (such as SCSI reservations)
DAVG/cmd This is the average response time in milliseconds per command being sent to the device.
KAVG/cmd This is the amount of time the command spends in the VMkernel.
GAVG/cmd This is the response time as it is perceived by the guest operating system. This number is calculated with the formula: DAVG + KAVG = GAVG

These columns are for both reads and writes, whereas xAVG/rd is for reads and xAVG/wr is for writes. The combined value of these columns is the best way to monitor performance, but high read or write response time it may indicate that the read or write cache is disabled on the array. All arrays perform differently, however,ย DAVG/cmd, KAVG/cmd, and GAVG/cmd should not exceed more than 10 milliseconds (ms) for sustained periods of time.

Note: VMware ESX 3.0.x does not include direct functionality to monitor individual LUNs or virtual machines using esxtop. Inactive LUNs lower the average for DAVG/cmd, KAVG/cmd, and GAVG/cmd. These values are also visible from the vCenter Server performance charts. For more information, see theย Performance Charts section in the Basic System Administration Guide.

If you experience high latency times, investigate current performance metrics and running configuration for the switches and the SAN targets. Check for errors or logging that may suggest a delay in operations being sent to, received, and acknowledged. This includes the array’s ability to process I/O from a spindle count aspect, or the array’s ability to handle the load presented to it.

If the response time increases to over 5000 ms (or 5 seconds), VMware ESX will time out the command and abort the operation. These events are logged; abort messages and other SCSI errors can be reviewed in these logs:

  • ESX 3.5 and 4.x โ€“ /var/log/vmkernel
  • ESXi 3.5 and 4.x โ€“ /var/log/messagesย 
  • ESXi 5.x and later – /var/log/vmkernel.log

The type of storage logging you may see in these files depends on the configuration of the server. You can find the value of these optionsย by navigating toย Host > Configuration > Advanced Settings > SCSI > SCSI.Log* or SCSI.Print*.

Connecting to a virtual machine console fails with the error: The VMRC Console has Disconnected. Trying to reconnect (2050470)

Posted on Updated on

Error: The VMRC Console has Disconnected.. Trying to reconnect

If this happens, then the VM will not be reachble on the network and you cannot see black screen on the VM console.

To fix this, killย the vmware-vmrc.exe*32 service from Windows Task Manager and thenย open the console again.

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

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.

Exporting PSTs and Setting Up External Forwarding | Lazy Admin Blog

Posted on Updated on

As a “Lazy Admin,” I like to keep my cheat sheets handy so I donโ€™t have to look up the same syntax every time a request comes in. Todayโ€™s snippets cover exporting mailboxes to PST and the “correct” way to forward mail to an external address.

1. Exporting Mailboxes to PST

Whether you are decommissioning a user or just taking a backup, New-MailboxExportRequest is your best friend.

Note: Ensure your Exchange Trusted Subsystem has read/write permissions on the network share before running these.

Export a Standard Mailbox:

New-MailboxExportRequest -Mailbox "Mailbox Name" -FilePath \\Sharelocation\Export.pst -DomainController DC_FQDN

Export a Mailbox Archive:

If the user has an In-Place Archive enabled, use the -IsArchive switch:

New-MailboxExportRequest -Mailbox "Mailbox Name" -IsArchive -FilePath \\Sharelocation\Archive.pst -DomainController DC_FQDN

2. Reconnecting a Disconnected Mailbox

If you have a disconnected mailbox that needs to be linked to a user account:

Connect-Mailbox -Identity "Mailbox Name" -Database DBName -User "UserName"

3. Forwarding Mail Externally (The Right Way)

To forward mail to an external address (like a Gmail or Outlook account), you shouldn’t just type an address into a transport rule. The cleanest way is to create a Mail Contact.

Step A: Create the Contact

  1. Login to Exchange Admin Center > Recipients > Contacts.
  2. Click Add > Mail Contact.
  3. Give it a sensible name.

Pro Tip: If you don’t want this contact cluttering up your Global Address List (GAL), hide it with PowerShell:

Set-MailContact "ALIAS-NAME" -HiddenFromAddressListsEnabled $true

Step B: Enable Forwarding via GUI

  1. Go to Recipients > Mailboxes.
  2. Locate the user you want to forward from and click Edit.
  3. Go to Mailbox Features > Scroll down to Mail Flow > View Details.
  4. Check Enable Forwarding.
  5. Browse and select the Contact you created in Step A.
  6. Optional: Check “Deliver message to both forwarding address and mailbox” if you want a copy kept in the original inbox.

Step C: Enable Forwarding via PowerShell (The Lazy Way)

If you don’t want to click through the GUI, just run this:

Set-Mailbox -Identity "Internal User" -ForwardingAddress "ExternalContactAlias" -DeliverToMailboxAndForward $true