From 51e4ccab73cacc1f4e25a05e19990cff7c6b74c6 Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:45:09 +0200 Subject: [PATCH 1/8] Initial draft --- .gitignore | 3 + 7-Report/Get-Report.ps1 | 243 ++++++++++++++++++++++++---------------- 2 files changed, 152 insertions(+), 94 deletions(-) diff --git a/.gitignore b/.gitignore index 7d4928b..eae8449 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ 3-CostInformation/*.csv 3-CostInformation/*.xls 3-CostInformation/*.xlsx +7-Report/*.csv +7-Report/*.xls +7-Report/*.xlsx # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/7-Report/Get-Report.ps1 b/7-Report/Get-Report.ps1 index df4a26b..f235ce9 100644 --- a/7-Report/Get-Report.ps1 +++ b/7-Report/Get-Report.ps1 @@ -16,120 +16,175 @@ If specified, exports to .xlsx (requires ImportExcel module), otherwise .csv. #> +## TODO: +# Is available in implementedSkus logic does not seem to be working as expected +# Add logic for cost (separate code)) + param( - [Parameter(Mandatory = $true)] - [string]$InputPath + [Parameter(Mandatory = $false)][array]$availabilityInfoPath, + [Parameter(Mandatory = $false)][string]$costComparisonPath = "..\3-CostInformation\region_comparison_prices.json" ) -# Import data -try { - if ($InputPath.EndsWith(".json")) { - $rawdata = Get-Content $InputPath | ConvertFrom-Json - } elseif ($InputPath.EndsWith(".csv")) { - $rawdata = Import-Csv $InputPath - } else { - throw "Unsupported input format. Please provide a JSON or CSV file." +Function New-Worksheet { + param ( + [string]$WorksheetName, + [int]$LastColumnNumber + ) +$excelParams = @{ + Path = $xlsxFileName + WorksheetName = $WorksheetName + AutoSize = $true + TableStyle = 'None' + PassThru = $true +} + $excelPkg = $reportData | Select-Object -Property $allProps | Export-Excel @excelParams + $ws = $excelPkg.Workbook.Worksheets[$WorksheetName] + $lastColLetter = [OfficeOpenXml.ExcelCellAddress]::GetColumnLetter($lastColumnNumber) + $headerRange = $ws.Cells["A1:$lastColLetter`1"] + $headerRange.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid + $headerRange.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::RoyalBlue) + $headerRange.Style.Font.Color.SetColor([System.Drawing.Color]::White) + for ($row = 2; $row -le ($reportData.Count + 1); $row++) { + # Get the total number of columns in the worksheet + $colCount = $ws.Dimension.Columns + for ($col = 5; $col -le $colCount; $col++) { + # Column 5 is E + $colLetter = [OfficeOpenXml.ExcelCellAddress]::GetColumnLetter($col) + $cell = $ws.Cells["$colLetter$row"] + if ($cell.Value -eq "Available") { + $cell.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid + $cell.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::LightGreen) + } + elseif ($cell.Value -eq "Not available") { + $cell.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid + $cell.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::LightCoral) + } + } } -} catch { - Write-Error "Failed to read input data: $_" - exit 1 + $excelPkg.Save() } -# Initialize an array to collect output +# Consider splitting into functions for better readability and maintainability $reportData = @() - -# Process each item in the JSON -foreach ($item in $rawdata) { - # if implementedSkus is exists and is not null - if ($item.ImplementedSkus -and $item.ImplementedSkus.Count -gt 0) { - $implementedSkus = ($item.ImplementedSkus | ForEach-Object { - # Return the SKU name based on the resource type - if ($item.ResourceType -eq "microsoft.compute/disks") { - $_.name - } elseif ($item.ResourceType -eq "microsoft.compute/virtualmachines") { - $_.vmSize - } elseif ($item.ResourceType -eq "microsoft.keyvault/vaults") { - $_.name + " (Family: " + $_.family + ")" - } elseif ($item.ResourceType -eq "microsoft.network/applicationgateways") { - $_.name + " (Family: " + $_.family + ")" - } elseif ($item.ResourceType -eq "microsoft.network/publicipaddresses") { - $_.name + " (" + $_.tier + ")" - } elseif ($item.ResourceType -eq "microsoft.operationalinsights/workspaces") { - $_.name + " (Last Sku Update: " + $_.lastSkuUpdate + ")" - } elseif ($item.ResourceType -eq "microsoft.recoveryservices/vaults") { - $_.name + " (" + $_.tier + ")" - } elseif ($item.ResourceType -eq "microsoft.sql/servers/databases") { - $_.name + " (Capacity: " + $_.capacity + ")" - } elseif ($item.ResourceType -eq "microsoft.storage/storageaccounts") { - $_.name - } else { - # No action for other resource types +foreach ($path in $availabilityInfoPath) { + $rawdata = Get-Content $path | ConvertFrom-Json -Depth 10 + foreach ($item in $rawdata) { + $implementedSkus = "" + # if implementedSkus is exists and is not null + if ($item.ImplementedSkus -and $item.ImplementedSkus.Count -gt 0) { + $resourceType = $item.ResourceType + ForEach ($sku in $item.ImplementedSkus) { + # Customize output based on ResourceType + switch ($resourceType) { + "microsoft.compute/virtualmachines" { $resourceType; $implementedSkus += $sku.vmSize + "," } + default { + $resourceType; $implementedSkus += $sku.name + "," + } # No action for other resource types + } } - }) -join ", " - } else { - $implementedSkus = "N/A" + } + else { + $implementedSkus += "N/A" + } + $implementedSkus = $implementedSkus.TrimEnd(",") + $regionAvailability = "Not available" + $regionHeader = $item.SelectedRegion.region + If ($item.SelectedRegion.available) { + $regionAvailability = "Available" + } + # If an object with this resource type already exists in reportData, update it + if ($reportData | Where-Object { $_.ResourceType -eq $item.ResourceType }) { + # If it exists, update the existing object with the new region availability + $existingItem = $reportData | Where-Object { $_.ResourceType -eq $item.ResourceType } + $existingItem | Add-Member -MemberType NoteProperty -Name $regionHeader -Value $regionAvailability + } + else { + $reportItem = [PSCustomObject]@{ + ResourceType = $item.ResourceType + ResourceCount = $item.ResourceCount + ImplementedRegions = ($item.ImplementedRegions -join ", ") + ImplementedSkus = $implementedSkus + $regionHeader = $regionAvailability + } + $reportData += $reportItem + } } +} +# $costComparisonPath = "..\3-CostInformation\region_comparison_prices.json" +$rawdata = Get-Content $costComparisonPath | ConvertFrom-Json -Depth 10 +$reportData = @() +$uniqueMeterIds = $rawdata | Select-Object -Property OrigMeterId -Unique +foreach ($meterId in $uniqueMeterIds) { + $meterId = $meterId.OrigMeterId + # get all occurrences of this meterId in $rawdata + $meterOccurrences = $rawdata | Where-Object { $_.OrigMeterId -eq $meterId } + $meterOccurrences + $basedata = $meterOccurrences | Select-Object -Property ServiceName, MeterName, ProductName, SKUName -Unique + $serviceName = $basedata.ServiceName + $meterName = $basedata.MeterName + $productName = $basedata.ProductName + $skuName = $basedata.SKUName + $pricingObj = [PSCustomObject]@{} + foreach ($occurrence in $meterOccurrences) { + $region = $occurrence.Region + if ($region -eq $null -or $region -eq "") { + $region = "Global" + } + "region is $region" - $reportItem = [PSCustomObject]@{ - ResourceType = $item.ResourceType - ResourceCount = $item.ResourceCount - ImplementedRegions = ($item.ImplementedRegions -join ", ") - ImplementedSkus = $implementedSkus - SelectedRegion = $item.SelectedRegion.region - IsAvailable = $item.SelectedRegion.available + $retailPrice = $occurrence.RetailPrice + $priceDiffToOrigin = $occurrence.PriceDiffToOrigin + $pricingObj | Add-Member -MemberType NoteProperty -Name "$region-RetailPrice" -Value $retailPrice + $pricingObj | Add-Member -MemberType NoteProperty -Name "$region-PriceDiffToOrigin" -Value $priceDiffToOrigin } - - $reportData += $reportItem + # Create a new object for each unique meter ID + $costReportItem = [PSCustomObject]@{ + MeterId = $meterId + ServiceName = $serviceName + MeterName = $meterName + ProductName = $productName + SKUName = $skuName + } + Foreach ($key in $pricingObj.PSObject.Properties.Name) { + $costReportItem | Add-Member -MemberType NoteProperty -Name $key -Value $pricingObj.$key + } + + # Add the cost report item to the report data array + $reportData += $costReportItem } -# Define output file name with current timestamp (yyyyMMdd_HHmmss) +#Define output file name with current timestamp (yyyyMMdd_HHmmss) $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $csvFileName = "Availability_Report_$timestamp.csv" $xlsxFileName = "Availability_Report_$timestamp.xlsx" -$excelParams = @{ - Path = $xlsxFileName - WorksheetName = "General" - AutoSize = $true - TableStyle = 'None' - PassThru = $true +# $excelParams = @{ +# Path = $xlsxFileName +# WorksheetName = "General" +# AutoSize = $true +# TableStyle = 'None' +# PassThru = $true +# } + +#$allProps = $reportData | ForEach-Object { $_.PSObject.Properties.Name } | Sort-Object -Unique +# Collect all property names in first-seen order +$allProps = @() +foreach ($obj in $reportData) { + foreach ($p in $obj.PSObject.Properties.Name) { + if ($allProps -notcontains $p) { + $allProps += $p + } + } } # Export to CSV -$reportData | Export-Csv -Path $csvFileName -NoTypeInformation +$reportData | Select-Object -Property $allProps | Export-Csv -Path $csvFileName -NoTypeInformation # Make the Excel first row (header) with blue background and white text -$excelParams = @{ - Path = $xlsxFileName - WorksheetName = "General" - AutoSize = $true - TableStyle = 'None' - PassThru = $true -} -if (Get-Module -ListAvailable -Name ImportExcel) { - $excelPkg = $reportData | Export-Excel @excelParams - $ws = $excelPkg.Workbook.Worksheets["General"] - if ($reportData -and $reportData.Count -gt 0 -and $reportData[0]) { - $lastColLetter = [OfficeOpenXml.ExcelCellAddress]::GetColumnLetter(6) - $headerRange = $ws.Cells["A1:$lastColLetter`1"] - $headerRange.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid - $headerRange.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::RoyalBlue) - $headerRange.Style.Font.Color.SetColor([System.Drawing.Color]::White) - - # Set background color for IsAvailable column based on value - for ($row = 2; $row -le ($reportData.Count + 1); $row++) { - $cell = $ws.Cells["F$row"] - if ($cell.Value -eq $true) { - $cell.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid - $cell.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::LightGreen) - } elseif ($cell.Value -eq $false) { - $cell.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid - $cell.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::LightCoral) - } - } - } - $excelPkg.Save() -} else { - Write-Warning "Excel export skipped. 'ImportExcel' module not found. Install with: Install-Module -Name ImportExcel" -} +$WorksheetName = "Cost" +$lastColumnNumber = $allProps.Count + +New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber + + From d5dc52c9ffde5a119ad6a7740d572da96ea5d16a Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:49:01 +0200 Subject: [PATCH 2/8] updated version with readme --- 7-Report/Get-Report.ps1 | 294 ++++++++++++++++++++-------------------- 7-Report/readme.md | 36 +++-- 2 files changed, 172 insertions(+), 158 deletions(-) diff --git a/7-Report/Get-Report.ps1 b/7-Report/Get-Report.ps1 index f235ce9..3f43d71 100644 --- a/7-Report/Get-Report.ps1 +++ b/7-Report/Get-Report.ps1 @@ -1,42 +1,58 @@ <# .SYNOPSIS - Exports Azure resource availability comparison between regions to Excel or CSV. + Exports Azure resource availability and cost comparison between regions to Excel .DESCRIPTION - Reads the output from Get-AvailabilityInformation.ps1, structures it, and - exports to an Excel or CSV file, including SKU details. + Reads the output from 2-AvailabilityCheck/Get-Region.ps1 and 3-CostInformation/Perform-RegionComparison.ps1, structures it, and + exports to an Excel file, including SKU details. -.PARAMETER InputPath - Path to the JSON or CSV file containing availability information. - -.PARAMETER OutputPath - Path where the report should be saved (without extension). - -.PARAMETER ExportExcel - If specified, exports to .xlsx (requires ImportExcel module), otherwise .csv. +.PARAMETER availabilityInfoPath + Array of paths to JSON files containing availability information. +.PARAMETER costComparisonPath + Path to the JSON file containing cost comparison information. #> -## TODO: -# Is available in implementedSkus logic does not seem to be working as expected -# Add logic for cost (separate code)) - param( [Parameter(Mandatory = $false)][array]$availabilityInfoPath, - [Parameter(Mandatory = $false)][string]$costComparisonPath = "..\3-CostInformation\region_comparison_prices.json" + [Parameter(Mandatory = $false)][string]$costComparisonPath ) +Function Set-ColumnColor { + param( + [Parameter(Mandatory = $true)] [object]$startColumn, + [Parameter(Mandatory = $true)] [string]$cellValPositive, + [Parameter(Mandatory = $true)] [string]$cellValNegative + ) + for ($col = $startColumn; $col -le $colCount; $col++) { + $colLetter = [OfficeOpenXml.ExcelCellAddress]::GetColumnLetter($col) + $cell = $ws.Cells["$colLetter$row"] + if ($cell.Value -eq $cellValPositive) { + $cell.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid + $cell.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::LightGreen) + } + elseif ($cell.Value -eq $cellValNegative) { + $cell.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid + $cell.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::LightCoral) + } + } +} + Function New-Worksheet { param ( - [string]$WorksheetName, - [int]$LastColumnNumber + [Parameter(Mandatory = $true)][string]$WorksheetName, + [Parameter(Mandatory = $true)][int]$LastColumnNumber, + [Parameter(Mandatory = $true)][array]$reportData, + [Parameter(Mandatory = $false)][int]$startColumnNumber, + [Parameter(Mandatory = $false)][string]$cellValPositive, + [Parameter(Mandatory = $false)][string]$cellValNegative ) -$excelParams = @{ - Path = $xlsxFileName - WorksheetName = $WorksheetName - AutoSize = $true - TableStyle = 'None' - PassThru = $true -} + $excelParams = @{ + Path = $xlsxFileName + WorksheetName = $WorksheetName + AutoSize = $true + TableStyle = 'None' + PassThru = $true + } $excelPkg = $reportData | Select-Object -Property $allProps | Export-Excel @excelParams $ws = $excelPkg.Workbook.Worksheets[$WorksheetName] $lastColLetter = [OfficeOpenXml.ExcelCellAddress]::GetColumnLetter($lastColumnNumber) @@ -47,144 +63,126 @@ $excelParams = @{ for ($row = 2; $row -le ($reportData.Count + 1); $row++) { # Get the total number of columns in the worksheet $colCount = $ws.Dimension.Columns - for ($col = 5; $col -le $colCount; $col++) { - # Column 5 is E - $colLetter = [OfficeOpenXml.ExcelCellAddress]::GetColumnLetter($col) - $cell = $ws.Cells["$colLetter$row"] - if ($cell.Value -eq "Available") { - $cell.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid - $cell.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::LightGreen) - } - elseif ($cell.Value -eq "Not available") { - $cell.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid - $cell.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::LightCoral) - } + # Call the function to set column colors based on cell values + If($startColumnNumber) { + Set-ColumnColor -startColumn $startColumnNumber -cellValPositive $cellValPositive -cellValNegative $cellValNegative } } $excelPkg.Save() + "Sheet '$WorksheetName' with $($reportData.Count) entries added to '$xlsxFileName'." } -# Consider splitting into functions for better readability and maintainability -$reportData = @() -foreach ($path in $availabilityInfoPath) { - $rawdata = Get-Content $path | ConvertFrom-Json -Depth 10 - foreach ($item in $rawdata) { - $implementedSkus = "" - # if implementedSkus is exists and is not null - if ($item.ImplementedSkus -and $item.ImplementedSkus.Count -gt 0) { - $resourceType = $item.ResourceType - ForEach ($sku in $item.ImplementedSkus) { - # Customize output based on ResourceType - switch ($resourceType) { - "microsoft.compute/virtualmachines" { $resourceType; $implementedSkus += $sku.vmSize + "," } - default { - $resourceType; $implementedSkus += $sku.name + "," - } # No action for other resource types - } - } - } - else { - $implementedSkus += "N/A" - } - $implementedSkus = $implementedSkus.TrimEnd(",") - $regionAvailability = "Not available" - $regionHeader = $item.SelectedRegion.region - If ($item.SelectedRegion.available) { - $regionAvailability = "Available" - } - # If an object with this resource type already exists in reportData, update it - if ($reportData | Where-Object { $_.ResourceType -eq $item.ResourceType }) { - # If it exists, update the existing object with the new region availability - $existingItem = $reportData | Where-Object { $_.ResourceType -eq $item.ResourceType } - $existingItem | Add-Member -MemberType NoteProperty -Name $regionHeader -Value $regionAvailability - } - else { - $reportItem = [PSCustomObject]@{ - ResourceType = $item.ResourceType - ResourceCount = $item.ResourceCount - ImplementedRegions = ($item.ImplementedRegions -join ", ") - ImplementedSkus = $implementedSkus - $regionHeader = $regionAvailability +# Collect all property names in first-seen order +Function Get-Props { + param ( + [array]$data + ) + $allProps = @() + foreach ($obj in $data) { + foreach ($p in $obj.PSObject.Properties.Name) { + if ($allProps -notcontains $p) { + $allProps += $p } - $reportData += $reportItem } } -} -# $costComparisonPath = "..\3-CostInformation\region_comparison_prices.json" -$rawdata = Get-Content $costComparisonPath | ConvertFrom-Json -Depth 10 -$reportData = @() -$uniqueMeterIds = $rawdata | Select-Object -Property OrigMeterId -Unique -foreach ($meterId in $uniqueMeterIds) { - $meterId = $meterId.OrigMeterId - # get all occurrences of this meterId in $rawdata - $meterOccurrences = $rawdata | Where-Object { $_.OrigMeterId -eq $meterId } - $meterOccurrences - $basedata = $meterOccurrences | Select-Object -Property ServiceName, MeterName, ProductName, SKUName -Unique - $serviceName = $basedata.ServiceName - $meterName = $basedata.MeterName - $productName = $basedata.ProductName - $skuName = $basedata.SKUName - $pricingObj = [PSCustomObject]@{} - foreach ($occurrence in $meterOccurrences) { - $region = $occurrence.Region - if ($region -eq $null -or $region -eq "") { - $region = "Global" - } - "region is $region" - - $retailPrice = $occurrence.RetailPrice - $priceDiffToOrigin = $occurrence.PriceDiffToOrigin - $pricingObj | Add-Member -MemberType NoteProperty -Name "$region-RetailPrice" -Value $retailPrice - $pricingObj | Add-Member -MemberType NoteProperty -Name "$region-PriceDiffToOrigin" -Value $priceDiffToOrigin - } - # Create a new object for each unique meter ID - $costReportItem = [PSCustomObject]@{ - MeterId = $meterId - ServiceName = $serviceName - MeterName = $meterName - ProductName = $productName - SKUName = $skuName - } - Foreach ($key in $pricingObj.PSObject.Properties.Name) { - $costReportItem | Add-Member -MemberType NoteProperty -Name $key -Value $pricingObj.$key - } - - # Add the cost report item to the report data array - $reportData += $costReportItem + return $allProps } #Define output file name with current timestamp (yyyyMMdd_HHmmss) $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$csvFileName = "Availability_Report_$timestamp.csv" $xlsxFileName = "Availability_Report_$timestamp.xlsx" -# $excelParams = @{ -# Path = $xlsxFileName -# WorksheetName = "General" -# AutoSize = $true -# TableStyle = 'None' -# PassThru = $true -# } - -#$allProps = $reportData | ForEach-Object { $_.PSObject.Properties.Name } | Sort-Object -Unique -# Collect all property names in first-seen order -$allProps = @() -foreach ($obj in $reportData) { - foreach ($p in $obj.PSObject.Properties.Name) { - if ($allProps -notcontains $p) { - $allProps += $p +If ($availabilityInfoPath) { + # Consider splitting into functions for better readability and maintainability + $reportData = @() + foreach ($path in $availabilityInfoPath) { + $rawdata = Get-Content $path | ConvertFrom-Json -Depth 10 + foreach ($item in $rawdata) { + $implementedSkus = "" + # if implementedSkus is exists and is not null + if ($item.ImplementedSkus -and $item.ImplementedSkus.Count -gt 0) { + $resourceType = $item.ResourceType + ForEach ($sku in $item.ImplementedSkus) { + # Customize output based on ResourceType + switch ($resourceType) { + "microsoft.compute/virtualmachines" { $implementedSkus += $sku.vmSize + "," } + default { $implementedSkus += $sku.name + "," } + } + } + } + else { + $implementedSkus += "N/A" + } + $implementedSkus = $implementedSkus.TrimEnd(",") + $regionAvailability = "Not available" + $regionHeader = $item.SelectedRegion.region + If ($item.SelectedRegion.available -eq "true") { + $regionAvailability = "Available" + } + # If an object with this resource type already exists in reportData, update it + if ($reportData | Where-Object { $_.ResourceType -eq $item.ResourceType }) { + # If it exists, update the existing object with the new region availability + $existingItem = $reportData | Where-Object { $_.ResourceType -eq $item.ResourceType } + $existingItem | Add-Member -MemberType NoteProperty -Name $regionHeader -Value $regionAvailability + } + else { + $reportItem = [PSCustomObject]@{ + ResourceType = $item.ResourceType + ResourceCount = $item.ResourceCount + ImplementedRegions = ($item.ImplementedRegions -join ", ") + ImplementedSkus = $implementedSkus + $regionHeader = $regionAvailability + } + $reportData += $reportItem + } } } + $WorksheetName = "ServiceAvailability" + $allProps = Get-Props -data $reportData + $lastColumnNumber = $allProps.Count + New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $reportData -startColumnNumber 5 -cellValPositive "Available" -cellValNegative "Not available" } -# Export to CSV -$reportData | Select-Object -Property $allProps | Export-Csv -Path $csvFileName -NoTypeInformation - -# Make the Excel first row (header) with blue background and white text - -$WorksheetName = "Cost" -$lastColumnNumber = $allProps.Count - -New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber - - +If ($costComparisonPath) { + $rawdata = Get-Content $costComparisonPath | ConvertFrom-Json -Depth 10 + $costReportData = @() + $uniqueMeterIds = $rawdata | Select-Object -Property OrigMeterId -Unique + foreach ($meterId in $uniqueMeterIds) { + $meterId = $meterId.OrigMeterId + # get all occurrences of this meterId in $rawdata + $meterOccurrences = $rawdata | Where-Object { $_.OrigMeterId -eq $meterId } + $basedata = $meterOccurrences | Select-Object -Property ServiceName, MeterName, ProductName, SKUName -Unique + $serviceName = $basedata.ServiceName + $meterName = $basedata.MeterName + $productName = $basedata.ProductName + $skuName = $basedata.SKUName + $pricingObj = [PSCustomObject]@{} + foreach ($occurrence in $meterOccurrences) { + $region = $occurrence.Region + if ($null -eq $region -or $region -eq "") { + $region = "Global" + } + $retailPrice = $occurrence.RetailPrice + $priceDiffToOrigin = $occurrence.PriceDiffToOrigin + $pricingObj | Add-Member -MemberType NoteProperty -Name "$region-RetailPrice" -Value $retailPrice + $pricingObj | Add-Member -MemberType NoteProperty -Name "$region-PriceDiffToOrigin" -Value $priceDiffToOrigin + } + # Create a new object for each unique meter ID + $costReportItem = [PSCustomObject]@{ + MeterId = $meterId + ServiceName = $serviceName + MeterName = $meterName + ProductName = $productName + SKUName = $skuName + } + Foreach ($key in $pricingObj.PSObject.Properties.Name) { + $costReportItem | Add-Member -MemberType NoteProperty -Name $key -Value $pricingObj.$key + } + # Add the cost report item to the report data array + $costReportData += $costReportItem + } + $WorksheetName = "CostComparison" + $allProps = Get-Props -data $costReportData + $lastColumnNumber = $allProps.Count + New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $costReportData +} diff --git a/7-Report/readme.md b/7-Report/readme.md index b8f86fe..8b92400 100644 --- a/7-Report/readme.md +++ b/7-Report/readme.md @@ -1,24 +1,40 @@ # Export Script -This script generates formatted Excel (`.xlsx`) and CSV reports based on the output from the previous check script. The reports provide detailed information for each service, including: +This script generates formatted Excel (`.xlsx`)reports based on the output from the previous check script. The reports provide detailed information for each service, including: + +## Service Availability Report - **Resource type** - **Resource count** - **Implemented (origin) regions** - **Implemented SKUs** -- **Selected (target) region** -- **Availability in the selected region** +- **Selected (target) regions** +- **Availability in the selected regions** + +## Cost Comparison Report + +- **Azure Cost Meter ID** +- **Service Name** +- **Meter Name** +- **Product Name** +- **SKU Name** +- **Retail Price per region** +- **Price Difference to origin region per region** -These reports help you analyze service compatibility across different regions. +These reports help you analyze service compatibility and cost differences across different regions. -## Usage +## Dependencies + +- This script requires the `ImportExcel` PowerShell module. +- The script requires you to have run either the `2-AvailabilityCheck/Get-Region.ps1` or `3-CostInformation/Perform-RegionComparison.ps1` or both scripts to generate the necessary JSON input files for availability and cost data. + +## Usage Instructions 1. Open a PowerShell command line. -2. Navigate to the `7-Export` folder. -3. Run the script: +2. Navigate to the `7-Report` folder. +3. If you have created one or more availability JSON files using the `2-AvailabilityCheck/Get-Region.ps1` script, run the following commands, replacing the path with your actual file path(s): ```powershell - .\Get-Report.ps1 -InputPath "..\2-AvailabilityCheck\Availability_Mapping_Asia_Pacific.json" + .\Get-Report.ps1 -availabilityInfoPath `@("..\2-AvailabilityCheck\Availability_Mapping_Asia_Pacific.json", "..\2-AvailabilityCheck\Availability_Mapping_Europe.json")` -costComparisonPath "..\3-CostInformation\region_comparison_prices.json" ``` - -The script generates `.xlsx` and `.csv` files in the `7-Export` folder, named `Availability_Report_CURRENTTIMESTAMP`. \ No newline at end of file +The script generates an `.xlsx` and `.csv` files in the `7-report` folder, named `Availability_Report_CURRENTTIMESTAMP`. \ No newline at end of file From 085f620e5be810efb3f10d2a9b3a53e364d828e4 Mon Sep 17 00:00:00 2001 From: Predrag Jelesijevic <5805065+prjelesi@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:37:51 +0200 Subject: [PATCH 3/8] Fix formatting in readme.md dependencies section --- 7-Report/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/7-Report/readme.md b/7-Report/readme.md index 8b92400..fe764c2 100644 --- a/7-Report/readme.md +++ b/7-Report/readme.md @@ -25,7 +25,7 @@ These reports help you analyze service compatibility and cost differences across ## Dependencies -- This script requires the `ImportExcel` PowerShell module. +- This script requires the `ImportExcel` PowerShell module. - The script requires you to have run either the `2-AvailabilityCheck/Get-Region.ps1` or `3-CostInformation/Perform-RegionComparison.ps1` or both scripts to generate the necessary JSON input files for availability and cost data. ## Usage Instructions @@ -37,4 +37,4 @@ These reports help you analyze service compatibility and cost differences across ```powershell .\Get-Report.ps1 -availabilityInfoPath `@("..\2-AvailabilityCheck\Availability_Mapping_Asia_Pacific.json", "..\2-AvailabilityCheck\Availability_Mapping_Europe.json")` -costComparisonPath "..\3-CostInformation\region_comparison_prices.json" ``` -The script generates an `.xlsx` and `.csv` files in the `7-report` folder, named `Availability_Report_CURRENTTIMESTAMP`. \ No newline at end of file +The script generates an `.xlsx` and `.csv` files in the `7-report` folder, named `Availability_Report_CURRENTTIMESTAMP`. From c03e3a3382e661a2ee61c1d99003315fcedc2cf2 Mon Sep 17 00:00:00 2001 From: Predrag Jelesijevic <5805065+prjelesi@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:43:52 +0200 Subject: [PATCH 4/8] Fix formatting issues in Get-Report.ps1 --- 7-Report/Get-Report.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/7-Report/Get-Report.ps1 b/7-Report/Get-Report.ps1 index 3f43d71..1a4647c 100644 --- a/7-Report/Get-Report.ps1 +++ b/7-Report/Get-Report.ps1 @@ -62,7 +62,7 @@ Function New-Worksheet { $headerRange.Style.Font.Color.SetColor([System.Drawing.Color]::White) for ($row = 2; $row -le ($reportData.Count + 1); $row++) { # Get the total number of columns in the worksheet - $colCount = $ws.Dimension.Columns + # $colCount = $ws.Dimension.Columns # Call the function to set column colors based on cell values If($startColumnNumber) { Set-ColumnColor -startColumn $startColumnNumber -cellValPositive $cellValPositive -cellValNegative $cellValNegative @@ -174,7 +174,7 @@ If ($costComparisonPath) { MeterName = $meterName ProductName = $productName SKUName = $skuName - } + } Foreach ($key in $pricingObj.PSObject.Properties.Name) { $costReportItem | Add-Member -MemberType NoteProperty -Name $key -Value $pricingObj.$key } From 223083bdc3b6ab8d312cc75c1b57167473f0cbb7 Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:38:55 +0200 Subject: [PATCH 5/8] update --- 7-Report/Get-Report.ps1 | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/7-Report/Get-Report.ps1 b/7-Report/Get-Report.ps1 index 1a4647c..19cd8f2 100644 --- a/7-Report/Get-Report.ps1 +++ b/7-Report/Get-Report.ps1 @@ -18,11 +18,13 @@ param( ) Function Set-ColumnColor { + [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [object]$startColumn, [Parameter(Mandatory = $true)] [string]$cellValPositive, [Parameter(Mandatory = $true)] [string]$cellValNegative ) + $colCount = $ws.Dimension.End.Column for ($col = $startColumn; $col -le $colCount; $col++) { $colLetter = [OfficeOpenXml.ExcelCellAddress]::GetColumnLetter($col) $cell = $ws.Cells["$colLetter$row"] @@ -38,6 +40,7 @@ Function Set-ColumnColor { } Function New-Worksheet { + [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)][string]$WorksheetName, [Parameter(Mandatory = $true)][int]$LastColumnNumber, @@ -61,11 +64,9 @@ Function New-Worksheet { $headerRange.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::RoyalBlue) $headerRange.Style.Font.Color.SetColor([System.Drawing.Color]::White) for ($row = 2; $row -le ($reportData.Count + 1); $row++) { - # Get the total number of columns in the worksheet - # $colCount = $ws.Dimension.Columns # Call the function to set column colors based on cell values If($startColumnNumber) { - Set-ColumnColor -startColumn $startColumnNumber -cellValPositive $cellValPositive -cellValNegative $cellValNegative + Set-ColumnColor -startColumn $startColumnNumber -cellValPositive $cellValPositive -cellValNegative $cellValNegative -Confirm:$false } } $excelPkg.Save() @@ -73,7 +74,7 @@ Function New-Worksheet { } # Collect all property names in first-seen order -Function Get-Props { +Function Get-PropertyArray { param ( [array]$data ) @@ -138,9 +139,9 @@ If ($availabilityInfoPath) { } } $WorksheetName = "ServiceAvailability" - $allProps = Get-Props -data $reportData + $allProps = Get-PropertyArray -data $reportData $lastColumnNumber = $allProps.Count - New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $reportData -startColumnNumber 5 -cellValPositive "Available" -cellValNegative "Not available" + New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $reportData -startColumnNumber 5 -cellValPositive "Available" -cellValNegative "Not available" -Confirm:$false } If ($costComparisonPath) { @@ -182,7 +183,7 @@ If ($costComparisonPath) { $costReportData += $costReportItem } $WorksheetName = "CostComparison" - $allProps = Get-Props -data $costReportData + $allProps = Get-PropertyArray -data $costReportData $lastColumnNumber = $allProps.Count - New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $costReportData + New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $costReportData -Confirm:$false } From 0f0c60730ce9eaa295480dcd0bacc17f18d3b235 Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:57:47 +0200 Subject: [PATCH 6/8] linter config --- .github/linters/PSScriptAnalyzerSettings.psd1 | 9 +++++++++ .github/workflows/code-review.yml | 1 + 7-Report/Get-Report.ps1 | 14 ++++++-------- 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 .github/linters/PSScriptAnalyzerSettings.psd1 diff --git a/.github/linters/PSScriptAnalyzerSettings.psd1 b/.github/linters/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..15bf1f5 --- /dev/null +++ b/.github/linters/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,9 @@ +@{ + Rules = @{ + # Disable specific rules by name + ExcludeRules = @( + 'PSUseShouldProcessForStateChangingFunctions', + 'PSUseSingularNouns' + ) + } +} \ No newline at end of file diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index d898f7c..bc36d8b 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -32,6 +32,7 @@ jobs: VALIDATE_MARKDOWN: true VALIDATE_POWERSHELL: true VALIDATE_YAML: true + POWERSHELL_CONFIG_FILE: .github/linters/PSScriptAnalyzerSettings.psd1 #YAMLLINT_CONFIG_FILE: .github/linters/.yamllint.yml #VALIDATE_EDITORCONFIG: true # Disable errors to only generate a report diff --git a/7-Report/Get-Report.ps1 b/7-Report/Get-Report.ps1 index 19cd8f2..24ad333 100644 --- a/7-Report/Get-Report.ps1 +++ b/7-Report/Get-Report.ps1 @@ -18,7 +18,6 @@ param( ) Function Set-ColumnColor { - [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [object]$startColumn, [Parameter(Mandatory = $true)] [string]$cellValPositive, @@ -40,7 +39,6 @@ Function Set-ColumnColor { } Function New-Worksheet { - [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)][string]$WorksheetName, [Parameter(Mandatory = $true)][int]$LastColumnNumber, @@ -66,7 +64,7 @@ Function New-Worksheet { for ($row = 2; $row -le ($reportData.Count + 1); $row++) { # Call the function to set column colors based on cell values If($startColumnNumber) { - Set-ColumnColor -startColumn $startColumnNumber -cellValPositive $cellValPositive -cellValNegative $cellValNegative -Confirm:$false + Set-ColumnColor -startColumn $startColumnNumber -cellValPositive $cellValPositive -cellValNegative $cellValNegative } } $excelPkg.Save() @@ -74,7 +72,7 @@ Function New-Worksheet { } # Collect all property names in first-seen order -Function Get-PropertyArray { +Function Get-Props { param ( [array]$data ) @@ -139,9 +137,9 @@ If ($availabilityInfoPath) { } } $WorksheetName = "ServiceAvailability" - $allProps = Get-PropertyArray -data $reportData + $allProps = Get-Props -data $reportData $lastColumnNumber = $allProps.Count - New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $reportData -startColumnNumber 5 -cellValPositive "Available" -cellValNegative "Not available" -Confirm:$false + New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $reportData -startColumnNumber 5 -cellValPositive "Available" -cellValNegative "Not available" } If ($costComparisonPath) { @@ -183,7 +181,7 @@ If ($costComparisonPath) { $costReportData += $costReportItem } $WorksheetName = "CostComparison" - $allProps = Get-PropertyArray -data $costReportData + $allProps = Get-Props -data $costReportData $lastColumnNumber = $allProps.Count - New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $costReportData -Confirm:$false + New-Worksheet -WorksheetName $WorksheetName -LastColumnNumber $lastColumnNumber -reportData $costReportData } From 6d0a63fb057a296c214134243eaddcaccff9c3f1 Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:01:49 +0200 Subject: [PATCH 7/8] path --- .github/workflows/code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index bc36d8b..20c8534 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -32,7 +32,7 @@ jobs: VALIDATE_MARKDOWN: true VALIDATE_POWERSHELL: true VALIDATE_YAML: true - POWERSHELL_CONFIG_FILE: .github/linters/PSScriptAnalyzerSettings.psd1 + POWERSHELL_CONFIG_FILE: PSScriptAnalyzerSettings.psd1 #YAMLLINT_CONFIG_FILE: .github/linters/.yamllint.yml #VALIDATE_EDITORCONFIG: true # Disable errors to only generate a report From ba78b5d01650f71d49aabdf9aab0e64ba8a80ebb Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:57:15 +0200 Subject: [PATCH 8/8] syntax --- .github/linters/PSScriptAnalyzerSettings.psd1 | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/linters/PSScriptAnalyzerSettings.psd1 b/.github/linters/PSScriptAnalyzerSettings.psd1 index 15bf1f5..a579773 100644 --- a/.github/linters/PSScriptAnalyzerSettings.psd1 +++ b/.github/linters/PSScriptAnalyzerSettings.psd1 @@ -1,9 +1,8 @@ @{ - Rules = @{ - # Disable specific rules by name - ExcludeRules = @( - 'PSUseShouldProcessForStateChangingFunctions', - 'PSUseSingularNouns' - ) - } + + # Disable specific rules by name + ExcludeRules = @( + 'PSUseShouldProcessForStateChangingFunctions', + 'PSUseSingularNouns' + ) } \ No newline at end of file