From 8f6b1072a328a7e9d1e50598947bd370e64a529a Mon Sep 17 00:00:00 2001 From: Federico Lillacci <65031947+tsmagnum@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:03:35 +0200 Subject: [PATCH] Add files via upload --- Functions.ps1 | 155 ++++++++++++++++++ GlobalVariables.ps1 | 40 +++++ HtmlCode.ps1 | 15 ++ Hyper-V-Report.ps1 | 378 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 91 +++++++++++ StyleCSS.ps1 | 40 +++++ 6 files changed, 719 insertions(+) create mode 100644 Functions.ps1 create mode 100644 GlobalVariables.ps1 create mode 100644 HtmlCode.ps1 create mode 100644 Hyper-V-Report.ps1 create mode 100644 README.md create mode 100644 StyleCSS.ps1 diff --git a/Functions.ps1 b/Functions.ps1 new file mode 100644 index 0000000..ca33609 --- /dev/null +++ b/Functions.ps1 @@ -0,0 +1,155 @@ +#DO NOT MODIFY +function Get-MgmtOsNicIpAddr +{ + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] $adapterName + ) + + $ipAddr = (Get-NetIPAddress | Where-Object {$_.InterfaceAlias -like "*$($adapterName)*" -and $_.AddressFamily -eq 'IPv4'}).IPAddress + + return $ipAddr +} + +function Get-VswitchMember +{ + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] $vswitch + ) + + $targetSwitch = Get-VMSwitch -Name $vswitch + + $vswitchMembers = ($targetSwitch.NetAdapterInterfaceDescriptions) + + $physNics = @() + + foreach ($vswitchMember in $vswitchMembers) + { + $physNic = (Get-Netadapter -InterfaceDescription $vswitchMember).Name + $physNics += $physNic + } + + $vswitchPhysNics = $physNics | Out-String + + return $vswitchPhysNics + +} + +function SendEmailReport-MSGraph +{ + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] $body + ) + #Checking if required module is present; if not, install it + if (!(Get-Module -Name Microsoft.Graph -ListAvailable)) + { + Write-Host -ForegroundColor Yellow "MS Graph module missing, installing..." + Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force + Import-Module -Name Microsoft.Graph + } + + #Connect interactively to Microsoft Graph with required permissions + Connect-MgGraph -NoWelcome -Scopes 'Mail.Send', 'Mail.Send.Shared' + + if ($ccrecipient) + { + $params = @{ + Message = @{ + Subject = $subject + Body = @{ + ContentType = $type + Content = $body + } + ToRecipients = @( + @{ + EmailAddress = @{ + Address = $reportRecipient + } + } + ) + CcRecipients = @( + @{ + EmailAddress = @{ + Address = $ccrecipient + } + } + ) + } + SaveToSentItems = $save + } + } + + else + { + $params = @{ + Message = @{ + Subject = $subject + Body = @{ + ContentType = $type + Content = $body + } + ToRecipients = @( + @{ + EmailAddress = @{ + Address = $reportRecipient + } + } + ) + } + SaveToSentItems = $save + } + } + + # Send message + Send-MgUserMail -UserId $reportSender -BodyParameter $params + +} + +function SendEmailReport-Mailkit +{ + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] $body + ) + + #Checking if required module is present; if not, install it + if (!(Get-Module -ListAvailable -Name "Send-MailKitMessage")) + { + Write-Host -ForegroundColor Yellow "Send-MailKitMessage module missing, installing..." + Install-Module -Name "Send-MailKitMessage" -Scope CurrentUser -Force + Import-Module -Name "Send-MailKitMessage" + } + + $UseSecureConnectionIfAvailable = $true + + $credential = ` + [System.Management.Automation.PSCredential]::new($smtpServerUser, ` + (ConvertTo-SecureString -String $smtpServerPwd -AsPlainText -Force)) + + $from = [MimeKit.MailboxAddress]$reportSender + + $recipientList = [MimeKit.InternetAddressList]::new() + $recipientList.Add([MimeKit.InternetAddress]$reportRecipient) + + if ($ccrecipient) + { + $ccList = [MimeKit.InternetAddressList]::new(); + $ccList.Add([MimeKit.InternetAddress]$ccrecipient); + } + + $Parameters = @{ + "UseSecureConnectionIfAvailable" = $UseSecureConnectionIfAvailable + "Credential" = $credential + "SMTPServer" = $smtpServer + "Port" = $smtpServerPort + "From" = $from + "RecipientList" = $recipientList + "CCList" = $ccList + "Subject" = $subject + "HTMLBody" = $body + } + + Send-MailKitMessage @Parameters +} \ No newline at end of file diff --git a/GlobalVariables.ps1 b/GlobalVariables.ps1 new file mode 100644 index 0000000..e642737 --- /dev/null +++ b/GlobalVariables.ps1 @@ -0,0 +1,40 @@ +#Script Info - do not modify +$scriptVersion = "1.0" + +#Cluster Environment - Coming soon... +$clusterDeployment = $false +$vmHosts = "hv1", "hv2" + +#Reporting section +$reportHtmlRequired = $true #Set to $true to generate an HTML report +$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 +$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 + +#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) +$ccrecipient = $null #CC email address (use quotes); leave as $null if not used +$subject = "Hyper-V Status Report" #Email subject line + +#MS Graph Email specific configuration +$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" +$smtpServerPort = 587 +$smtpAuthRequired = $true +#If SMTP authentication is required, set a username and password below. +#DO NOT USE A SENSITIVE OR PRIVILEGED ACCOUNT HERE!!! +$smtpServerUser = "smtpserver.user" +$smtpServerPwd = "mySecretPwd" diff --git a/HtmlCode.ps1 b/HtmlCode.ps1 new file mode 100644 index 0000000..84bf574 --- /dev/null +++ b/HtmlCode.ps1 @@ -0,0 +1,15 @@ +$preContent = "

Hyper-V Status Report

" +$postContent = "

Creation Date: $($today | Out-String) - + Hyper-V-Report ($scriptVersion) by F. Lillacci

" + +$title = "Hyper-V Status Report" +$breakHtml = "
" + +$titleHtmlHosts = "

Hyper-V Server

" +$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 diff --git a/Hyper-V-Report.ps1 b/Hyper-V-Report.ps1 new file mode 100644 index 0000000..fca7c2e --- /dev/null +++ b/Hyper-V-Report.ps1 @@ -0,0 +1,378 @@ +<# + .SYNOPSIS + Generates a comprehensive HTML report of the Hyper-V environment, including host details, + virtual machines, snapshots, replication status, VHDX files, network adapters, + and virtual switches. Optionally sends the report via email. + + .DESCRIPTION + The Hyper-V-Report.ps1 script is designed to automate the collection and reporting of key metrics + and configuration details from a Hyper-V infrastructure. It supports both standalone and + (future) clustered deployments and provides detailed insights into: + + Host system resources and configuration + Virtual machine specifications and states + Snapshot inventory and age + Replication status and health + VHDX file properties and fragmentation + VM and management OS network adapter configurations + Virtual switch topology and uplinks + + The script generates an HTML report and can send it via email using either MS Graph or MailKit, + depending on the configuration. + It relies on external modular scripts (GlobalVariables.ps1, StyleCSS.ps1, HtmlCode.ps1, Functions.ps1) + for customization and formatting. + + .EXAMPLE + .\Hyper-V-Report.ps1 +#> + +#Region Credits +#Author: Federico Lillacci +#Github: https://github.com/tsmagnum +#endregion + +# Setup all paths required for script to run + +#Scripted execution +$ScriptPath = (Split-Path ((Get-Variable MyInvocation).Value).MyCommand.Path) + +#Getting date (logging,timestamps, etc.) +$today = Get-Date +$launchTime = $today.ToString('ddMMyyyy-hhmm') + +#Setting the report filename +$reportHtmlFile = $reportHtmlDir+"\$($reportHtmlName)_"+$launchTime+".html" + +#Importing required assets +. ("$($ScriptPath)\GlobalVariables.ps1") +. ("$($ScriptPath)\StyleCSS.ps1") +. ("$($ScriptPath)\HtmlCode.ps1") +. ("$($ScriptPath)\Functions.ps1") + +#Region Hosts +#Getting Host infos +if ($clusterDeployment) + { + #Doing stuff for cluster env - Coming soon... + } + +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 + } +Write-Host -ForegroundColor Cyan "###### Hyper-V Hosts infos ######" +$vmHostsList | Format-Table + +#endregion + +#Region VMs +#Getting VMs detailed infos +$vmsList =@() +$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 + } + + $vmsList += $vmInfo + } + +Write-Host -ForegroundColor Cyan "###### Virtual Machines infos ######" +$vmsList | Format-Table + +#endregion + +#Region Snapshots +#Getting Snapshots +$vmSnapshotsList = @() + +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 + } + + #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 + 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 + } + + } + Write-Host -ForegroundColor Cyan "###### VHDX infos ######" + $vhdxList | Format-Table + } +#endregion + +#Region VMNetworkAdapter +if ($vmnetInfoNeeded) + { + $vmnetAdapterList = @() + + 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 + + } +#endregion + +#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 + } + + Write-Host -ForegroundColor Cyan "###### Management OS Adapters infos ######" + $osNetAdapterList | Format-Table + + } +#endregion + +#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 + } + + Write-Host -ForegroundColor Cyan "###### Virtual Switches infos ######" + $vswitchList | Format-Table + } +#endregion + +#Creating the HTML report +if ($reportHtmlRequired) + { + $dataHTML =@() + + $vmhostsHTML = $preContent + $titleHtmlHosts + ($vmHostsList | ConvertTo-Html -Fragment) + $dataHTML += $vmhostsHTML + + $vmsHTML = $titleHtmlVms + ($vmsList | ConvertTo-Html -Fragment) + $dataHTML += $vmsHTML + + $snapshotsHTML = $titleHtmlSnapshots + ($vmSnapshotsList | ConvertTo-Html -Fragment) + $dataHTML += $snapshotsHTML + + if ($replicationInfoNeeded) + { + $replicationHTML = $titleHtmlReplication + ($replicationsList | ConvertTo-Html -Fragment) + $dataHTML += $replicationHTML + } + + if ($vhdxList) + { + $vhdxListHTML = $titleHtmlVhdx + ($vhdxList | ConvertTo-Html -Fragment) + $dataHTML += $vhdxListHTML + } + + if ($vmnetInfoNeeded) + { + $vmnetAdapterListHTML = $titleHtmlVmnetAdapter + ($vmnetAdapterList | ConvertTo-Html -Fragment) + $dataHTML += $vmnetAdapterListHTML + } + + if ($osNetInfoNeeded) + { + $osNetAdapterListHTML = $titleHtmlOsNetAdapter + ($osNetAdapterList | ConvertTo-Html -Fragment) + $dataHTML += $osNetAdapterListHTML + } + + if ($vswitchInfoNeeded) + { + $vswitchListHTML = $titleHtmlVswitch + ($vswitchList | ConvertTo-Html -Fragment) + $dataHTML += $vswitchListHTML + } + + $htmlReport = ConvertTo-Html -Head $header -Title $title -PostContent $postContent -Body $dataHTML + $htmlReport | Out-File $reportHtmlFile + + } + +#Sending the report via email +if ($emailReport -and $reportHtmlRequired) + { + switch ($emailSystem) { + msgraph + { SendEmailReport-MSGraph -body (Out-String -InputObject $htmlReport) } + + mailkit + { SendEmailReport-Mailkit -body (Out-String -InputObject $htmlReport) } + + Default {Write-Host -ForegroundColor Yellow "You must select an email system, msgraph or mailkit"} + } + + } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..addd5e7 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ + +# Hyper-V Report Script Documentation + +## 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. + +--- + +## Features + +- Collects host system information +- Enumerates all virtual machines and their configurations +- Lists VM snapshots and calculates their age +- Reports replication status for VMs +- Gathers VHDX file details +- Extracts VM and management OS network adapter data +- Lists virtual switch configurations +- Generates a comprehensive HTML report +- 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` +- External scripts in the same directory: + - `GlobalVariables.ps1` + - `StyleCSS.ps1` + - `HtmlCode.ps1` + - `Functions.ps1` + +--- + +## Script Parameters + +These are expected to be defined in `GlobalVariables.ps1`: + +- `$reportHtmlDir` – Directory to save the HTML report +- `$reportHtmlName` – Base name for the report file +- `$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 +- `$emailSystem` – Email system to use (`msgraph` or `mailkit`) + +--- + +## Output + +- **HTML Report**: Saved in `$reportHtmlDir` with timestamped filename. +- **Console Output**: Displays formatted tables for each section. +- **Email**: Sent if `$emailReport` is enabled and `$reportHtmlRequired` is true. + +--- + +## Usage + +```powershell +.\Hyper-V-Report.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 + +--- + +## Notes + +- Cluster support is marked as "Coming soon". +- Email system must be explicitly selected (`msgraph` or `mailkit`). \ No newline at end of file diff --git a/StyleCSS.ps1 b/StyleCSS.ps1 new file mode 100644 index 0000000..9a5038b --- /dev/null +++ b/StyleCSS.ps1 @@ -0,0 +1,40 @@ +$header = @" + +"@ \ No newline at end of file