Modules/Outputs/Reports/40-PowerPointOutput.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS v2.5.0 (#80): PowerPoint .pptx deck generator — executive environment overview. .DESCRIPTION Builds a minimal, standards-compliant OOXML .pptx directly via System.IO.Packaging so the module has zero extra dependencies (no Office, no PowerShell PowerPoint modules). Produces a 7-slide deck suitable for handoff, design review, or customer presentation. #> function New-RangerPptxDeck { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Manifest, [Parameter(Mandatory = $true)] [string]$OutputPath ) Add-Type -AssemblyName System.IO.Packaging -ErrorAction SilentlyContinue Add-Type -AssemblyName WindowsBase -ErrorAction SilentlyContinue $cluster = [string]($Manifest.topology.clusterName ?? $Manifest.domains.clusterNode.cluster.name ?? 'Unknown') $runDate = [string]($Manifest.run.endTimeUtc ?? (Get-Date -Format 'yyyy-MM-dd')) $nodes = @($Manifest.domains.clusterNode.nodes) $vms = @($Manifest.domains.virtualMachines.inventory) $wafSummary = $Manifest.domains.wafAssessment.summary $capSummary = $Manifest.domains.capacityAnalysis.summary $vmUtilSummary = $Manifest.domains.vmUtilization.summary $storageSummary = $Manifest.domains.storage.summary $slides = @( @{ title = "Azure Local — $cluster" lines = @( "Environment overview", "Run date: $runDate", "Generated by AzureLocalRanger" ) }, @{ title = 'Executive Summary' lines = @( "Nodes: $($nodes.Count)" "VMs: $($vms.Count)" ("WAF Score: {0}% ({1})" -f ([int]($wafSummary.overallScore ?? 0)), ([string]($wafSummary.status ?? 'Unknown'))) ("Failing rules: {0}" -f ([int]($wafSummary.failingRules ?? 0))) ) }, @{ title = 'Cluster Overview' lines = @( "Cluster name: $cluster" ("Deployment type: {0}" -f ([string]($Manifest.topology.deploymentType ?? 'unknown'))) ("Identity mode: {0}" -f ([string]($Manifest.topology.identityMode ?? 'unknown'))) ("Storage architecture: {0}" -f ([string]($Manifest.topology.storageArchitecture ?? 'unknown'))) ("Control plane: {0}" -f ([string]($Manifest.topology.controlPlaneMode ?? 'unknown'))) ) }, @{ title = 'Capacity Headroom' lines = if ($capSummary) { @( ("vCPU allocation: {0}% ({1})" -f ([double]$capSummary.vcpuUtilizationPct), ([string]$capSummary.vcpuStatus)) ("Memory allocation: {0}% ({1})" -f ([double]$capSummary.memoryUtilizationPct), ([string]$capSummary.memoryStatus)) ("Storage used: {0}% ({1})" -f ([double]$capSummary.storageUtilizationPct), ([string]$capSummary.storageStatus)) ("Pool allocated: {0}% ({1})" -f ([double]$capSummary.poolUtilizationPct), ([string]$capSummary.poolStatus)) ) } else { @('No capacity analysis data available.') } }, @{ title = 'Workload Efficiency' lines = if ($vmUtilSummary) { @( ("Total VMs: {0}" -f ([int]$vmUtilSummary.vmCount)) ("Idle VMs: {0}" -f ([int]$vmUtilSummary.idleCount)) ("Underutilized VMs: {0}" -f ([int]$vmUtilSummary.underutilizedCount)) ("Potential vCPU freed: {0}" -f ([int]$vmUtilSummary.potentialVcpuFreed)) ("Potential memory freed: {0} GiB" -f ([double]$vmUtilSummary.potentialMemoryFreedGiB)) ) } else { @('No VM utilization data collected.') } }, @{ title = 'Storage Summary' lines = if ($storageSummary) { @( ("Storage pools: {0}" -f ([int]$storageSummary.poolCount)) ("Physical disks: {0}" -f ([int]$storageSummary.physicalDiskCount)) ("Virtual disks: {0}" -f ([int]$storageSummary.virtualDiskCount)) ("Volumes: {0}" -f ([int]$storageSummary.volumeCount)) ("Unhealthy disks: {0}" -f ([int]$storageSummary.unhealthyDisks)) ) } else { @('No storage summary data available.') } }, @{ title = 'Next Steps' lines = @( 'Review WAF Compliance Roadmap (Now / Next / Later)' 'Schedule capacity expansion if any status is Warning or Critical' 'Validate idle VMs for decommission or rightsize' 'Re-run Ranger quarterly for drift detection' ) } ) # Use OPC packaging to build the .pptx if (Test-Path $OutputPath) { Remove-Item $OutputPath -Force } $pkg = [System.IO.Packaging.Package]::Open($OutputPath, [System.IO.FileMode]::Create) try { Add-PptxCorePart -Package $pkg Add-PptxPresentationPart -Package $pkg -SlideCount $slides.Count for ($i = 0; $i -lt $slides.Count; $i++) { Add-PptxSlidePart -Package $pkg -Index ($i + 1) -Slide $slides[$i] } } finally { $pkg.Close() } return [ordered]@{ status = 'ok' path = $OutputPath slideCount = $slides.Count sizeBytes = (Get-Item $OutputPath).Length } } function Add-PptxCorePart { param([System.IO.Packaging.Package]$Package) $relsUri = [System.IO.Packaging.PackUriHelper]::CreatePartUri('/_rels/.rels') # Implicitly created by AddRelationship below. $null = $Package.CreateRelationship( [uri]'/ppt/presentation.xml', [System.IO.Packaging.TargetMode]::Internal, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument', 'rId1') } function Add-PptxPresentationPart { param( [System.IO.Packaging.Package]$Package, [int]$SlideCount ) $presUri = [System.IO.Packaging.PackUriHelper]::CreatePartUri('/ppt/presentation.xml') $pres = $Package.CreatePart($presUri, 'application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml') $slideIdEntries = for ($i = 1; $i -le $SlideCount; $i++) { " <p:sldId id='$(255 + $i)' r:id='rId$i'/>" } $slideIdsXml = ($slideIdEntries -join [Environment]::NewLine) $presXml = @" <?xml version='1.0' encoding='UTF-8' standalone='yes'?> <p:presentation xmlns:a='http://schemas.openxmlformats.org/drawingml/2006/main' xmlns:r='http://schemas.openxmlformats.org/officeDocument/2006/relationships' xmlns:p='http://schemas.openxmlformats.org/presentationml/2006/main'> <p:sldIdLst> $slideIdsXml </p:sldIdLst> <p:sldSz cx='9144000' cy='6858000' type='screen4x3'/> <p:notesSz cx='6858000' cy='9144000'/> </p:presentation> "@ $sw = New-Object System.IO.StreamWriter($pres.GetStream(), [System.Text.Encoding]::UTF8) try { $sw.Write($presXml) } finally { $sw.Close() } for ($i = 1; $i -le $SlideCount; $i++) { $null = $pres.CreateRelationship( [uri]"/ppt/slides/slide$i.xml", [System.IO.Packaging.TargetMode]::Internal, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide', "rId$i") } } function Add-PptxSlidePart { param( [System.IO.Packaging.Package]$Package, [int]$Index, [hashtable]$Slide ) $uri = [System.IO.Packaging.PackUriHelper]::CreatePartUri("/ppt/slides/slide$Index.xml") $part = $Package.CreatePart($uri, 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml') $titleEsc = [System.Security.SecurityElement]::Escape([string]$Slide.title) $bodyParagraphs = foreach ($line in @($Slide.lines)) { $e = [System.Security.SecurityElement]::Escape([string]$line) "<a:p><a:r><a:rPr lang='en-US' sz='1800'/><a:t>$e</a:t></a:r></a:p>" } $bodyXml = ($bodyParagraphs -join '') $xml = @" <?xml version='1.0' encoding='UTF-8' standalone='yes'?> <p:sld xmlns:a='http://schemas.openxmlformats.org/drawingml/2006/main' xmlns:r='http://schemas.openxmlformats.org/officeDocument/2006/relationships' xmlns:p='http://schemas.openxmlformats.org/presentationml/2006/main'> <p:cSld> <p:spTree> <p:nvGrpSpPr><p:cNvPr id='1' name=''/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr> <p:grpSpPr><a:xfrm><a:off x='0' y='0'/><a:ext cx='9144000' cy='6858000'/><a:chOff x='0' y='0'/><a:chExt cx='9144000' cy='6858000'/></a:xfrm></p:grpSpPr> <p:sp> <p:nvSpPr><p:cNvPr id='2' name='Title'/><p:cNvSpPr><a:spLocks noGrp='1'/></p:cNvSpPr><p:nvPr><p:ph type='title'/></p:nvPr></p:nvSpPr> <p:spPr><a:xfrm><a:off x='457200' y='274638'/><a:ext cx='8229600' cy='800000'/></a:xfrm></p:spPr> <p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang='en-US' sz='3200' b='1'/><a:t>$titleEsc</a:t></a:r></a:p></p:txBody> </p:sp> <p:sp> <p:nvSpPr><p:cNvPr id='3' name='Content'/><p:cNvSpPr><a:spLocks noGrp='1'/></p:cNvSpPr><p:nvPr><p:ph idx='1'/></p:nvPr></p:nvSpPr> <p:spPr><a:xfrm><a:off x='457200' y='1200000'/><a:ext cx='8229600' cy='5200000'/></a:xfrm></p:spPr> <p:txBody><a:bodyPr wrap='square'/><a:lstStyle/>$bodyXml</p:txBody> </p:sp> </p:spTree> </p:cSld> </p:sld> "@ $sw = New-Object System.IO.StreamWriter($part.GetStream(), [System.Text.Encoding]::UTF8) try { $sw.Write($xml) } finally { $sw.Close() } } |