diff --git a/.github/linters/PSScriptAnalyzerSettings.psd1 b/.github/linters/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..a579773 --- /dev/null +++ b/.github/linters/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,8 @@ +@{ + + # 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..20c8534 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: PSScriptAnalyzerSettings.psd1 #YAMLLINT_CONFIG_FILE: .github/linters/.yamllint.yml #VALIDATE_EDITORCONFIG: true # Disable errors to only generate a report 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..24ad333 100644 --- a/7-Report/Get-Report.ps1 +++ b/7-Report/Get-Report.ps1 @@ -1,135 +1,187 @@ <# .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. #> param( - [Parameter(Mandatory = $true)] - [string]$InputPath + [Parameter(Mandatory = $false)][array]$availabilityInfoPath, + [Parameter(Mandatory = $false)][string]$costComparisonPath ) -# 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 Set-ColumnColor { + 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"] + 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) + } } -} catch { - Write-Error "Failed to read input data: $_" - exit 1 } -# Initialize an array to collect output -$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 - } - }) -join ", " - } else { - $implementedSkus = "N/A" +Function New-Worksheet { + param ( + [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 } - - $reportItem = [PSCustomObject]@{ - ResourceType = $item.ResourceType - ResourceCount = $item.ResourceCount - ImplementedRegions = ($item.ImplementedRegions -join ", ") - ImplementedSkus = $implementedSkus - SelectedRegion = $item.SelectedRegion.region - IsAvailable = $item.SelectedRegion.available + $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++) { + # 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'." +} - $reportData += $reportItem +# 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 + } + } + } + return $allProps } -# 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 -} - -# Export to CSV -$reportData | 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 ($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" } -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) +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 } - $excelPkg.Save() -} else { - Write-Warning "Excel export skipped. 'ImportExcel' module not found. Install with: Install-Module -Name ImportExcel" + $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..fe764c2 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`.