diff --git a/1-Collect/Get-AzureServices.ps1 b/1-Collect/Get-AzureServices.ps1 index 0703ffa..e025059 100644 --- a/1-Collect/Get-AzureServices.ps1 +++ b/1-Collect/Get-AzureServices.ps1 @@ -25,6 +25,10 @@ .PARAMETER summaryOutputFile The name of the output file where the summary will be exported. Default is "summary.json". +.PARAMETER includeCost + A boolean flag indicating whether to include cost report generation. Default is $false. Note that this requires the identity + running the script to have permissions to access cost management APIs, i.e. Cost Management Contributor role. + .FUNCTION Get-SingleData Queries Azure Resource Graph for resources within a single subscription and retrieves all results, handling pagination if necessary. @@ -41,16 +45,26 @@ .FUNCTION Get-Method Determines the appropriate method to retrieve resource-specific data based on the resource type and flag type. +.FUNCTION Invoke-CostReportSchedule + Generates a cost report for a specified subscription by invoking the Azure REST API and retrieves cost details + for the previous month. + +.FUNCTION Get-CostReport + Fetches the cost report details from the Azure REST API and processes the results. + +.FUNCTION Get-MeterId + Retrieves unique meter IDs associated with a specific resource ID from the cost details CSV. + .EXAMPLE - PS C:\> .\assess_resources.ps1 -scopeType singleSubscription -subscriptionId "12345678-1234-1234-1234-123456789abc" + PS C:\> .\Get-AzureServices.ps1 -scopeType singleSubscription -subscriptionId "12345678-1234-1234-1234-123456789abc" Runs the script for a single subscription with the specified subscription ID and outputs the results to the default file. .EXAMPLE - PS C:\> .\assess_resources.ps1 -scopeType resourceGroup -resourceGroupName "MyResourceGroup" + PS C:\> .\Get-AzureServices.ps1 -scopeType resourceGroup -resourceGroupName "MyResourceGroup" Runs the script for a specific resource group within the current subscription and outputs the results to the default file. .EXAMPLE - PS C:\> .\assess_resources.ps1 -scopeType multiSubscription -workloadFile "subscriptions.json" -fullOutputFile "output.json" + PS C:\> .\Get-AzureServices.ps1 -scopeType multiSubscription -workloadFile "subscriptions.json" -fullOutputFile "output.json" Runs the script for multiple subscriptions defined in the workload file and outputs the results to "output.json". @@ -69,7 +83,8 @@ param( [Parameter(Mandatory = $false)] [string] $resourceGroupName, # resource group to run the query against [Parameter(Mandatory = $false)] [string] $workloadFile, # JSON file containing subscriptions [Parameter(Mandatory = $false)] [string] $fullOutputFile = "resources.json", # Json file to export the results to - [Parameter(Mandatory = $false)] [string] $summaryOutputFile = "summary.json" # Json file to export the results to + [Parameter(Mandatory = $false)] [string] $summaryOutputFile = "summary.json", # Json file to export the results to + [Parameter(Mandatory = $false)] [bool] $includeCost = $false # Include cost report ) Function Get-SingleData { @@ -98,8 +113,14 @@ Function Get-MultiLoop { $basequery = "resources | where subscriptionId == '$subscription'" Get-SingleData -query $basequery $tempArray += $Script:baseresult + If ($includeCost) { + Invoke-CostReportSchedule -SubscriptionId $subscription + Get-CostReport -PathForResult $pathForResult + $tempCostArray += $Script:costdetails + } } $Script:baseresult = $tempArray + $Script:costdetails = $tempCostArray } Function Get-Property { @@ -191,6 +212,68 @@ Function Get-Method { } } +Function Invoke-CostReportSchedule { + param ( + [Parameter(Mandatory = $true)] [string]$SubscriptionId + ) + $uri = "https://management.azure.com/subscriptions/$($SubscriptionId)/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2025-03-01" + $startDate =(Get-Date).AddDays(-1) + $endDate = (Get-Date) + # Define the request body + $body = @{ + metric = "AmortizedCost" + timePeriod = @{ + start = $startDate.ToString("yyyy-MM-dd") + end = $endDate.ToString("yyyy-MM-dd") + } + } + #Convert the body to JSON + $bodyJson = $body | ConvertTo-Json + $result = invoke-AzRestMethod -Uri $uri -Method POST -Payload $bodyJson + $pathForResult = "https://management.azure.com" + $result.Headers.Location.AbsolutePath + "?api-version=2025-03-01" + write-output "Cost report request submitted. Path for result: $pathForResult" + Set-Variable -Name 'pathForResult' -Value $pathForResult -Scope Script +} + +Function Get-CostReport { + param ( + [Parameter(Mandatory = $true)] [string]$PathForResult + ) + $i = 0 + $details = Invoke-AzRestMethod -uri $PathForResult -Method GET + # Loop until $details.statuscode is 200 + while ($details.StatusCode -eq 202) { + Start-Sleep -Seconds 10 + $i = $i + 10 + $details = Invoke-AzRestMethod -uri $PathForResult -Method GET + Write-Output "Waiting for the cost report to be ready. Elapsed time: $i seconds" + } + "Cost report is ready. Downloading the report..." + $subscriptionID = (($details.Content | ConvertFrom-Json).manifest.requestContext.requestScope) -replace "^/subscriptions/", "" -replace "/$", "" + $blobLink = ($details.Content | ConvertFrom-Json).manifest.blobs.bloblink + $blobContent = Invoke-RestMethod -Uri $blobLink -Method Get + $blobContent | out-file "$subscriptionID.csv" + $csv = Import-Csv -Path "$subscriptionID.csv" + Set-Variable -name costdetails -Value $csv -Scope Script +} + +Function Get-MeterId { + param ( + [Parameter(Mandatory = $true)] [string]$ResourceId, + [Parameter(Mandatory = $true)] [PSCustomObject]$csvObject + ) + $outputArray = @() + #Reset variable to avoid conflicts + Set-Variable -Name 'meterIds' -Value @() -scope script + $resMeterIds = $csvObject | Where-Object { $_.resourceId -eq $ResourceId } | Select-Object meterId -Unique + # For each meterId, get the meterId value and add it to the output array + foreach ($meterId in $resMeterIds) { + $outputArray += $meterId.meterId + } + Set-Variable -Name 'meterIds' -Value $outputArray -Scope Script +} + + # Main script starts here # Turn off breaking change warnings for Azure PowerShell, for Get-AzMetric CmdLet Set-Item -Path Env:\SuppressAzurePowerShellBreakingChangeWarnings -Value $true @@ -204,6 +287,11 @@ Switch ($scopeType) { } $baseQuery = "resources | where subscriptionId == '$subscriptionId'" Get-SingleData -query $baseQuery + If ($includeCost) { + # Generate cost report for the subscription + Invoke-CostReportSchedule -SubscriptionId $subscriptionId + Get-CostReport -PathForResult $pathForResult + } } 'resourceGroup' { # KQL Query to get all resources in a specific resource group and subscription @@ -212,6 +300,11 @@ Switch ($scopeType) { } $baseQuery = "resources | where resourceGroup == '$resourceGroupName' and subscriptionId == '$subscriptionId'" Get-SingleData -query $baseQuery + If ($includeCost) { + # Generate cost report for the subscription + Invoke-CostReportSchedule -SubscriptionId $subscriptionId + Get-CostReport -PathForResult $pathForResult + } } 'multiSubscription' { "multiple subscriptions" @@ -248,6 +341,12 @@ $baseResult | ForEach-Object { resiliencyProperties = $resiliencyProperties dataSizeGB = $dataSize ipAddress = $ipAddress + meterIds = @() + } + If ($includeCost) { + Get-MeterId -ResourceId $resourceId -csvObject $costDetails + # add meterIds to the output object + $outObject.meterIds += $meterIds } $outputArray += $outObject } @@ -256,6 +355,11 @@ $groupedResources = $outputArray | Group-Object -Property ResourceType $summary = @() foreach ($group in $groupedResources) { $resourceType = $group.Name + $uniqueMeterIds = $group.Group | Select-Object -Property meterIds -Unique | Select-Object -ExpandProperty meterIds + $uniqueMeterIds = $uniqueMeterIds | Select-Object -Unique + if ($uniqueMeterIds -isnot [System.Array]) { + $uniqueMeterIds = @($uniqueMeterIds) + } $uniqueLocations = $group.Group | Select-Object -Property ResourceLocation -Unique | Select-Object -ExpandProperty ResourceLocation if ($uniqueLocations -isnot [System.Array]) { $uniqueLocations = @($uniqueLocations) @@ -263,10 +367,10 @@ foreach ($group in $groupedResources) { If ($group.Group.ResourceSku -ne 'N/A') { $uniqueSkus = $group.Group.ResourceSku | Select-Object * -Unique - $summary += [PSCustomObject]@{ResourceCount = $group.Count; ResourceType = $resourceType; ResourceSkus = $uniqueSkus; AzureRegions = $uniqueLocations } + $summary += [PSCustomObject]@{ResourceCount = $group.Count; ResourceType = $resourceType; ResourceSkus = $uniqueSkus; AzureRegions = $uniqueLocations; meterIds = $uniqueMeterIds } } Else { - $summary += [PSCustomObject]@{ResourceCount = $group.Count; ResourceType = $resourceType; ResourceSkus = @("N/A"); AzureRegions = $uniqueLocations } + $summary += [PSCustomObject]@{ResourceCount = $group.Count; ResourceType = $resourceType; ResourceSkus = @("N/A"); AzureRegions = $uniqueLocations; meterIds = $uniqueMeterIds } } } $summary | ConvertTo-Json -Depth 100 | Out-File -FilePath $summaryOutputFile \ No newline at end of file