diff --git a/CHANGELOG.md b/CHANGELOG.md
index f9ab256..a564319 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,22 @@ The format is based on https://keepachangelog.com/en/1.0.0/, and this project ad
Placeholder for upcoming changes.
-## [1.0.0] - 2025-10-23
+## [2.0.0] - 2025-10-27
+
+### Added
+Support for clustered environments (currently only AD clusters).
+Option to securely store SMTP credentials.
+CSS templates for styling the report.
+
+### Fixed
+N/A
+
+### Changed
+Information gathering features moved to the Functions.ps1 file.
+CSS stylesheets moved to the Style folder.
+
+
+## [1.0.0] - 2025-10-17
### Added
Initial release of the project.
diff --git a/Functions.ps1 b/Functions.ps1
index ca33609..26a5f9d 100644
--- a/Functions.ps1
+++ b/Functions.ps1
@@ -1,12 +1,357 @@
#DO NOT MODIFY
+function Get-VmHostInfo
+{
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $vmHosts
+ )
+
+ $vmHostsList = [System.Collections.ArrayList]@()
+
+ foreach ($vmHost in $vmHosts)
+ {
+
+ $hostInfos = Get-CimInstance Win32_OperatingSystem -ComputerName $vmHost.ComputerName
+ $vhdPathDrive = (Get-VMHost).VirtualHardDiskPath.Split(":")[0]
+ $hypervInfo = [PSCustomObject]@{
+ Name = $vmHost.ComputerName
+ LogicalCPU = $vmHost.LogicalProcessorCount
+ RAM_GB = [System.Math]::round((($vmHost.MemoryCapacity)/1GB),2)
+ Free_RAM_GB = [System.Math]::round((($hostInfos).FreePhysicalMemory/1MB),2)
+ VHD_Volume = $vhdPathDrive+":"
+ Free_VHD_Vol_GB = [System.Math]::round(((Get-Volume $vhdPathDrive -CimSession $vmHost.ComputerName).SizeRemaining/1GB),2)
+ LiveMig = $vmHost.VirtualMachineMigrationEnabled
+ Last_Boot = $hostInfos.LastBootUpTime.ToString('dd/MM/yy HH:mm')
+ OsBuild = Get-OsBuildLevel -vmHost $vmHost.ComputerName
+ }
+
+ [void]$vmHostsList.Add($hypervInfo)
+ }
+
+ return $vmHostsList
+}
+
+function Get-VHDXInfo {
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $vms
+ )
+
+ $vhdxList = [System.Collections.ArrayList]@()
+
+ foreach ($vm in $vms)
+ {
+ $vhdxs = Get-VHD -VMId $vm.VMId |`
+ Select-Object ComputerName,Path,VhdFormat,VhdType,FileSize,Size,FragmentationPercentage
+
+ foreach ($vhdx in $vhdxs)
+ {
+ $vhdxInfo = [PSCustomObject]@{
+ Host = $vhdx.ComputerName
+ VM = $vm.VMName
+ Path = $vhdx.Path
+ Format = $vhdx.VhdFormat
+ Type = $vhdx.VhdType
+ File_Size_GB = [System.Math]::round(($vhdx.FileSize/1GB),2)
+ Size_GB = $vhdx.Size/1GB
+ Frag_Perc = $vhdx.FragmentationPercentage
+ }
+
+ [void]$vhdxList.Add($vhdxInfo)
+ }
+
+ }
+
+ return $vhdxList
+
+}
+
+function Get-VmnetInfo {
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $vms
+ )
+
+ $vmnetAdapterList = [System.Collections.ArrayList]@()
+
+ foreach ($vm in $vms)
+ {
+ $vmnetadapts = Get-VMNetworkAdapter -vm $vm |`
+ Select-Object MacAddress,Connected,VMName,IsSynthetic,IPAddresses,SwitchName,Status,VlanSetting
+
+ foreach ($vmnetadapt in $vmnetadapts)
+ {
+ $vmnetAdaptInfo = [PSCustomObject]@{
+ VM = $vmnetadapt.VMName
+ MAC = $vmnetadapt.MacAddress
+ IP_Addr = $vmnetadapt.IPAddresses | Out-String
+ Connected = $vmnetadapt.Connected
+ vSwitch = $vmnetadapt.SwitchName
+ #Status = $vmnetadapt.Status.
+ Vlan_Mode = $vmnetadapt.VlanSetting.OperationMode
+ Vlan_Id = $vmnetadapt.VlanSetting.AccessVlanId
+
+ }
+
+ [void]$vmnetAdapterList.Add($vmnetAdaptInfo)
+ }
+
+ }
+
+ return $vmnetAdapterList
+
+}
+function Get-CsvHealth
+{
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $clusterNodes
+ )
+
+ $csvHealthList = [System.Collections.ArrayList]@()
+
+ foreach ($clusterNode in $clusterNodes)
+ {
+ $csvHealth = Get-ClusterSharedVolumeState -Node $clusterNode.NodeName |`
+ Select-Object Node,VolumeFriendlyName,Name,StateInfo
+
+ foreach ($csv in $csvHealth)
+ {
+ $csvHealthVol = [PSCustomObject]@{
+ Node = $csv.Node
+ Volume = $csv.VolumeFriendlyName
+ Disk = $csv.Name
+ State = $csv.StateInfo
+ }
+
+ [void]$csvHealthList.Add($csvHealthVol)
+
+ }
+ }
+
+ return $csvHealthList
+}
+
+function Get-CsvSpaceUtilization {
+
+ [CmdletBinding()]
+
+ $csvSpaceList = [System.Collections.ArrayList]@()
+
+ $csvVolumes = Get-Volume | Where-Object {$_.FileSystem -match "CSVFS"} |`
+ Select-Object FileSystemLabel,HealthStatus,Size,SizeRemaining
+
+ foreach ($csvVolume in $csvVolumes)
+ {
+ $csvInfo = [PSCustomObject]@{
+ Volume = $csvVolume.FileSystemLabel
+ Health = $csvVolume.HealthStatus
+ Size_GB = [System.Math]::round(($csvVolume.Size/1GB),2)
+ Free_GB = [System.Math]::round(($csvVolume.SizeRemaining/1GB),2)
+ Used_Perc = [System.Math]::round(((($csvVolume.Size - $csvVolume.SizeRemaining)/$csvVolume.Size)*100),2)
+ }
+
+ [void]$csvSpaceList.Add($csvInfo)
+ }
+
+ return $csvSpaceList
+
+}
+
+function Get-VmInfo
+{
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $vm
+ )
+
+ $vmInfo = [PSCustomObject]@{
+ Name = $vm.Name
+ Host = $vm.ComputerName
+ Gen = $vm.Generation
+ Version = $vm.Version
+ vCPU = $vm.ProcessorCount
+ RAM_Assigned = [System.Math]::round((($vm.MemoryAssigned)/1GB),2)
+ RAM_Demand = [System.Math]::round((($vm.MemoryDemand)/1GB),2)
+ Dyn_Memory = $vm.DynamicMemoryEnabled
+ IP_Addr_NIC0 = $vm.NetworkAdapters[0].IPAddresses[0]
+ Snapshots = (Get-VMSnapshot -VMName $vm.Name).Count
+ Clustered = $vm.IsClustered
+ State = $vm.State
+ Heartbeat = $vm.Heartbeat
+ Uptime = $vm.Uptime.ToString("dd\.hh\:mm")
+ Replication = $vm.ReplicationState
+ Creation_Time = $vm.CreationTime
+ }
+
+ return $vmInfo
+}
+
+function Get-SnapshotInfo {
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $vms
+ )
+
+ $vmSnapshotsList = [System.Collections.ArrayList]@()
+
+ foreach ($vm in $vms)
+ {
+ $foundSnapshots = Get-VMSnapshot -VMName $vm.name
+
+ if (($foundSnapshots).Count -gt 0)
+ {
+ foreach ($foundSnapshot in $foundSnapshots)
+ {
+ $snapInfo = [PSCustomObject]@{
+ VM = $foundSnapshot.VMName
+ Name = $foundSnapshot.Name
+ Creation_Time = $foundSnapshot.CreationTime
+ Age_Days = $today.Subtract($foundSnapshot.CreationTime).Days
+ Parent_Snap = $foundSnapshot.ParentSnapshotName
+ }
+
+ [void]$vmSnapshotsList.Add($snapInfo)
+ }
+ }
+
+ else
+ {
+ $snapInfo = [PSCustomObject]@{
+ VM = $vm.VMName
+ Name = "No snapshots found"
+ Creation_Time = "N/A"
+ Age_Days = "N/A"
+ Parent_Snap = "No snapshots found"
+ }
+
+ [void]$vmSnapshotsList.Add($snapInfo)
+
+ }
+
+
+ }
+ return $vmSnapshotsList
+}
+
+function Get-ReplicationInfo {
+
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $vms
+ )
+
+ $replicationsList = [System.Collections.ArrayList]@()
+ #Getting the Replication status
+ $replicatedVms = ($vms | Where-Object {$_.ReplicationState -ne "Disabled"} | Select-Object -Unique)
+ if (($replicatedVms).Count -gt 0)
+ {
+ foreach ($vm in $replicatedVms)
+ {
+ $replication = Get-VmReplication -Vmname $vm.VMName |`
+ Select-Object Name,State,Health,LastReplicationTime,PrimaryServer,ReplicaServer,AuthType
+ [void]$replicationsList.Add($replication)
+ }
+ }
+
+ #Creating a dummy object to correctly format the HTML report with no replications
+ else
+ {
+ $statusMsg = "No replicated VMs found!"
+ $noReplicationInfo = [PSCustomObject]@{
+ Replication_Infos = $statusMsg
+ }
+ [void]$replicationsList.Add($noReplicationInfo)
+ }
+
+ return $replicationsList
+}
+
+function Get-OsNetAdapterInfo
+{
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $vmHosts
+ )
+
+ $osNetadaptsList = @()
+
+ foreach ($vmHost in $vmHosts)
+ {
+ $osNetadaptsArray = @()
+
+ $osNetadapts = Get-VMNetworkAdapter -ManagementOS -ComputerName $vmHost.Name |`
+ Select-Object ComputerName,Name,MacAddress,IPAddresses,SwitchName,Status,VlanSetting
+
+ foreach ($osNetadapt in $osNetadapts)
+ {
+ $osNetAdaptInfo = [PSCustomObject]@{
+ Host = $osNetadapt.ComputerName
+ Name = $osNetadapt.Name
+ MAC = $osNetadapt.MacAddress
+ IP_Addr = Get-MgmtOsNicIpAddr -adapterName $osNetadapt.Name -vmHost $vmHost
+ vSwitch = $osNetadapt.SwitchName
+ Status = $osNetadapt.Status | Out-String
+ Vlan_Mode = $osNetadapt.VlanSetting.OperationMode
+ Vlan_Id = $osNetadapt.VlanSetting.AccessVlanId
+ }
+
+ $osNetadaptsArray += $osNetAdaptInfo
+ }
+
+ $osNetadaptsList += $osNetadaptsArray
+
+ }
+
+ return $osNetadaptsList
+}
+
+function Get-VswitchInfo
+{
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $vmHosts
+ )
+
+ $vswitchesList = @()
+
+ foreach ($vmHost in $vmHosts)
+ {
+ $vswitches = Get-VMSwitch -ComputerName $vmHost.Name | `
+ Select-Object ComputerName,Name,EmbeddedTeamingEnabled,SwitchType,AllowManagementOS
+
+ $vswitchesArray = @()
+
+ foreach ($vswitch in $vswitches)
+ {
+ $vswitchInfo = [PSCustomObject]@{
+ Host = $vswitch.ComputerName
+ Virtual_Switch = $vswitch.Name
+ SET = $vswitch.EmbeddedTeamingEnabled
+ Uplinks = Get-VswitchMember -vswitch $vswitch.Name
+ Type = $vswitch.SwitchType
+ Mgmt_OS_Allowed = $vswitch.AllowManagementOS
+ }
+
+ $vswitchesArray += $vswitchInfo
+ }
+
+ $vswitchesList += $vswitchesArray
+ }
+
+ return $vswitchesList
+}
+
function Get-MgmtOsNicIpAddr
{
[CmdletBinding()]
Param(
- [Parameter(Mandatory = $true)] $adapterName
+ [Parameter(Mandatory = $true)] $adapterName,
+ [Parameter(Mandatory = $true)] $vmHost
)
- $ipAddr = (Get-NetIPAddress | Where-Object {$_.InterfaceAlias -like "*$($adapterName)*" -and $_.AddressFamily -eq 'IPv4'}).IPAddress
+ $ipAddr = (Get-NetIPAddress -CimSession $vmHost.Name |`
+ Where-Object {$_.InterfaceAlias -like "*($($adapterName))" -and $_.AddressFamily -eq 'IPv4'}).IPAddress
return $ipAddr
}
@@ -22,12 +367,12 @@ function Get-VswitchMember
$vswitchMembers = ($targetSwitch.NetAdapterInterfaceDescriptions)
- $physNics = @()
+ $physNics = [System.Collections.ArrayList]@()
foreach ($vswitchMember in $vswitchMembers)
{
$physNic = (Get-Netadapter -InterfaceDescription $vswitchMember).Name
- $physNics += $physNic
+ [void]$physNics.Add($physNic)
}
$vswitchPhysNics = $physNics | Out-String
@@ -36,6 +381,101 @@ function Get-VswitchMember
}
+function Get-ClusterConfigInfo {
+
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $false)] $cluster = "."
+ )
+
+ $cluster = Get-Cluster -Name $cluster | Select-Object Name,Domain
+
+ $clusterConfigInfoList = [PSCustomObject]@{
+
+ Cluster_Name = $cluster.Name
+ Cluster_Domain = $cluster.Domain
+ }
+
+ return $clusterConfigInfoList
+
+}
+
+function Get-ClusterNetworksInfo {
+
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $false)] $cluster = "."
+ )
+
+ $clusterNets = Get-ClusterNetwork -Cluster $cluster | Select-Object Name,Role,State,Address,Autometric,Metric
+
+ $clusterNetworksList = [System.Collections.ArrayList]@()
+
+ foreach ($clusterNet in $clusterNets)
+ {
+ $clusterNetInfo = [PSCustomObject]@{
+ Name = $clusterNet.Name
+ Role = $clusterNet.Role
+ State = $clusterNet.State
+ Address = $clusterNet.Address
+ Autometric = $clusterNet.Autometric
+ Metric = $clusterNet.Metric
+ }
+
+ [void]$clusterNetworksList.Add($clusterNetInfo)
+ }
+
+ return $clusterNetworksList
+
+}
+
+function Get-OsBuildLevel
+{
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $vmHost
+ )
+
+ $code = {
+
+ Try
+ {
+ $osBuildLevel = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name LCUver -Verbose).LCUVer
+ return $osBuildLevel
+ }
+ Catch
+ {
+ $osBuildLevel = "N/A"
+ return $osBuildLevel
+ }
+ }
+
+ if ($vmHost -eq $env:COMPUTERNAME)
+ {
+ $osBuildLevel = Invoke-Command -ScriptBlock $code
+ }
+
+ else
+ {
+ $osBuildLevel = Invoke-Command -ComputerName $vmHost -ScriptBlock $code -Verbose
+ }
+
+
+
+ return $osBuildLevel
+}
+
+function Import-SafeCreds {
+ [CmdletBinding()]
+ Param(
+ [Parameter(Mandatory = $true)] $encryptedSMTPCredsFile
+ )
+
+ $credentials = Import-Clixml -Path $encryptedSMTPCredsFile
+ return $credentials
+
+}
+
function SendEmailReport-MSGraph
{
[CmdletBinding()]
@@ -124,9 +564,17 @@ function SendEmailReport-Mailkit
$UseSecureConnectionIfAvailable = $true
- $credential = `
- [System.Management.Automation.PSCredential]::new($smtpServerUser, `
- (ConvertTo-SecureString -String $smtpServerPwd -AsPlainText -Force))
+ if ($encryptedSMTPCreds)
+ {
+ $Credentials = Import-SafeCreds -encryptedSMTPCredsFile $encryptedSMTPCredsFile
+ }
+
+ else
+ {
+ $credentials = `
+ [System.Management.Automation.PSCredential]::new($smtpServerUser, `
+ (ConvertTo-SecureString -String $smtpServerPwd -AsPlainText -Force))
+ }
$from = [MimeKit.MailboxAddress]$reportSender
@@ -141,7 +589,7 @@ function SendEmailReport-Mailkit
$Parameters = @{
"UseSecureConnectionIfAvailable" = $UseSecureConnectionIfAvailable
- "Credential" = $credential
+ "Credential" = $credentials
"SMTPServer" = $smtpServer
"Port" = $smtpServerPort
"From" = $from
diff --git a/GlobalVariables.ps1 b/GlobalVariables.ps1
index e8b54aa..4078331 100644
--- a/GlobalVariables.ps1
+++ b/GlobalVariables.ps1
@@ -1,28 +1,31 @@
-#Script Info - do not modify the following line
-$scriptVersion = "1.0"
+#Script Info - please do not modify the following line
+$scriptVersion = "2.0"
-#Cluster Environment - Coming soon...
-$clusterDeployment = $false
-$vmHosts = "hv1", "hv2"
+#Cluster Environment
+$clusterDeployment = $false #Set to $true for a clustered environment
#Reporting section
$reportHtmlRequired = $true #Set to $true to generate an HTML report
+$reportStyle = "prodark" #Choose between "minimal", "pro", "prodark" or "colorful"
$reportHtmlName = "Hyper-V_Status_Report" #Report file name without extension, e.g., "MyReport"
$reportHtmlDir = "C:\Temp" #Directory to save the report (no trailing slash), e.g., "C:\Temp"
#Information to be included in the report
+$csvHealthInfoNeeded = $true #Set to true to include CSV health details
$replicationInfoNeeded = $true # Set to true to include replication details
$vhdxInfoNeeded = $true #Set to true to include detailed VHDX information
$vmnetInfoNeeded = $true #Set to true to include VM network adapter details
$osNetInfoNeeded = $true #Set to true to include Management OS network adapter details
$vswitchInfoNeeded = $true #Set to true to include virtual switch details
+$clusterConfigInfoNeeded = $true #Set to true to include cluster config details
+$clusterNetworksInfoNeeded = $true #Set to true to include cluster networks details
#Email Configuration
$emailReport = $true #Set to true to send the report via email
$emailSystem = "mailkit" #Choose the email system: "msgraph" or "mailkit"
-$reportSender = "mySender@mydomain.com" #Sender email address (use quotes)
-$reportRecipient = "myRecipient@mydomain.com" #Recipient email address (use quotes)
+$reportSender = "mySender@domain.com" #Sender email address (use quotes)
+$reportRecipient = "myrecipient@domain.com" #Recipient email address (use quotes)
$ccrecipient = $null #CC email address (use quotes); leave as $null if not used
$subject = "Hyper-V Status Report" #Email subject line
@@ -30,12 +33,22 @@ $subject = "Hyper-V Status Report" #Email subject line
$type = "HTML" #Choose between "HTML" or "TXT"
$save = $false #Set to true to save the email in the Sent Items folder
-#MailKit Email specific configuration
-$smtpServer = "mysmtpserver.domain.com"
+#MailKit-specific email configuration
+$smtpServer = "mySmtp.server.com"
$smtpServerPort = 587
$smtpAuthRequired = $true
-#If SMTP authentication is required, set a username and password below.
+#It is recommended to use an encrypted XML file for SMTP credentials.
+#Run the Save-SafeCreds.ps1 script to store your credentials in an encrypted XML file.
+#Save the encrypted XML file in the script directory.
+#Set the following variable to $true and enter the path to the XML file.
+#Please note: only the user encrypting the creds will be able to decrypt them!
+$encryptedSMTPCreds = $true #set to true to use the encrypted XML file for the creds.
+$encryptedSMTPCredsFileName = "EncryptedCreds.xml" #name of the encrypted creds file.
+#If you prefer to store the credentials in plain text, set the username and password below.
+#and set $encryptedSMTPCreds to $false
+#
#DO NOT USE A SENSITIVE OR PRIVILEGED ACCOUNT HERE!!!
+#This poses a security risk — use these credentials only for testing purposes.
$smtpServerUser = "smtpserver.user"
$smtpServerPwd = "mySecretPwd"
diff --git a/HtmlCode.ps1 b/HtmlCode.ps1
index 84bf574..4221329 100644
--- a/HtmlCode.ps1
+++ b/HtmlCode.ps1
@@ -1,4 +1,4 @@
-$preContent = "
Hyper-V Status Report
"
+$preContent = "Hyper-V Status Report
"
$postContent = "Creation Date: $($today | Out-String) -
Hyper-V-Report ($scriptVersion) by F. Lillacci
"
@@ -6,10 +6,14 @@ $title = "Hyper-V Status Report"
$breakHtml = ""
$titleHtmlHosts = "
Hyper-V Server
"
+$titleHtmlcsvHealth = "CSV Health
"
+$titleHtmlcsvSpace = "CSV Space Utilization
"
$titleHtmlVms = "Virtual Machines
"
$titleHtmlSnapshots = "Snapshots
"
$titleHtmlReplication = "Replication
"
$titleHtmlVhdx = "VHDX Disks
"
$titleHtmlVmnetAdapter = "VM Network Adatpers
"
$titleHtmlOsNetAdapter = "Management OS Network Adatpers
"
-$titleHtmlVswitch = "Virtual Switches
"
\ No newline at end of file
+$titleHtmlVswitch = "Virtual Switches
"
+$titleHtmlClusterConfig = "Cluster Config
"
+$titleHtmlClusterNetworks = "Cluster Networks
"
\ No newline at end of file
diff --git a/Hyper-V-Report.ps1 b/Hyper-V-Report.ps1
index 64167e7..453a9f4 100644
--- a/Hyper-V-Report.ps1
+++ b/Hyper-V-Report.ps1
@@ -42,67 +42,106 @@ $launchTime = $today.ToString('ddMMyyyy-hhmm')
#Importing required assets
. ("$($ScriptPath)\GlobalVariables.ps1")
-. ("$($ScriptPath)\StyleCSS.ps1")
. ("$($ScriptPath)\HtmlCode.ps1")
. ("$($ScriptPath)\Functions.ps1")
+#Setting the report style
+switch ($reportStyle) {
+ minimal
+ { $styleSheet = "StyleCSS-Minimal.ps1" }
+ pro
+ { $styleSheet = "StyleCSS-Pro.ps1" }
+ prodark
+ { $styleSheet = "StyleCSS-ProDark.ps1" }
+ colorful
+ { $styleSheet = "StyleCSS-Colorful.ps1" }
+ Default
+ { $styleSheet = "StyleCSS-Professional.ps1" }
+}
+
+. ("$($ScriptPath)\Style\$styleSheet")
+
#Setting the report filename
$reportHtmlFile = $reportHtmlDir+"\$($reportHtmlName)_"+$launchTime+".html"
+#Setting the encrypted creds file path
+if ($emailReport)
+{
+ $encryptedSMTPCredsFile = "$($ScriptPath)\$encryptedSMTPCredsFileName"
+}
+
#Region Hosts
+$vmHosts = [System.Collections.ArrayList]@()
+
#Getting Host infos
if ($clusterDeployment)
{
- #Doing stuff for cluster env - Coming soon...
+ $clusterNodes = Get-ClusterNode -Cluster .
+
+ foreach ($clusterNode in $clusterNodes)
+ {
+ $vmHost = Get-VMHost -ComputerName $clusterNode.NodeName
+
+ [void]$vmHosts.Add($vmHost)
+ }
+
+ $vmHostsList = Get-VmHostInfo -vmHosts $vmHosts
}
+#Non clustered deployments
else
- {
- $vmHostsList = @()
+ {
$vmHost = Get-VMHost
- $hostInfos = Get-CimInstance Win32_OperatingSystem
- $vhdPathDrive = (Get-VMHost).VirtualHardDiskPath.Split(":")[0]
- $hypervInfo = [PSCustomObject]@{
- Name = $vmHost.ComputerName
- LogicalCPU = $vmHost.LogicalProcessorCount
- RAM_GB = [System.Math]::round((($vmHost.MemoryCapacity)/1GB),2)
- Free_RAM_GB = [System.Math]::round((($hostInfos).FreePhysicalMemory/1MB),2)
- VHD_Volume = $vhdPathDrive+":"
- Free_VHD_Vol_GB = [System.Math]::round(((Get-Volume $vhdPathDrive).SizeRemaining/1GB),2)
- OsVersion = $hostInfos.Version
- }
-
- $vmHostsList += $hypervInfo
+ $vmHostsList = Get-VmHostInfo -vmHosts $vmHost
}
+
Write-Host -ForegroundColor Cyan "###### Hyper-V Hosts infos ######"
$vmHostsList | Format-Table
#endregion
+#Region CSV Health - Only for clustered environments
+if (($clusterDeployment) -and ($csvHealthInfoNeeded))
+ {
+ $csvHealthList = Get-CsvHealth -clusterNodes $clusterNodes
+
+ Write-Host -ForegroundColor Cyan "###### CSV Health Info ######"
+ $csvHealthList | Format-Table
+ }
+#endregion
+
+#Region CSV Space Utilization - Only for clustered environments
+if ($clusterDeployment)
+ {
+ $csvSpaceList = Get-CsvSpaceUtilization
+
+ Write-Host -ForegroundColor Cyan "###### CSV Space Utilization ######"
+ $csvSpaceList | Format-Table
+ }
+#endregion
+
#Region VMs
#Getting VMs detailed infos
$vmsList =@()
-$vms = Get-VM
+
+if ($clusterDeployment)
+ {
+ $vms = foreach ($clusterNode in $clusterNodes)
+ {
+ Get-VM -ComputerName $clusterNode.NodeName
+ }
+ }
+
+#Non clustered deployments
+else
+ {
+ $vms = Get-VM
+ }
foreach ($vm in $vms)
{
- $vmInfo = [PSCustomObject]@{
- Name = $vm.Name
- Gen = $vm.Generation
- Version = $vm.Version
- vCPU = $vm.ProcessorCount
- RAM_Assigned = [System.Math]::round((($vm.MemoryAssigned)/1GB),2)
- RAM_Demand = [System.Math]::round((($vm.MemoryDemand)/1GB),2)
- Dyn_Memory = $vm.DynamicMemoryEnabled
- IP_Addr_NIC0 = $vm.NetworkAdapters[0].IPAddresses[0]
- Snapshots = (Get-VMSnapshot -VMName $vm.Name).Count
- State = $vm.State
- Heartbeat = $vm.Heartbeat
- Uptime = $vm.Uptime.ToString("dd\.hh\:mm")
- Replication = $vm.ReplicationState
- Creation_Time = $vm.CreationTime
- }
+ $vmInfo = Get-VmInfo -vm $vm
$vmsList += $vmInfo
}
@@ -114,108 +153,28 @@ $vmsList | Format-Table
#Region Snapshots
#Getting Snapshots
-$vmSnapshotsList = @()
+$vmSnapshotsList = Get-SnapshotInfo -vms $vms
-foreach ($vm in $vms)
- {
- $foundSnapshots = Get-VMSnapshot -VMName $vm.name
-
- if (($foundSnapshots).Count -gt 0)
- {
- foreach ($foundSnapshot in $foundSnapshots)
- {
- $snapInfo = [PSCustomObject]@{
- VM = $foundSnapshot.VMName
- Name = $foundSnapshot.Name
- Creation_Time = $foundSnapshot.CreationTime
- Age_Days = $today.Subtract($foundSnapshot.CreationTime).Days
- Parent_Snap = $foundSnapshot.ParentSnapshotName
- }
-
- $vmSnapshotsList += $snapInfo
- }
- }
-
- else
- {
- $snapInfo = [PSCustomObject]@{
- VM = $vm.VMName
- Name = "No snapshots found"
- Creation_Time = "N/A"
- Age_Days = "N/A"
- Parent_Snap = "No snapshots found"
- }
-
- $vmSnapshotsList += $snapInfo
-
- }
-
- }
Write-Host -ForegroundColor Cyan "###### Snapshots infos ######"
$vmSnapshotsList | Format-Table
-
#endregion
#Region replication
if($replicationInfoNeeded)
{
- $replicationsList = @()
- #Getting the Replication status
- $replicatedVms = ($vm | Where-Object {$_.ReplicationState -ne "Disabled"})
- if (($replicatedVms).Count -gt 0)
- {
- foreach ($vm in $replicatedVms)
- {
- $replication = Get-VmReplication -Vmname $vm.VMName |`
- Select-Object Name,State,Health,LastReplicationTime,PrimaryServer,ReplicaServer,AuthType
- $replicationsList += $replication
- }
-
- Write-Host -ForegroundColor Cyan "###### Replication infos ######"
- $replicationsList | Format-Table
- }
+ $replicationsList = Get-ReplicationInfo -vms $vms
+
+ Write-Host -ForegroundColor Cyan "###### Replication infos ######"
+ $replicationsList | Format-Table
- #Creating a dummy object to correctly format the HTML report with no replications
- else
- {
- $statusMsg = "No replicated VMs found!"
- Write-Host -ForegroundColor Cyan "###### Replication infos ######"
- Write-Host -ForegroundColor Yellow $statusMsg
- $noReplicationInfo = [PSCustomObject]@{
- Replication_Infos = $statusMsg
- }
- $replicationsList += $noReplicationInfo
- }
}
#endregion
#Region VHDX
if ($vhdxInfoNeeded)
{
- $vhdxList = @()
-
- foreach ($vm in $vms)
- {
- $vhdxs = Get-VHD -VMId $vm.VMId |`
- Select-Object ComputerName,Path,VhdFormat,VhdType,FileSize,Size,FragmentationPercentage
-
- foreach ($vhdx in $vhdxs)
- {
- $vhdxInfo = [PSCustomObject]@{
- Host = $vhdx.ComputerName
- VM = $vm.VMName
- Path = $vhdx.Path
- Format = $vhdx.VhdFormat
- Type = $vhdx.VhdType
- File_Size_GB = [System.Math]::round(($vhdx.FileSize/1GB),2)
- Size_GB = $vhdx.Size/1GB
- Frag_Perc = $vhdx.FragmentationPercentage
- }
-
- $vhdxList += $vhdxInfo
- }
-
- }
+ $vhdxList = Get-VHDXInfo -vms $vms
+
Write-Host -ForegroundColor Cyan "###### VHDX infos ######"
$vhdxList | Format-Table
}
@@ -224,31 +183,8 @@ if ($vhdxInfoNeeded)
#Region VMNetworkAdapter
if ($vmnetInfoNeeded)
{
- $vmnetAdapterList = @()
+ $vmnetAdapterList = Get-VmnetInfo -vms $vms
- foreach ($vm in $vms)
- {
- $vmnetadapts = Get-VMNetworkAdapter -vm $vm |`
- Select-Object MacAddress,Connected,VMName,IsSynthetic,IPAddresses,SwitchName,Status,VlanSetting
-
- foreach ($vmnetadapt in $vmnetadapts)
- {
- $vmnetAdaptInfo = [PSCustomObject]@{
- VM = $vmnetadapt.VMName
- MAC = $vmnetadapt.MacAddress
- IP_Addr = $vmnetadapt.IPAddresses | Out-String
- Connected = $vmnetadapt.Connected
- vSwitch = $vmnetadapt.SwitchName
- #Status = $vmnetadapt.Status.
- Vlan_Mode = $vmnetadapt.VlanSetting.OperationMode
- Vlan_Id = $vmnetadapt.VlanSetting.AccessVlanId
-
- }
-
- $vmnetAdapterList += $vmnetAdaptInfo
- }
-
- }
Write-Host -ForegroundColor Cyan "###### VM Net Adapters infos ######"
$vmnetAdapterList | Format-Table
@@ -258,27 +194,15 @@ if ($vmnetInfoNeeded)
#Region Management OS NetworkAdapter
if ($osNetInfoNeeded)
{
- $osNetAdapterList = @()
-
- $osNetadapts = Get-VMNetworkAdapter -ManagementOS |`
- Select-Object Name,MacAddress,IPAddresses,SwitchName,Status,VlanSetting
-
- foreach ($osNetadapt in $osNetadapts)
- {
- $osNetAdaptInfo = [PSCustomObject]@{
- Name = $osNetadapt.Name
- MAC = $osNetadapt.MacAddress
- IP_Addr = Get-MgmtOsNicIpAddr -adapterName $osNetadapt.Name
- vSwitch = $osNetadapt.SwitchName
- Status = $osNetadapt.Status | Out-String
- Vlan_Mode = $osNetadapt.VlanSetting.OperationMode
- Vlan_Id = $osNetadapt.VlanSetting.AccessVlanId
-
-
- }
-
- $osNetAdapterList += $osNetAdaptInfo
- }
+ if ($clusterDeployment)
+ {
+ $osNetAdapterList = Get-OsNetAdapterInfo -vmHost $vmHosts
+ }
+
+ else
+ {
+ $osNetAdapterList = Get-OsNetAdapterInfo -vmHost $vmHost
+ }
Write-Host -ForegroundColor Cyan "###### Management OS Adapters infos ######"
$osNetAdapterList | Format-Table
@@ -289,73 +213,110 @@ if ($osNetInfoNeeded)
#Region VirtualSwitch
if ($vswitchInfoNeeded)
{
- $vswitchList = @()
-
- $vswitches = Get-VMSwitch | `
- Select-Object ComputerName,Name,EmbeddedTeamingEnabled,SwitchType,AllowManagementOS
-
- foreach ($vswitch in $vswitches)
- {
- $vswitchInfo = [PSCustomObject]@{
- Host = $vswitch.ComputerName
- Virtual_Switch = $vswitch.Name
- SET = $vswitch.EmbeddedTeamingEnabled
- Uplinks = Get-VswitchMember -vswitch $vswitch.Name
- Type = $vswitch.SwitchType
- Mgmt_OS_Allowed = $vswitch.AllowManagementOS
-
- }
-
- $vswitchList += $vswitchInfo
- }
+
+ if ($clusterDeployment)
+ {
+ $vswitchesList = Get-VswitchInfo -vmHost $vmhosts
+ }
+
+ else
+ {
+ $vswitchesList = Get-VswitchInfo -vmHost $vmhost
+ }
Write-Host -ForegroundColor Cyan "###### Virtual Switches infos ######"
- $vswitchList | Format-Table
+ $vswitchesList | Format-Table
}
#endregion
+#Region Cluster Configuration Info
+if ($clusterDeployment -and $clusterConfigInfoNeeded)
+{
+ $clusterConfigInfoList = Get-ClusterConfigInfo
+
+ Write-Host -ForegroundColor Cyan "###### Cluster config infos ######"
+ $clusterConfigInfoList | Format-Table
+}
+#endregion
+
+#Region Cluster Networks Info
+if ($clusterDeployment -and $clusterNetworksInfoNeeded)
+{
+ $clusterNetworksList = Get-ClusterNetworksInfo
+
+ Write-Host -ForegroundColor Cyan "###### Cluster networks infos ######"
+ $clusterNetworksList | Format-Table
+}
+#endregion
+
+############### Report and Email ###############
+
#Creating the HTML report
if ($reportHtmlRequired)
{
- $dataHTML =@()
+ $dataHTML = [System.Collections.ArrayList]@()
$vmhostsHTML = $preContent + $titleHtmlHosts + ($vmHostsList | ConvertTo-Html -Fragment)
- $dataHTML += $vmhostsHTML
+ [void]$dataHTML.Add($vmhostsHTML)
+
+ if (($clusterDeployment) -and ($csvHealthInfoNeeded))
+ {
+ $csvHealthHTML = $titleHtmlcsvHealth + ($csvHealthList | ConvertTo-Html -Fragment)
+ [void]$dataHTML.Add($csvHealthHTML)
+ }
+
+ if ($clusterDeployment)
+ {
+ $csvSpaceHTML = $titleHtmlcsvSpace + ($csvSpaceList | ConvertTo-Html -Fragment)
+ [void]$dataHTML.Add($csvSpaceHTML)
+ }
$vmsHTML = $titleHtmlVms + ($vmsList | ConvertTo-Html -Fragment)
- $dataHTML += $vmsHTML
+ [void]$dataHTML.Add($vmsHTML)
$snapshotsHTML = $titleHtmlSnapshots + ($vmSnapshotsList | ConvertTo-Html -Fragment)
- $dataHTML += $snapshotsHTML
+ [void]$dataHTML.Add($snapshotsHTML)
if ($replicationInfoNeeded)
{
$replicationHTML = $titleHtmlReplication + ($replicationsList | ConvertTo-Html -Fragment)
- $dataHTML += $replicationHTML
+ [void]$dataHTML.Add($replicationHTML)
}
if ($vhdxList)
{
$vhdxListHTML = $titleHtmlVhdx + ($vhdxList | ConvertTo-Html -Fragment)
- $dataHTML += $vhdxListHTML
+ [void]$dataHTML.Add($vhdxListHTML)
}
if ($vmnetInfoNeeded)
{
$vmnetAdapterListHTML = $titleHtmlVmnetAdapter + ($vmnetAdapterList | ConvertTo-Html -Fragment)
- $dataHTML += $vmnetAdapterListHTML
+ [void]$dataHTML.Add($vmnetAdapterListHTML)
}
if ($osNetInfoNeeded)
{
$osNetAdapterListHTML = $titleHtmlOsNetAdapter + ($osNetAdapterList | ConvertTo-Html -Fragment)
- $dataHTML += $osNetAdapterListHTML
+ [void]$dataHTML.Add($osNetAdapterListHTML)
}
if ($vswitchInfoNeeded)
{
- $vswitchListHTML = $titleHtmlVswitch + ($vswitchList | ConvertTo-Html -Fragment)
- $dataHTML += $vswitchListHTML
+ $vswitchesListHTML = $titleHtmlVswitch + ($vswitchesList | ConvertTo-Html -Fragment)
+ [void]$dataHTML.Add($vswitchesListHTML)
+ }
+
+ if ($clusterDeployment -and $clusterConfigInfoNeeded)
+ {
+ $clusterConfigInfoListHTML = $titleHtmlClusterConfig + ($clusterConfigInfoList | ConvertTo-Html -Fragment)
+ [void]$dataHTML.Add($clusterConfigInfoListHTML)
+ }
+
+ if ($clusterDeployment -and $clusterNetworksInfoNeeded)
+ {
+ $clusterNetworksListHTML = $titleHtmlClusterNetworks + ($clusterNetworksList | ConvertTo-Html -Fragment)
+ [void]$dataHTML.Add($clusterNetworksListHTML)
}
$htmlReport = ConvertTo-Html -Head $header -Title $title -PostContent $postContent -Body $dataHTML
diff --git a/README.md b/README.md
index 21718d0..957dd51 100644
--- a/README.md
+++ b/README.md
@@ -3,14 +3,11 @@
## Overview
-This PowerShell script automates the collection of detailed information about a Hyper-V environment, including hosts, virtual machines, snapshots, replication status, virtual hard disks (VHDX), network adapters, and virtual switches. It generates an HTML report and optionally sends it via email.
-Support for clustered environments will be introduced in a future release.
+This PowerShell script automates the collection of detailed information about a Hyper-V environment, including hosts, virtual machines, snapshots, replication status, VHDX files, network adapters, virtual switches, and—new in this version—CSV health and space utilization, cluster configuration, and cluster network details. It generates a customizable HTML report and can optionally send it via email using MS Graph or MailKit.
-The script can be scheduled like any other PowerShell script using the Task Scheduler in Windows.
+The script supports both standalone and clustered deployments.
-Notice: Please be aware that the script installs the MS Graph or Send-MailKitMessage PowerShell modules if they are not already present. Do not run the script if installing these modules could cause issues in your environment.
-
----
+> ⚠️ Note: The script installs required modules (MS Graph or MailKit) if not already present. Avoid running it if module installation could impact your environment.
## Features
@@ -21,52 +18,52 @@ Notice: Please be aware that the script installs the MS Graph or Send-MailKitMes
- Gathers VHDX file details
- Extracts VM and management OS network adapter data
- Lists virtual switch configurations
-- Generates a comprehensive HTML report
+- NEW: Reports CSV health and space utilization (clustered only)
+- NEW: Includes cluster configuration and network details
+- Generates a comprehensive HTML report with selectable styles
- Sends the report via email using MS Graph or MailKit
----
-
## Prerequisites
- PowerShell 5.1 or later
- Hyper-V role installed
- Required modules:
- - `Hyper-V`
- - `CimCmdlets`
+ - Hyper-V
+ - CimCmdlets
- External scripts in the same directory:
- - `GlobalVariables.ps1`
- - `StyleCSS.ps1`
- - `HtmlCode.ps1`
- - `Functions.ps1`
-
----
+ - GlobalVariables.ps1
+ - StyleCSS.ps1 (or variants like StyleCSS-Pro.ps1, StyleCSS-Colorful.ps1)
+ - HtmlCode.ps1
+ - Functions.ps1
## Script Parameters
-These are expected to be defined in `GlobalVariables.ps1`:
+Defined in `GlobalVariables.ps1`:
- `$reportHtmlDir` – Directory to save the HTML report
- `$reportHtmlName` – Base name for the report file
+- `$reportStyle` – Style of the HTML report (`minimal`, `pro`, `prodark`, `colorful`, `professional`)
- `$clusterDeployment` – Boolean flag for cluster support
-- `$replicationInfoNeeded` – Boolean flag to include replication info
-- `$vhdxInfoNeeded` – Boolean flag to include VHDX info
-- `$vmnetInfoNeeded` – Boolean flag to include VM network adapter info
-- `$osNetInfoNeeded` – Boolean flag to include management OS network adapter info
-- `$vswitchInfoNeeded` – Boolean flag to include virtual switch info
-- `$reportHtmlRequired` – Boolean flag to generate HTML report
-- `$emailReport` – Boolean flag to send report via email
+- `$csvHealthInfoNeeded` – Include CSV health info (clustered only)
+- `$csvSpaceInfoNeeded` – Include CSV space utilization (clustered only)
+- `$clusterConfigInfoNeeded` – Include cluster configuration details
+- `$clusterNetworksInfoNeeded` – Include cluster network details
+- `$replicationInfoNeeded` – Include replication info
+- `$vhdxInfoNeeded` – Include VHDX info
+- `$vmnetInfoNeeded` – Include VM network adapter info
+- `$osNetInfoNeeded` – Include management OS network adapter info
+- `$vswitchInfoNeeded` – Include virtual switch info
+- `$reportHtmlRequired` – Generate HTML report
+- `$emailReport` – Send report via email
- `$emailSystem` – Email system to use (`msgraph` or `mailkit`)
-
----
+- `$encryptedSMTPCredsFileName` – Filename for encrypted SMTP credentials
## Output
-- **HTML Report**: Saved in `$reportHtmlDir` with timestamped filename.
+- **HTML Report**: Saved in `$reportHtmlDir` with a timestamped filename.
- **Console Output**: Displays formatted tables for each section.
- **Email**: Sent if `$emailReport` is enabled and `$reportHtmlRequired` is true.
----
-
## Usage
```powershell
@@ -75,22 +72,23 @@ These are expected to be defined in `GlobalVariables.ps1`:
Ensure all required variables and modules are properly configured before execution.
----
-
## Sections in the Report
1. **Host Info** – CPU, RAM, OS version, VHD volume stats
-2. **VM Info** – Name, generation, memory, IP, state, uptime, replication
-3. **Snapshots** – Snapshot name, age, parent snapshot
-4. **Replication** – Status, health, last replication time
-5. **VHDX Info** – Format, type, size, fragmentation
-6. **VM Network Adapters** – MAC, IP, vSwitch, VLAN
-7. **Management OS Adapters** – IP, MAC, vSwitch, VLAN
-8. **Virtual Switches** – Name, type, uplinks, SET status
-
----
+2. **CSV Health Info** – CSV status and health (clustered only)
+3. **CSV Space Utilization** – CSV volume usage (clustered only)
+4. **VM Info** – Name, generation, memory, IP, state, uptime, replication
+5. **Snapshots** – Snapshot name, age, parent snapshot
+6. **Replication** – Status, health, last replication time
+7. **VHDX Info** – Format, type, size, fragmentation
+8. **VM Network Adapters** – MAC, IP, vSwitch, VLAN
+9. **Management OS Adapters** – IP, MAC, vSwitch, VLAN
+10. **Virtual Switches** – Name, type, uplinks, SET status
+11. **Cluster Configuration** – Cluster settings and roles
+12. **Cluster Networks** – Cluster network topology and status
## Notes
-- Cluster support is marked as "Coming soon".
+- Cluster support is now implemented.
- Email system must be explicitly selected (`msgraph` or `mailkit`).
+- Only the user who encrypted SMTP credentials can decrypt them.
diff --git a/Save-SafeCreds.ps1 b/Save-SafeCreds.ps1
new file mode 100644
index 0000000..861e4fe
--- /dev/null
+++ b/Save-SafeCreds.ps1
@@ -0,0 +1,65 @@
+<#
+.SYNOPSIS
+Securely prompts for SMTP credentials and stores them in an encrypted XML file for later use.
+
+.DESCRIPTION
+This PowerShell script, authored by Federico Lillacci, is designed to be run interactively to collect and securely store SMTP server credentials. It performs the following steps:
+
+Credential File Check: Verifies if an encrypted credentials file (EncryptedCreds.xml) already exists in the current working directory.
+User Confirmation: If the file exists, prompts the user to confirm whether they want to overwrite it.
+Credential Input: Prompts the user to enter their SMTP username and password using the Get-Credential cmdlet.
+Encryption and Export: Converts the secure password to an encrypted string and exports the credentials to an XML file using Export-Clixml. The credentials are encrypted using the current user's Windows Data Protection API (DPAPI), meaning only the same user on the same machine can decrypt them.
+Validation and Feedback: Confirms successful storage by checking the presence of the username in the saved file and provides appropriate feedback.
+
+This script is useful for securely storing credentials for later use in automated scripts or scheduled tasks that require SMTP authentication.
+#>
+
+#Region Credits
+#Author: Federico Lillacci
+#Github: https://github.com/tsmagnum
+#endregion
+
+#Run this script interactively to securely store SMTP server credentials
+$credsFileName = "EncryptedCreds.xml"
+$credsFile = "$(pwd)\$($credsFileName)"
+
+#Checking if a creds file already exists
+if (Test-Path -Path $credsFile)
+ {
+ Write-Host -ForegroundColor Yellow "An encrypted XML file with creds already exists at this location"
+ $userChoice = Read-Host `
+ -Prompt "Do you want to overwrite it? Press Y and ENTER to continue, any other key to abort"
+ }
+
+if ($userChoice -ne "y")
+ {
+ exit
+ }
+
+#Prompting for username and password
+Write-Host -ForegroundColor Yellow "Enter the credentials you want to save"
+$Credentials = Get-Credential
+$Credentials.Password
+$Credentials.Password | ConvertFrom-SecureString
+
+#Exporting creds
+#Please note: only the user encrypting the creds will be able to decrypt them!
+Write-Host -ForegroundColor Yellow "Saving credentials to an encrypted XML file..."
+Write-Host "Your cred will be stored in $credsFile"
+try {
+ $Credentials | Export-Clixml -Path $credsFile -Force
+ if (Get-Content $credsFile | Select-String -Pattern $($Credentials.UserName))
+ {
+ Write-Host -ForegroundColor Green "Success - Credentials securely stored in $credsFile"
+ }
+ else
+ {
+ throw "There was a problem saving your credentials"
+ }
+
+}
+
+catch
+{
+ Write-Host -ForegroundColor Red "There was a problem saving your credentials"
+}
\ No newline at end of file