Public/Export-AACDiagram.ps1
|
function Export-AACDiagram { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Draws your existing Azure resources as an editable draw.io diagram - resources, the network topology or Application Gateways - with real containers, Azure icons and attached connections; -Update refreshes it from Azure and keeps the changes you made. .DESCRIPTION Reads Azure (read-only, Reader is enough) the way the map commands do and writes a .drawio file you open and edit in draw.io - the desktop app, app.diagrams.net, the Draw.io Integration for VS Code, Confluence - and export from there to Visio (.vsdx), PNG, SVG or PDF. -View chooses what is drawn: Resources the resources in resource groups or subscriptions, in subscription > resource group > virtual network > subnet boxes, with their connections: network links, peerings, private endpoints, routes, DNS links and dependencies (as Show-AACResourceMap) Network the network topology of a tenant, management groups, subscriptions or regions: hubs and spokes, peerings, gateways, firewalls and appliances (as Show-AACNetworkTopology) ApplicationGateway each gateway's flow - listeners > rules > backend settings > pools and targets - with probes, WAF policies and health (as Show-AACApplicationGatewayMap) Governance the management group hierarchy: the tenant root and management groups as nested boxes, each subscription inside its group, with the policy and role assignments at each level (as Show-AACManagementGroupMap) In the diagram: - boxes are draw.io containers: move a box and what is in it moves with it; drag a resource into another box and it belongs there - resources carry the Azure icons (vector), their name and type; unattached ones are red, those outside the selection faded - connections are attached at both ends and follow what you move - each shape keeps its resource ID, type, location, resource group and subscription (Edit > Edit Data, or hover), and Ctrl+click opens it in the Azure portal - a Legend page shows the kinds of box and connection, what was read and when (-NoLegend leaves it out) -SplitBy puts each subscription, resource group, management group, region or gateway on a page of its own, for large estates. Edit it as you like - move, restyle, add notes, users, on-premises shapes - then refresh it with -Update: it reads Azure again and merges into the file: - what you moved, restyled or relabelled stays as you left it - new resources are added into their box (which grows to fit) and marked green until the next refresh - resources no longer in Azure are faded and red - or deleted, with -Prune - shapes and connections you added are left alone Without -Update an existing file is not overwritten unless you add -Force. To look at rather than edit - or to send to someone without draw.io - add a clean copy alongside: -HtmlPath an interactive page, self-contained (opens offline): the diagram laid out with a legend of its connections and boxes, to pan, zoom, search and click for details -PdfPath one page: the title and what was read, the whole diagram and its legend, in vector so it stays sharp when zoomed or printed (drawn by Microsoft Edge or Google Chrome, headless) Both show what was read from Azure this time, on one page whatever -SplitBy says - not the changes you made in draw.io. .PARAMETER View Resources (the default), Network, ApplicationGateway or Governance. .PARAMETER SubscriptionId Only these subscriptions. .PARAMETER ResourceGroupName Only these resource groups (Resources and ApplicationGateway views). .PARAMETER ManagementGroupId Only the subscriptions under these management groups (Network and Governance views). .PARAMETER Location Only these regions (Network view). .PARAMETER Name Only the gateways with these names; wildcards work (ApplicationGateway view). .PARAMETER GroupBy Network view: the boxes around the networks - ManagementGroup (the default), Subscription or Region. .PARAMETER NetworkOnly Network view: only the network fabric, without NICs, VMs, scale sets and private endpoints. .PARAMETER IncludeHealth ApplicationGateway view: run the live backend health check (30-90 seconds per gateway) instead of reading health metrics. .PARAMETER ExcludeType Leave out these resource types (Resources and Network views); wildcards work, for example 'microsoft.compute/disks'. .PARAMETER Path The .drawio file to write - or, with -Update, to refresh. .PARAMETER SplitBy A page per Subscription, ResourceGroup, ManagementGroup, Region or Gateway; None (the default) for one page. .PARAMETER Update Refresh the diagram at -Path from Azure, keeping your changes. .PARAMETER Prune With -Update: delete the resources no longer in Azure instead of fading them. .PARAMETER NoLegend Leave out the Legend page. .PARAMETER Title The diagram's title (the page name, without -SplitBy). .PARAMETER Force Overwrite an existing file without -Update. .PARAMETER HtmlPath Also write the diagram as an interactive HTML page here, with its legend - a clean copy to view or share. .PARAMETER PdfPath Also write the diagram as a one-page PDF here, with its title, what was read and its legend. Needs Microsoft Edge or Google Chrome installed. .PARAMETER Open Open what was written: the diagram (in the app .drawio files open with), and the HTML page and PDF when you asked for them. .PARAMETER PassThru Return what was written: the paths, pages, shapes and connections (and, with -Update, what was kept, added and removed). .EXAMPLE Connect-AAC Export-AACDiagram -ResourceGroupName 'rg-hub', 'rg-spoke-app' -Path .\estate.drawio -Open Two resource groups as an editable diagram, opened in draw.io. .EXAMPLE Export-AACDiagram -View Network -ManagementGroupId 'mg-landingzones' -SplitBy Subscription -Path .\network.drawio The landing zones' network topology, a page per subscription. .EXAMPLE Export-AACDiagram -ResourceGroupName 'rg-hub', 'rg-spoke-app' -Path .\estate.drawio -Update Refreshes the diagram you edited: new resources added and marked, removed ones faded, your changes kept. .EXAMPLE Export-AACDiagram -View ApplicationGateway -ResourceGroupName 'rg-edge' -IncludeHealth -Path .\gateways.drawio The gateways' flows with live backend health. .EXAMPLE Export-AACDiagram -View Governance -Path .\hierarchy.drawio -PdfPath .\hierarchy.pdf The management group hierarchy as an editable diagram, and a one-page PDF. .EXAMPLE Export-AACDiagram -SubscriptionId $sub -Path .\estate.drawio -HtmlPath .\estate.html -PdfPath .\estate.pdf The editable diagram, plus a clean interactive page and a one-page PDF, each with a legend, to share. .OUTPUTS AAC.Diagram, with -PassThru #> [CmdletBinding()] [OutputType('AAC.Diagram')] param( [ValidateSet('Resources', 'Network', 'ApplicationGateway', 'Governance')] [string] $View = 'Resources', [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [ValidateNotNullOrEmpty()] [string[]] $ResourceGroupName, [ValidateNotNullOrEmpty()] [string[]] $ManagementGroupId, [ValidateNotNullOrEmpty()] [string[]] $Location, [SupportsWildcards()] [string[]] $Name, [ValidateSet('ManagementGroup', 'Subscription', 'Region')] [string] $GroupBy = 'ManagementGroup', [switch] $NetworkOnly, [switch] $IncludeHealth, [SupportsWildcards()] [string[]] $ExcludeType, [Parameter(Mandatory)] [string] $Path, [ValidateSet('None', 'Subscription', 'ResourceGroup', 'ManagementGroup', 'Region', 'Gateway')] [string] $SplitBy = 'None', [switch] $Update, [switch] $Prune, [switch] $NoLegend, [string] $Title, [string] $HtmlPath, [string] $PdfPath, [switch] $Force, [switch] $Open, [switch] $PassThru ) trap { if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { return }; $PSCmdlet.ThrowTerminatingError((Show-AACError -ErrorRecord $_ -Cmdlet $PSCmdlet)) } # What each view takes: a parameter it doesn't is a mistake worth saying. $takes = @{ Resources = @('SubscriptionId', 'ResourceGroupName', 'ExcludeType') Network = @('SubscriptionId', 'ManagementGroupId', 'Location', 'GroupBy', 'NetworkOnly', 'ExcludeType') ApplicationGateway = @('SubscriptionId', 'ResourceGroupName', 'Name', 'IncludeHealth') Governance = @('SubscriptionId', 'ManagementGroupId') } foreach ($parameter in 'ResourceGroupName', 'ManagementGroupId', 'Location', 'Name', 'GroupBy', 'NetworkOnly', 'IncludeHealth', 'ExcludeType') { if ($PSBoundParameters.ContainsKey($parameter) -and $parameter -notin $takes[$View]) { throw "Azure Admin Console: -$parameter doesn't apply to -View $View. It takes $(($takes[$View] | ForEach-Object { "-$_" }) -join ', ')." } } if ($View -eq 'Resources' -and -not $SubscriptionId -and -not $ResourceGroupName) { throw 'Azure Admin Console: say what to draw - -SubscriptionId, -ResourceGroupName, or both.' } if ($Prune -and -not $Update) { throw 'Azure Admin Console: -Prune works with -Update (it deletes what is no longer in Azure from the diagram you are refreshing).' } $splitKind = @{ None = ''; Subscription = 'subscription'; ResourceGroup = 'resourcegroup'; ManagementGroup = 'managementgroup'; Region = 'region'; Gateway = 'appgw' }[$SplitBy] $fullPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) $exists = Test-Path -LiteralPath $fullPath $htmlFullPath = if ($HtmlPath) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($HtmlPath) } else { '' } $pdfFullPath = if ($PdfPath) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PdfPath) } else { '' } if ($exists -and -not $Update -and -not $Force) { throw "Azure Admin Console won't overwrite $fullPath - it may hold your edits. Add -Update to refresh it and keep them, or -Force to replace it." } $request = @{ View = $View; SubscriptionId = @($SubscriptionId | Where-Object { $_ }); ResourceGroupName = @($ResourceGroupName | Where-Object { $_ }); ManagementGroupId = @($ManagementGroupId | Where-Object { $_ }) Location = @($Location | Where-Object { $_ }); Name = @($Name | Where-Object { $_ }); GroupBy = $GroupBy; NetworkOnly = [bool]$NetworkOnly; IncludeHealth = [bool]$IncludeHealth ExcludeType = @($ExcludeType | Where-Object { $_ }); Title = $Title; SplitKind = $splitKind; Legend = -not $NoLegend; Merge = [bool]($Update -and $exists); Prune = [bool]$Prune } Write-AACRule -Title 'Azure Admin Console :: Diagram' -Color 'deepskyblue3_1' $state = Invoke-AACProgress -ScriptBlock { $null = Get-AACAccessToken $built = switch ($request.View) { 'Network' { Read-AACNetworkTopology -ManagementGroupId $request.ManagementGroupId -SubscriptionId $request.SubscriptionId -Location $request.Location -GroupBy $request.GroupBy -NetworkOnly:$request.NetworkOnly -ExcludeType $request.ExcludeType -Title $request.Title } 'Governance' { Read-AACManagementGroupMap -ManagementGroupId $request.ManagementGroupId -SubscriptionId $request.SubscriptionId -Title $request.Title } 'ApplicationGateway' { Read-AACApplicationGatewayMap -SubscriptionId $request.SubscriptionId -ResourceGroupName $request.ResourceGroupName -Name $request.Name -IncludeHealth:$request.IncludeHealth -Title $request.Title } default { Read-AACResourceMap -SubscriptionId $request.SubscriptionId -ResourceGroupName $request.ResourceGroupName -ExcludeType $request.ExcludeType -Title $request.Title } } Update-AACProgress -Id 'drawio' -Indeterminate -Description 'Drawing the diagram' $asset = Get-AACResourceMapAsset $drawn = ConvertTo-AACDrawio -Map $built.Map -Icons $asset.Icons -Title $built.Title -SplitBy $request.SplitKind -Fact $built.Facts -Legend:$request.Legend $xml = $drawn.Xml $merge = $null if ($request.Merge) { Update-AACProgress -Id 'drawio' -Indeterminate -Description 'Merging into your diagram, keeping your changes' $merge = Merge-AACDrawio -Existing ([System.IO.File]::ReadAllText($fullPath)) -Generated $xml -Prune:$request.Prune $xml = $merge.Xml } $folder = Split-Path -Parent $fullPath if ($folder -and -not (Test-Path -LiteralPath $folder)) { $null = New-Item -ItemType Directory -Path $folder -Force } [System.IO.File]::WriteAllText($fullPath, $xml, [System.Text.UTF8Encoding]::new($false)) Update-AACProgress -Id 'drawio' -Complete -Description ('Diagram: {0:N0} shape(s), {1:N0} connection(s) on {2:N0} page(s){3}' -f $drawn.Shapes, $drawn.Connections, @($drawn.Pages).Count, $(if ($merge) { "; kept $($merge.Kept), added $($merge.Added), removed $($merge.Removed + $merge.Pruned)" })) if ($htmlFullPath -or $pdfFullPath) { # Laid out as the view's own map command lays it out: the network as hub and spoke when there are hubs. $look = @{ Map = $built.Map; Title = $built.Title; Fact = $built.Facts; Theme = 'light'; Layout = 'layered'; Hierarchy = 'layered' } if ($request.View -eq 'Governance') { $look.Layout = 'packed'; $look.Hierarchy = 'packed' } if ($request.View -eq 'Network') { $look.Layout = if ($built.Map['Layout'] -eq 'hubspoke') { 'hubspoke' } else { 'packed' } $look.Hierarchy = 'packed' } if ($htmlFullPath) { Update-AACProgress -Id 'html' -Indeterminate -Description 'Writing the HTML page' Write-AACResourceMapHtml @look -Path $htmlFullPath Update-AACProgress -Id 'html' -Complete -Description 'HTML page written' } if ($pdfFullPath) { Update-AACProgress -Id 'pdf' -Indeterminate -Description 'Drawing the PDF in a headless browser' Write-AACResourceMapPdf @look -Path $pdfFullPath Update-AACProgress -Id 'pdf' -Complete -Description 'PDF written' } } @{ Built = $built; Drawn = $drawn; Merge = $merge } } $drawn = $state.Drawn $merge = $state.Merge $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) } if (-not $drawn.Shapes) { Write-AACMarkup '[grey58]Nothing was found in that scope; the diagram is empty.[/]' } foreach ($page in @($drawn.Pages)) { Write-AACMarkup (" [white]{0}[/] [grey58]{1:N0} shape(s), {2:N0} connection(s)[/]" -f (& $escape $page.Name), $page.Shapes, $page.Connections) } if ($merge) { Write-AACMarkup ("[grey58]Refreshed: [white]{0}[/] kept, [green3]{1}[/] added (marked green), [red1]{2}[/] {3}; your own shapes left as they were.[/]" -f $merge.Kept, $merge.Added, ($merge.Removed + $merge.Pruned), $(if ($Prune) { 'deleted' } else { 'removed (faded)' })) } elseif ($Update) { Write-AACMarkup '[grey58]There was no diagram to update at that path, so a new one was written.[/]' } Write-AACMarkup "[grey58]Diagram: $(& $escape $fullPath) - open it in draw.io (desktop, app.diagrams.net or the VS Code extension) to edit; -Update refreshes it later.[/]" if ($htmlFullPath) { Write-AACMarkup "[grey58]HTML page: $(& $escape $htmlFullPath)[/]" } if ($pdfFullPath) { Write-AACMarkup "[grey58]PDF: $(& $escape $pdfFullPath)[/]" } if ($Open) { foreach ($written in @($fullPath, $htmlFullPath, $pdfFullPath) | Where-Object { $_ }) { Open-AACFile -Path $written } } if ($PassThru) { [pscustomobject]@{ PSTypeName = 'AAC.Diagram' Path = $fullPath HtmlPath = $htmlFullPath PdfPath = $pdfFullPath View = $View Title = $state.Built.Title Pages = @($drawn.Pages | ForEach-Object { [pscustomobject]@{ Name = $_.Name; Shapes = $_.Shapes; Connections = $_.Connections } }) Shapes = $drawn.Shapes Connections = $drawn.Connections Kept = $(if ($merge) { $merge.Kept } else { 0 }) Added = $(if ($merge) { $merge.Added } else { $drawn.Shapes }) Removed = $(if ($merge) { $merge.Removed } else { 0 }) Pruned = $(if ($merge) { $merge.Pruned } else { 0 }) } } } |