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