Public/New-FinOpsCostExport.ps1
# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. <# .SYNOPSIS Creates a new Cost Management export. .DESCRIPTION The New-FinOpsCostExport command creates a new Cost Management export for the specified scope. This command has been tested with the following API versions: - 2023-07-01-preview (default) – Enables FocusCost and other datasets. - 2023-08-01 .PARAMETER Name Required. Name of the export. .PARAMETER Scope Required. Resource ID of the scope to export data for. .PARAMETER Dataset Optional. Dataset to export. Allowed values = "ActualCost", "AmortizedCost", "FocusCost", "PriceSheet", "ReservationDetails", "ReservationTransactions", "ReservationRecommendations". Default = "FocusCost". .PARAMETER DatasetVersion Optional. Schema version of the dataset to export. Default = "1.0" (applies to FocusCost only). .PARAMETER DatasetFilters Optional. Dictionary of key/value pairs to filter the dataset with. Only applies to ReservationRecommendations dataset in 2023-07-01-preview. Valid filters are reservationScope (Shared or Single), resourceType (e.g., VirtualMachines), lookBackPeriod (Last7Days, Last30Days, Last60Days). .PARAMETER Monthly Optional. Indicates that the export should be executed monthly (instead of daily). Default = false. .PARAMETER OneTime Optional. Indicates that the export should only be executed once. When set, the start/end dates are the dates to query data for. Cannot be used in conjunction with the -Monthly option. .PARAMETER StartDate Optional. Day to start running exports. Default = First day of the previous month if -OneTime is set; otherwise, tomorrow (DateTime.Now.AddDays(1)). .PARAMETER EndDate Optional. Last day to run the export. Default = Last day of the month identified in -StartDate if -OneTime is set; otherwise, 5 years from -StartDate. .PARAMETER StorageAccountId Required. Resource ID of the storage account to export data to. .PARAMETER StorageContainer Optional. Name of the container to export data to. Container is created if it doesn't exist. Default = "cost-management". .PARAMETER StoragePath Optional. Path to export data to within the storage container. Default = (scope ID). .PARAMETER DoNotPartition Optional. Indicates whether to partition the exported data into multiple files. Partitioning is recommended for reliability so this option is to disable partitioning. Default = false. .PARAMETER DoNotOverwrite Optional. Indicates whether to overwrite previously exported data for the current month. Overwriting is recommended to keep storage size and costs down so this option is to disable overwriting. If creating an export for FinOps hubs, we recommend you specify the -DoNotOverwrite option to improve troubleshooting. Default = false. .PARAMETER Location Optional. Indicates the Azure location to use for the managed identity used to push data to the storage account. Managed identity is required in order to work with storage accounts behind a firewall but require access to grant permissions (e.g., Owner). If specified, managed identity will be used; otherwise, managed identity will not be used and your export will not be able to push data to a storage account behind a firewall. Default = (empty). .PARAMETER Execute Optional. Indicates that the export should be run immediately after created. .PARAMETER Backfill Optional. Number of months to export the data for. This is only run once at create time. Failed exports are not re-attempted. Not supported when -OneTime is set. Default = 0. .PARAMETER ApiVersion Optional. API version to use when calling the Cost Management Exports API. Default = 2023-07-01-preview. .EXAMPLE New-FinopsCostExport -Name 'July2023OneTime' ` -Scope "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ` -StorageAccountId "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/SharedStorage/providers/Microsoft.Storage/storageAccounts/ddsharedstorage" ` -DataSet ActualCost ` -OneTime ` -StartDate "2023-07-01" ` -EndDate "2023-07-31" Creates a new one time export called 'July2023OneTime from 2023-07-01 to 2023-07-31 with Dataset = Actual and execute it once. .EXAMPLE New-FinopsCostExport -Name 'DailyMTD' ` -Scope "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ` -StorageAccountId "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/SharedStorage/providers/Microsoft.Storage/storageAccounts/ddsharedstorage" ` -DataSet AmortizedCost ` -EndDate "2024-12-31" ` -Execute Creates a new scheduled export called Daily-MTD with StartDate = DateTime.Now and EndDate = 2024-12-31. Export is run immediately after creation. .EXAMPLE New-FinopsCostExport -Name 'Monthly-Report' ` -Scope "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ` -StorageAccountId "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/SharedStorage/providers/Microsoft.Storage/storageAccounts/ddsharedstorage" ` -DataSet AmortizedCost ` -StartDate $(Get-Date).AddDays(5) ` -EndDate "2024-08-15" ` -Monthly ` -Execute Creates a new monthly export called Monthly-Report with StartDate = 1 day from DateTime.Now and EndDate 2024-08-15. Export is run immediately after creation. .EXAMPLE New-FinopsCostExport -Name 'Daily--MTD' ` -Scope "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ` -StorageAccountId "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/SharedStorage/providers/Microsoft.Storage/storageAccounts/ddsharedstorage" ` -DataSet ActualCost ` -StorageContainer "costreports" ` -Backfill 4 ` -Execute Creates a new daily export called Daily-MTD with StartDate = DateTime.Now and EndDate 5 years from StartDate. Additiionally, export cost data for the previous 4 months and save all results in costreports container of the specified storage account. .LINK https://aka.ms/ftk/New-FinOpsCostExport #> function New-FinOpsCostExport { [CmdletBinding(DefaultParameterSetName = "Scheduled")] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $Scope, [Parameter()] [ValidateSet("ActualCost", "AmortizedCost", "FocusCost", "PriceSheet", "ReservationDetails", "ReservationTransactions", "ReservationRecommendations")] [string] $Dataset = "FocusCost", [Parameter()] [string] $DatasetVersion, [Parameter()] [hashtable] $DatasetFilters, [Parameter(ParameterSetName = "Scheduled")] [switch] $Monthly, [Parameter(ParameterSetName = "OneTime")] [switch] $OneTime, [Parameter(ParameterSetName = "OneTime")] [Parameter(ParameterSetName = "Scheduled")] [System.DateTime] $StartDate, [Parameter(ParameterSetName = "OneTime")] [Parameter(ParameterSetName = "Scheduled")] [System.DateTime] $EndDate, [Parameter(Mandatory = $true)] [string] $StorageAccountId, [Parameter()] [string] $StorageContainer = "cost-management", [Parameter()] [string] $StoragePath, [Parameter()] [string] $Location, [Parameter()] [switch] $DoNotPartition, [Parameter()] [switch] $DoNotOverwrite, [Parameter()] [switch] $Execute, [Parameter(ParameterSetName = "Scheduled")] [int32] $Backfill = 0, [Parameter()] [string] $ApiVersion = '2023-07-01-preview' ) function getProperties() { # Set default dates based on schedule type $start = $StartDate $end = $EndDate if (-not $start) { if ($OneTime) { $start = $(Get-Date -Day 1 -Hour 0 -Minute 0 -Second 0 -Millisecond 0).AddMonths(-1) } else { $start = $(Get-Date).AddDays(1) } } if (-not $end) { if ($OneTime) { $end = $start.AddDays($start.Day - 1).AddMonths(1).AddMilliseconds(-1) } else { $end = $start.AddYears(5) } } $timePeriod = @{ from = $start.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") to = $end.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'") } # Default storage path to scope ID if ([System.String]::IsNullOrEmpty($StoragePath)) { $StoragePath = $Scope } $props = @{ properties = @{ definition = @{ type = $Dataset timeframe = "Custom" dataSet = @{ configuration = @{} granularity = "Daily" } } schedule = @{ status = "Inactive" } format = "Csv" deliveryInfo = @{ destination = @{ resourceId = $StorageAccountId container = $StorageContainer.ToLower() rootFolderPath = $StoragePath.Trim('/') # TODO: Add storageAccount + sasToken } } partitionData = (-not $DoNotPartition) } } # Enable managed identity if ($Location) { $props | Add-Member -Name identity -Value @{ type = "SystemAssigned" } -MemberType NoteProperty -Force $props | Add-Member -Name location -Value $Location -MemberType NoteProperty -Force } # Add scheduling-specific settings if ($OneTime) { $props.properties.definition = $props.properties.definition | Add-Member -Name timePeriod -Value $timePeriod -MemberType NoteProperty -Force -PassThru } else { $props.properties.definition.timeframe = "$(if ($Monthly) { 'TheLastMonth' } elseif ($Dataset -eq "PriceSheet") { 'TheCurrentMonth' } else { 'MonthToDate' })" $props.properties.schedule = @{ status = "Active" recurrence = "$(if ($Monthly) { 'Monthly' } elseif ($Dataset -eq "PriceSheet") { 'Daily' } else { 'Daily' })" recurrencePeriod = $timePeriod } } # Add version-specific settings if ($ApiVersion -eq '2023-07-01-preview' -or $ApiVersion.Substring(0, 4) -ge 2024) { # Default dataset versions -- as of July 3, 2024 if (-not $DatasetVersion) { if ($Dataset -eq "FocusCost") { $DatasetVersion = "1.0" } elseif ($Dataset -eq "ActualCost" -or $Dataset -eq "AmortizedCost") { $DatasetVersion = "2021-10-01" } elseif ($Dataset -eq "PriceSheet") { $DatasetVersion = "2023-05-01" } elseif ($Dataset -eq "ReservationDetails") { $DatasetVersion = "2023-03-01" } elseif ($Dataset -eq "ReservationTransactions") { $DatasetVersion = "2023-05-01" } elseif ($Dataset -eq "ReservationRecommendations") { $DatasetVersion = "2023-05-01" } } # Add 2023-07-01-preview settings $props | Add-Member -Name name -Value $Name -MemberType NoteProperty -Force $props.properties = $props.properties | Add-Member -Name exportDescription -Value $Description -MemberType NoteProperty -Force -PassThru $props.properties = $props.properties | Add-Member -Name dataOverwriteBehavior -Value "$(if ($DoNotOverwrite) { "CreateNewReport" } else { "OverwritePreviousReport" })" -MemberType NoteProperty -Force -PassThru $props.properties = $props.properties | Add-Member -Name compressionMode -Value "None" -MemberType NoteProperty -Force -PassThru $props.properties.definition.dataSet.configuration = $props.properties.definition.dataSet.configuration | Add-Member -Name dataVersion -Value $DatasetVersion -MemberType NoteProperty -Force -PassThru $props.properties.deliveryInfo.destination.type = "AzureBlob" # Add dataset filters if ($DatasetFilters.Count -gt 0) { $props.properties.definition.dataSet.configuration = $props.properties.definition.dataSet.configuration | Add-Member -Name filters -Value $DatasetFilters -MemberType NoteProperty -Force -PassThru } } elseif ($Dataset -ne 'ActualCost' -and $Dataset -ne 'AmortizedCost') { $props.properties.definition.type = 'ActualCost' } return $props } # Command details for Invoke-Rest calls $commandDetails = @{ CommandName = "New-FinOpsCostExport" ParameterSetName = $PsCmdlet.ParameterSetName } $context = Get-AzContext if (-not $context) { throw $script:localizedData.Common_ContextNotFound } # Register the Microsoft.CostManagementExports RP if ((Get-AzResourceProvider -ProviderNamespace Microsoft.CostManagementExports).RegistrationState -ne 'Registered') { Write-Verbose "Microsoft.CostManagementExports provider is not registered. Registering provider." Register-AzResourceProvider -ProviderNamespace 'Microsoft.CostManagementExports' } else { Write-Verbose "Provider Microsoft.CostManagementExports is registered" } $properties = getProperties # Check if exists and get etag $uri = "$Scope/providers/Microsoft.CostManagement/exports/$Name`?api-version=$ApiVersion" Write-Verbose "Checking if export $Name exists with path $uri" $export = Get-FinOpsCostExport -Name $Name -Scope $Scope -ApiVersion $ApiVersion if ($export) { Write-Verbose "Export with name $name already exists in scope $scope. Updating export." $etag = $export.etag Write-Verbose "Adding etag to the request for modify request" $properties = $properties | Add-Member -Name eTag -Value $etag -MemberType NoteProperty -Force -PassThru } else { # Create the export using the JSON properties below. Write-Verbose "Creating a new export from $startdateString to $enddateString : $uri" } # Create/update export $createResponse = Invoke-Rest -Method PUT -Uri $uri -Body $properties @commandDetails if ($createResponse.Failure) { Write-Error "Unable to create export $Name in scope $Scope. Error: $($createResponse.Content.error.message) ($($createResponse.Content.error.code))" -ErrorAction Stop return } # Run now if requested if ($Backfill -gt 0 -and $OneTime -eq $false) { Start-FinOpsCostExport -Name $Name -Scope $Scope -Backfill $Backfill } elseif ($Execute -eq $true -or $OneTime -eq $true) { Start-FinOpsCostExport -Name $Name -Scope $Scope } return (Get-FinOpsCostExport -Name $Name -Scope $Scope -ApiVersion $ApiVersion) } |