Private/Read-AACInventoryCost.ps1
|
function Read-AACInventoryCost { <# .SYNOPSIS Reads the actual cost of every resource - month to date and last month - for Get-AACInventory -Cost, as few Cost Management queries as the billing account allows. .DESCRIPTION One query, grouped by ResourceId and SubscriptionId, monthly, from the first of last month to today: - at management-group scope first - -ManagementGroupId, or the tenant root group - one query for everything. Cost Management answers there for Enterprise Agreement and Microsoft Customer Agreement billing, not pay-as-you-go - otherwise (or with -PerSubscription: a subscription or resource group selection) once per subscription, three at a time (Invoke-AACCostBatch) Subscriptions that are disabled, deleted or expired aren't asked. Returns @{ Rows; Status (subscription ID -> 'OK' or why it couldn't be read); ThisMonth; LastMonth ('yyyy-MM'); Period; Notice }. #> [CmdletBinding()] [OutputType([hashtable])] param( [string] $TenantId, # Rows with subscriptionId, name and state. [AllowEmptyCollection()] [object[]] $Subscription = @(), [string[]] $ManagementGroupId, [switch] $PerSubscription ) $invariant = [cultureinfo]::InvariantCulture $today = (Get-Date).Date $thisMonth = [datetime]::new($today.Year, $today.Month, 1) $lastMonth = $thisMonth.AddMonths(-1) $body = @{ type = 'ActualCost' timeframe = 'Custom' timePeriod = @{ from = $lastMonth.ToString('yyyy-MM-ddT00:00:00Z', $invariant); to = $today.ToString('yyyy-MM-ddT23:59:59Z', $invariant) } dataset = @{ granularity = 'Monthly' aggregation = @{ totalCost = @{ name = 'Cost'; function = 'Sum' } } grouping = @( @{ type = 'Dimension'; name = 'ResourceId' } @{ type = 'Dimension'; name = 'SubscriptionId' } ) } } $value = { param($Row, [string] $Key) if ($Row -is [System.Collections.IDictionary]) { $Row[$Key] } else { Get-AACPropertyValue -InputObject $Row -Name $Key } } $result = @{ Rows = @() Status = @{} ThisMonth = $thisMonth.ToString('yyyy-MM', $invariant) LastMonth = $lastMonth.ToString('yyyy-MM', $invariant) Period = "actual cost, $($thisMonth.ToString('MMM yyyy')) to date and $($lastMonth.ToString('MMM yyyy'))" Notice = @() } $ids = @($Subscription | ForEach-Object { ([string](& $value $_ 'subscriptionId')).ToLowerInvariant() } | Where-Object { $_ } | Select-Object -Unique) if (-not $ids.Count) { return $result } # --- One query for the whole scope, when Cost Management allows it ----------------------------- $groups = @(if ($ManagementGroupId) { $ManagementGroupId } elseif ($TenantId) { $TenantId }) if (-not $PerSubscription -and $groups.Count) { Update-AACProgress -Id 'cost' -Total $groups.Count -Description "Reading costs for the management group$(if ($groups.Count -ne 1) { 's' })" $read = Invoke-AACCostBatch -Scope @($groups | ForEach-Object { "/providers/Microsoft.Management/managementGroups/$_" }) -Body $body -OnProgress { param($Scope, $Status, $Done, $Total) Update-AACProgress -Id 'cost' -Increment 1 } $failed = @($read.Values | Where-Object { $_.Status -ne 'OK' }) if (-not $failed.Count) { $result.Rows = @($read.Values | ForEach-Object { $_.Rows }) foreach ($id in $ids) { $result.Status[$id] = 'OK' } Update-AACProgress -Id 'cost' -Complete -Description ('Cost Management: {0:N0} cost row(s) for {1:N0} subscription(s), in one query per management group' -f $result.Rows.Count, $ids.Count) return $result } Write-Verbose "Cost Management didn't answer for the management group ($($failed[0].Status)); reading each subscription instead." Update-AACProgress -Id 'cost' -Complete -Description 'Cost Management: not available for the management group - reading each subscription' } # --- Otherwise once per subscription, three at a time ------------------------------------------------- $names = @{} $scopes = [System.Collections.Generic.List[string]]::new() foreach ($row in $Subscription) { $id = ([string](& $value $row 'subscriptionId')).ToLowerInvariant() if (-not $id -or $names.Contains($id)) { continue } $names[$id] = [string](& $value $row 'name') $state = [string](& $value $row 'state') if ($state -in 'Disabled', 'Deleted', 'Expired') { $result.Status[$id] = "Not read: the subscription is $state."; continue } $scopes.Add("/subscriptions/$id") } Update-AACProgress -Id 'cost' -Total ([Math]::Max(1, $scopes.Count)) -Description "Reading costs for $($scopes.Count) subscription(s)" $read = Invoke-AACCostBatch -Scope $scopes.ToArray() -Body $body -OnProgress { param($Scope, $Status, $Done, $Total) $id = $Scope -replace '^/subscriptions/', '' Update-AACProgress -Id 'cost' -Increment 1 -Description "Read costs: $(if ($names[$id]) { $names[$id] } else { $id }) ($Done of $Total)" } $rows = [System.Collections.Generic.List[object]]::new() foreach ($scope in $scopes) { $id = $scope -replace '^/subscriptions/', '' $entry = if ($read.Contains($scope)) { $read[$scope] } else { @{ Rows = @(); Status = 'Not read.' } } $result.Status[$id] = $entry.Status foreach ($row in @($entry.Rows)) { $rows.Add($row) } } $result.Rows = $rows.ToArray() $unread = @($result.Status.Values | Where-Object { $_ -ne 'OK' }).Count Update-AACProgress -Id 'cost' -Complete -Description ('Cost Management: {0:N0} subscription(s) read{1}' -f ($ids.Count - $unread), $(if ($unread) { ", $unread could not be" })) if ($unread) { $result.Notice = @("The cost of $unread subscription(s) couldn't be read (the subscription's CostStatus says why); they show no cost.") } $result } |