run-ReportAzureResourceGroupBilling.ps1
<#PSScriptInfo
.VERSION 1.0 .GUID 89267615-ea56-44a8-8465-6203b930e4df .AUTHOR matt.quickenden .COMPANYNAME ACE .COPYRIGHT .TAGS .LICENSEURI .PROJECTURI http://www.azurefieldnotes.com/2018/02/08/reporting-on-resource-group-tags-in-azure .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES #> <# .DESCRIPTION A powerful module for creating HTML reports within PowerShell no HTML coding required. For more details on what is possible, you can view the help file https://azurefieldnotesblog.blob.core.windows.net/wp-content/2017/06/Help-ReportHTML2.html There is a multi-part blog series. (which is a little out of date but still relevant) http://www.azurefieldnotes.com/2016/08/04/powershellhtmlreportingpart1 #> #####Requires �Modules ReportHTML Param ( [parameter(Mandatory=$false,ValueFromPipeline = $true)] [Array]$KeyNames = @('Owner','Solution'), [parameter(Mandatory=$false)] [String]$ReportOutputPath=$env:temp, [parameter(Mandatory=$false)] [String]$YouLogoHereURLString , [parameter(Mandatory=$false)] [string]$SubscriptionID, $skipSize = 1 #$EmailDomain = "*" ) #Login-AzureRmAccount function Get-AzureRmCachedAccessToken() { $ErrorActionPreference = 'Stop' if(-not (Get-Module AzureRm.Profile)) { Import-Module AzureRm.Profile } $azureRmProfileModuleVersion = (Get-Module AzureRm.Profile).Version # refactoring performed in AzureRm.Profile v3.0 or later if($azureRmProfileModuleVersion.Major -ge 3) { $azureRmProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile if(-not $azureRmProfile.Accounts.Count) { Write-Error "Ensure you have logged in before calling this function." } } else { # AzureRm.Profile < v3.0 $azureRmProfile = [Microsoft.WindowsAzure.Commands.Common.AzureRmProfileProvider]::Instance.Profile if(-not $azureRmProfile.Context.Account.Count) { Write-Error "Ensure you have logged in before calling this function." } } $currentAzureContext = Get-AzureRmContext $profileClient = New-Object Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient($azureRmProfile) Write-Debug ("Getting access token for tenant" + $currentAzureContext.Subscription.TenantId) $token = $profileClient.AcquireAccessToken($currentAzureContext.Subscription.TenantId) $token.AccessToken } #[switch]$AutoKeyName =$false #$m = Get-Module -List ReportHTML #if(!$m) {"Can't locate module ReportHTML. Use Install-module ReportHTML";break} #else {import-module reporthtml} if ([string]::IsNullOrEmpty($(Get-AzureRmContext).Account)) {Login-AzureRmAccount} if ($SubscriptionID -eq '') { $RGs = @() $subs = @(Get-AzureRmSubscription) foreach ($sub in $subs) { Write-Verbose "selecting subscription $($sub.SubscriptionId) $($sub.Name)" Select-AzureRmSubscription $sub.SubscriptionId $RGs += Get-AzureRmResourceGroup } } else { Select-AzureRmSubscription $SubscriptionID $RGs = Get-AzureRmResourceGroup } if ($KeyNames.count -eq 0) { [switch]$AutoKeyName =$true $KeyNames = (($rgs.Tags.keys) | select -Unique) } $SubscriptionRGs = @() foreach ($RG in $RGs) { $myRG = [PSCustomObject]@{ ResourceGroupId = $RG.ResourceId SubscriptionId = $RG.ResourceId.Split('/')[2] Subscription = ($SUBS | ? {$_.id -eq $RG.ResourceId.Split('/')[2]}).name ResourceGroupName = $RG.ResourceGroupName Location = $RG.Location Link = ("URL01" + "https://portal.azure.com/#resource" + $RG.ResourceId + "URL02" + ($RG.ResourceId.Split('/') | select -last 1) + "URL03" ) } $i=0 foreach ($KeyName in $KeyNames) { if ($AutoKeyName) { $myRG | Add-Member -MemberType NoteProperty -Name ([string]$i + "_" + $keyname) -Value $rg.Tags.($KeyName) $i++ } else { $myRG | Add-Member -MemberType NoteProperty -Name ($keyname) -Value $rg.Tags.($KeyName) } } $SubscriptionRGs += $myRG } $year =(get-date).year $month =(get-date).Month $DaysInMonth= [DateTime]::DaysInMonth($year, $month ) $token = Get-AzureRmCachedAccessToken $headers = @{"authorization"="bearer $token"} $Body = @{"type"="Usage";"timeframe"="Custom";"timePeriod"=@{"from"="$($year)-$($month)-01T00:00:00+00:00";"to"="$($year)-$($month)-$($DaysInMonth)T23:59:59+00:00"};"dataSet"=@{"granularity"="Daily";"aggregation"=@{"totalCost"=@{"name"="PreTaxCost";"function"="Sum"}};"sorting"=@(@{"direction"="ascending";"name"="UsageDate"})}} $Records = @() #$SubscriptionRG = $SubscriptionRGs[5] foreach ($SubscriptionRG in $SubscriptionRGs ) { $usageUri = "https://management.azure.com/subscriptions/$($SubscriptionRG.SubscriptionId)/resourceGroups/$($SubscriptionRG.ResourceGroupName)/providers/Microsoft.CostManagement/query?api-version=2018-08-31" $Record = '' | select usage, SubscriptionRG $Record.usage = Invoke-RestMethod $usageUri -Headers $headers -ContentType "application/json" -Method Post -Body ($body | ConvertTo-Json -Depth 100) $Record.SubscriptionRG = $SubscriptionRG $Records += $Record } $DataSetDateRange = $Records.usage.properties.rows | % {$_[1]} | select -Unique $RGCosts =@() #$usageResult = $usageResults[10] foreach ($Record in $Records) { $RGCost = '' | select SubscriptionID, Subscription,Location, Link, ResourceGroupName, Owner,OwnerName, Solution,OwnerSolution, UsageData ,TotalCost $UsageData = @() foreach ($DataDate in $DataSetDateRange) { $NewDate = ([string]$DataDate ).substring(6,2) +'/' + ([string]$DataDate ).substring(4,2) +'/' + ([string]$DataDate ).substring(2,2) $AllRows = $Record.usage.properties.rows $Currency = $Record.usage.properties.rows[0][2] $FoundRecord = $AllRows | ? {$_[1] -eq $DataDate} $Usage = '' | select Cost, Date, Currency if ($FoundRecord.Count -eq 3) { $Usage.Cost = [Math]::round(([float]$FoundRecord[0]),1) $Usage.Date = $NewDate $Usage.Currency = $Currency } else { $Usage.Cost = 0 $Usage.Date = $NewDate $Usage.Currency = $Currency } $UsageData += $Usage } if ([int]($UsageData | measure -Sum -Property cost).Sum -gt $skipSize) { $RGCost.SubscriptionID = $Record.SubscriptionRG.SubscriptionId $RGCost.Subscription = $Record.SubscriptionRG.Subscription $RGCost.ResourceGroupName = $Record.SubscriptionRG.ResourceGroupName $RGCost.Link = $Record.SubscriptionRG.Link $RGCost.Location = $Record.SubscriptionRG.Location $RGCost.Owner= $Record.SubscriptionRG.Owner $RGCost.OwnerName= $Record.SubscriptionRG.Owner.Split('@')[0] $RGCost.Solution = $Record.SubscriptionRG.Solution $RGCost.OwnerSolution= ($RGCost.OwnerName + "-" + $RGCost.Solution) $RGCost.UsageData = $UsageData $RGCost.TotalCost = [int]($UsageData | measure -Sum -Property cost).Sum $RGCosts += $RGCost } else { write-verbose ("Record skipped cost less than $skipSize" ) } } write-verbose ("Records created " + [string]$RGCosts.Count) $PeopleCosts = @() $PeopleGrouping = ($RGCosts | group Owner) foreach ($costGroup in $PeopleGrouping ) { $PeopleCost = '' | select Owner,OwnerName, UsageData ,Currency, Total $PeopleCost.Owner = $costGroup.Name $PeopleCost.OwnerName = $costGroup.name.Split('@')[0] $UsageGrouping = $costGroup.Group.usagedata.date | select -Unique $PeopleCost.currency = $costGroup.Group.usagedata.currency[0] $DailyTotals = @() foreach ($UsageDate in $UsageGrouping) { $DailyTotal = '' | select 'CostTotal','Currency','Date' $DailyRecords = @(@($costGroup.Group.usagedata) | ? {$_.date -eq $UsageDate}) $DailyTotal.Date = $UsageDate $DailyTotal.Currency = $DailyRecords[0].currency $DailyTotal.costTotal = ($DailyRecords | measure cost -Sum).sum $DailyTotals += $DailyTotal } $PeopleCost.UsageData = $DailyTotals $PeopleCost.Total = ($DailyTotals | measure costtotal -Sum).sum $PeopleCosts+= $PeopleCost } remove-module reporthtml import-module C:\Users\matth\Documents\GitHub\ReportHTML\ReportHTML\ReportHTML.psd1 $rpt = @() $tabarray = @('Stacked Charts','Costs','Resource Groups','Missing Tags') if ($YouLogoHereURLString -ne $null) { $rpt += Get-HTMLOpenPage -TitleText "Azure Cost Breakdown" -LeftLogoName Corporate -RightLogoName Alternate } else { $rpt += Get-HTMLOpenPage -TitleText "Azure Cost Breakdown" -LeftLogoName Corporate -RightLogoName Alternate } $rpt += Get-HTMLTabHeader -TabNames $tabarray $rpt += get-htmltabcontentopen -TabName 'Stacked Charts' -tabheading ('Stacked Spend Charts') $StackChartObject1 = Get-HTMLStackedChartObject $StackChartObject1.Size.Width =1400 $StackChartObject1.Size.Height = 600 $StackChartObject1.DataDefinition.DataSetArrayData = 'UsageData' $StackChartObject1.DataDefinition.DataSetArrayDataColumn = 'Cost' $StackChartObject1.DataDefinition.DataSetArrayName ='OwnerSolution' $StackChartObject1.DataDefinition.DataSetArrayXLabels = 'Date' $StackChartObject1.ChartStyle.hover.mode = 'dataset' $StackChartObject1.ChartStyle.hover.intersect = 'true' $StackChartObject1.ChartStyle.tooltips.mode = 'dataset' $StackChartObject1.ChartStyle.tooltips.intersect = 'true' $StackChartObject2 = Get-HTMLStackedChartObject $StackChartObject2.Size.Width =1400 $StackChartObject2.Size.Height = 600 $StackChartObject2.DataDefinition.DataSetArrayData = 'UsageData' $StackChartObject2.DataDefinition.DataSetArrayDataColumn = 'CostTotal' $StackChartObject2.DataDefinition.DataSetArrayName ='OwnerName' $StackChartObject2.DataDefinition.DataSetArrayXLabels = 'Date' $StackChartObject2.ChartStyle.hover.mode = 'x' $StackChartObject2.ChartStyle.hover.intersect = 'false' $StackChartObject2.ChartStyle.tooltips.mode = 'x' $StackChartObject2.ChartStyle.tooltips.intersect = 'false' $rpt += Get-HTMLContentOpen -HeaderText "By Person" $rpt += Get-HTMLStackedChart -ChartObject $StackChartObject2 -DataSet ($PeopleCosts | sort total -Descending) $rpt += Get-HTMLContentclose $rpt += Get-HTMLContentOpen -HeaderText "Person & Solution Cost" $rpt += Get-HTMLStackedChart -ChartObject $StackChartObject1 -DataSet $RGCosts $rpt += Get-HTMLContentclose $rpt += get-htmltabcontentclose $rpt += get-htmltabcontentopen -TabName 'Resource Groups' -tabheading ('Resource Groups') if (!$AutoKeyName) { $Pie1 = $SubscriptionRGs| group $KeyNames[0] $Pie2 = $SubscriptionRGs| group $KeyNames[1] $Pie1Object = Get-HTMLPieChartObject -ColorScheme Random $Pie2Object = Get-HTMLPieChartObject -ColorScheme Generated $rpt += Get-HTMLContentOpen -HeaderText "Pie Charts" $rpt += Get-HTMLColumnOpen -ColumnNumber 1 -ColumnCount 2 $rpt += Get-HTMLPieChart -ChartObject $Pie1Object -DataSet $Pie1 $rpt += Get-HTMLColumnClose $rpt += Get-HTMLColumnOpen -ColumnNumber 2 -ColumnCount 2 $rpt += Get-HTMLPieChart -ChartObject $Pie2Object -DataSet $Pie2 $rpt += Get-HTMLColumnClose $rpt += Get-HTMLContentclose } $rpt += Get-HTMLContentOpen -HeaderText "Complete List" $rpt += Get-HTMLContentdatatable -ArrayOfObjects ( $SubscriptionRGs | select ResourceGroupName, Location, Link,Owner,Solution) $rpt += Get-HTMLContentClose $rpt += get-htmltabcontentclose $rpt += get-htmltabcontentopen -TabName 'Costs' -tabheading ('Costs') $Pie3 = $RGCosts | group Owner | select Name, @{ Name = 'Count'; Expression = { ($_.Group | Measure-Object -Property TotalCost -Sum).Sum } } $Pie4 = $RGCosts | group Solution | select Name, @{ Name = 'Count'; Expression = { ($_.Group | Measure-Object -Property TotalCost -Sum).Sum } } $Pie3Object = Get-HTMLPieChartObject -ColorScheme Random $Pie4Object = Get-HTMLPieChartObject -ColorScheme Generated $rpt += Get-HTMLContentOpen -HeaderText "Pie Charts" $rpt += Get-HTMLColumnOpen -ColumnNumber 1 -ColumnCount 2 $rpt += Get-HTMLPieChart -ChartObject $Pie3Object -DataSet $Pie3 $rpt += Get-HTMLColumnClose $rpt += Get-HTMLColumnOpen -ColumnNumber 2 -ColumnCount 2 $rpt += Get-HTMLPieChart -ChartObject $Pie4Object -DataSet $Pie4 $rpt += Get-HTMLColumnClose $rpt += Get-HTMLContentclose $rpt += Get-HTMLContentOpen -HeaderText "Complete List" $rpt += Get-HTMLContentdatatable -ArrayOfObjects ( $RGCosts | select ResourceGroupName, Location, Link,Owner,Solution, TotalCost) $rpt += Get-HTMLContentClose $rpt += get-htmltabcontentclose #Missing tags $rpt += get-htmltabcontentopen -TabName 'Missing Tags' -tabheading ('Missing Tags') $rpt += Get-HTMLContentOpen -HeaderText "Resource Groups Missing Tags" $rpt += Get-HTMLContenttable -ArrayOfObjects ( $SubscriptionRGs | ? {$_.owner -eq $null -or $_.solution -eq $null} | select ResourceGroupName, Location, Link,Owner,Solution) $rpt += Get-HTMLContentClose $rpt += get-htmltabcontentclose $rpt += Get-HTMLClosePage if ($ReportOutputPath -ne $null) { $ReportFile = Save-HTMLReport -ReportContent $rpt -ReportName CostBreakdown -ReportPath $ReportOutputPath } else { Save-HTMLReport -ShowReport -ReportContent $rpt -ReportName CostBreakdown } $SendTo = $PeopleCosts # | ? {$_.Owner -like $EmailDomain } foreach ($People in $SendTo ) { $DailyAvg = [math]::round(($People.UsageData | measure CostTotal -Average).Average,0) $DailyMax = [math]::round(($People.UsageData | measure CostTotal -Maximum).Maximum,0) $CostSum = [math]::round(($People.UsageData | measure CostTotal -Sum).sum,0) write-host "sending $($People.Owner) $($CostSum ) $($DailyMax) $($DailyAvg)" $Outlook = New-Object -ComObject Outlook.Application $Mail = $Outlook.CreateItem(0) $Mail.To = $People.Owner $Mail.Subject = "Total: $('$')$($CostSum) | Daily Max: $('$')$($DailyMax) Avg: $('$')$($DailyAvg)" $Mail.Body ="Please find attached Azure Spend Report. " #write-host "Total: $('$')$($CostSum) | Daily Max: $('$')$($DailyMax) Avg: $('$')$($DailyAvg)" $Mail.Attachments.add($ReportFile) $Mail.Send() } |