Add files via upload

This commit is contained in:
Federico Lillacci
2025-10-26 19:44:44 +01:00
committed by GitHub
parent 5e31775980
commit 97a4703253
7 changed files with 763 additions and 259 deletions

View File

@@ -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.

View File

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

View File

@@ -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"

View File

@@ -1,4 +1,4 @@
$preContent = "<h2>Hyper-V Status Report</h2>"
$preContent = "<h1>Hyper-V Status Report</h1>"
$postContent = "<p>Creation Date: $($today | Out-String) -
<a href='https://github.com/tsmagnum/Hyper-V_Report' target='_blank'>Hyper-V-Report ($scriptVersion) by F. Lillacci</a><p>"
@@ -6,10 +6,14 @@ $title = "Hyper-V Status Report"
$breakHtml = "</br>"
$titleHtmlHosts = "<h3>Hyper-V Server</h3>"
$titleHtmlcsvHealth = "<h3>CSV Health</h3>"
$titleHtmlcsvSpace = "<h3>CSV Space Utilization</h3>"
$titleHtmlVms = "<h3>Virtual Machines</h3>"
$titleHtmlSnapshots = "<h3>Snapshots</h3>"
$titleHtmlReplication = "<h3>Replication</h3>"
$titleHtmlVhdx = "<h3>VHDX Disks</h3>"
$titleHtmlVmnetAdapter = "<h3>VM Network Adatpers</h3>"
$titleHtmlOsNetAdapter = "<h3>Management OS Network Adatpers</h3>"
$titleHtmlVswitch = "<h3>Virtual Switches</h3>"
$titleHtmlVswitch = "<h3>Virtual Switches</h3>"
$titleHtmlClusterConfig = "<h3>Cluster Config</h3>"
$titleHtmlClusterNetworks = "<h3>Cluster Networks</h3>"

View File

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

View File

@@ -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.

65
Save-SafeCreds.ps1 Normal file
View File

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