CapacityManagement/DashboardGenerator/Create-AzSStorageDashboard.ps1 (483 lines of code) (raw):

param( [CmdletBinding(DefaultParameterSetName="relativeTime")] [Parameter(Position = 1, Mandatory = $false)] [string]$adminSubscriptionName = "Default Provider Subscription" , [Parameter(Position = 2, Mandatory = $false)] [string]$jsonTemplateLocation = ".\templateJson" , [Parameter(Mandatory = $false)] [System.Object]$DefaultProfile, [Parameter(ParameterSetName="relativeTime")] [ValidateSet('PT30M', 'PT4H', 'PT12H', 'P1D', 'P2D', 'P3D', 'P7D', 'P30D')] [string]$duration = 'P1D', [Parameter(Mandatory=$True, ParameterSetName="absoluteTime", HelpMessage="Please enter the start time of time range you want to see.")] [datetime]$startTime, [Parameter(Mandatory=$True, ParameterSetName="absoluteTime", HelpMessage="Please enter the end time of time range you want to see.")] [datetime]$endTime, [Parameter(Mandatory = $false)] [ValidateSet('Automatic', 'PT1M', 'PT1H', 'P1D', 'PT5M', 'PT15M', 'PT30M', 'PT6H', 'PT12H')] [string]$timeGrain = 'Automatic', [Parameter(Mandatory = $false)] [string]$outputLocation = '.', [Parameter(Mandatory = $false)] [Boolean]$capacityOnly = $false, [Parameter(Mandatory = $false)] [ValidateSet('all', 'object', 'infrastructure', 'vmtemp')] [string]$volumeType = 'all' ) <# .SYNOPSIS Generate json used in Azure Stack portal to show volumes performances. .Description This function is used to generate dashboard jsons, which can be uploaded on Azure Stack Dashboard to create related dashboards. You can use this function without parameters to create jsons, showing last 1 day metrics, at the current folder. To set time range, you can define duration or pair of startTime and endTime. .Inputs Charts' Time range and granularity settings. Json output location setting. .Outputs Three jsons represent count, latency and throughput performance respectively. .Parameter timeGrain The timespan defines the time granularity in charts, in ISO 8601 duration format. .Parameter outputLocation The location to expose generated jsons. .Parameter duration The timespan defines the time range of volume metrics, in ISO 8601 duration format. .Parameter startTime The start time of time range shown on dashboard. .Parameter endTime The start time of time range shown on dashboard. .Example # default json save to spedified location Save-AzureStackVolumesPerformanceDashboardJson -outputLocation 'D:\dashboardJsons' .Example # data of last day with 15min interval Save-AzureStackVolumesPerformanceDashboardJson -duration "P1D" -timeGrain "PT15M" .Example # date from 4/1 to 4/8 with 1hr interval Save-AzureStackVolumesPerformanceDashboardJson -startTime (Get-date("2019-04-01")) -endTime (Get-date("2019-04-08")) -timeGrain "PT1H" .Notes Author: Azure Stack Azure Monitor Team .Link Source Code: https://github.com/GTMer/AzureStack-VolumesPerformanceDashboard-Generator #> function Save-AzureStackVolumesPerformanceDashboardJson { [CmdletBinding(DefaultParameterSetName="relativeTime")] param ( [Parameter(Mandatory = $false)] [System.Object]$DefaultProfile, [Parameter(ParameterSetName="relativeTime")] [ValidateSet('PT30M', 'PT4H', 'PT12H', 'P1D', 'P2D', 'P3D', 'P7D', 'P30D')] [string]$duration = 'P1D', [Parameter(Mandatory=$True, ParameterSetName="absoluteTime", HelpMessage="Please enter the start time of time range you want to see.")] [datetime]$startTime, [Parameter(Mandatory=$True, ParameterSetName="absoluteTime", HelpMessage="Please enter the end time of time range you want to see.")] [datetime]$endTime, [Parameter(Mandatory = $false)] [ValidateSet('Automatic', 'PT1M', 'PT1H', 'P1D', 'PT5M', 'PT15M', 'PT30M', 'PT6H', 'PT12H')] [string]$timeGrain = 'Automatic', [Parameter(Mandatory = $false)] [string]$outputLocation = '.' ) # check if AzureRM Module is present $AzureRMModule = Get-command -Name Get-AzureRmContext -ErrorAction SilentlyContinue if($AzureRMModule){ # do nothing, cmdlets should work natively. } else { # check if Az Module is present $AzModule = Get-command -Name Get-AzContext -ErrorAction SilentlyContinue if($AzModule){ $answer = Read-Host "`n'Az' module detected, script requires 'Enable-AzureRmAlias' to be enabled for this session.`n`nType 'Y' to Enable or Press Enter for Exit`n" switch($answer){ Y { try { Write-Host "`nExecuting 'Enable-AzureRmAlias -Scope Local' to created Aliases, this will not persist outside of this session.`n" Enable-AzureRmAlias -Scope Local } catch { Write-Host -ForegroundColor Red "Error: $($error[0].Exception.Message)" throw "Error executing 'Enable-AzureRmAlias'" } } $null { Write-Host "Script exiting..." Return } default { Write-Host "Script exiting..." Return } } } else { throw "Error: This script requires either the 'AzureRM' or 'Az' PowerShell Module to be installed." } } # If user do not input DefaultProfile if ($null -eq $DefaultProfile) { $script:context = Get-AzureRmContext } else { $script:context = $DefaultProfile.Context } # if user hadn't added and login to AzureRmEnvironment, exit if ($null -eq $script:context.Account) { throw "Please login in AzureRm or Az account first." } if ((Test-Path -Path $outputLocation) -eq $false) { throw "Output location not exist." } # $resourceId = Get-AzureStackResourceId $volumes = Get-AzureStackVolumes if ($timeGrain -eq 'Automatic') { if ($PSCmdlet.ParameterSetName -eq "absoluteTime") { $timeRange = $endTime - $startTime } else { $timeRange = [System.Xml.XmlConvert]::ToTimeSpan($duration) } if ($timeRange -le (New-TimeSpan -Hours 4)) { $timeGrain = "PT1M" } if ($timeRange -lt (New-TimeSpan -Days 1)) { $timeGrain = "PT5M" } elseif ($timeRange -le (New-TimeSpan -Days 1)) { $timeGrain = "PT15M" } elseif ($timeRange -le (New-TimeSpan -Days 3)) { $timeGrain = "PT30M" } elseif ($timeRange -le (New-TimeSpan -Days 7)) { $timeGrain = "PT1H" } else { $timeGrain = "PT6H" } } $description = "timeGrain: $timeGrain; `n" if ($PSCmdlet.ParameterSetName -eq "absoluteTime") { if ($startTime -gt $endTime) { throw ("StartTime should less than EndTime!") } if ($startTime -gt $(Get-Date)) { throw ("StartTime should less than Now!") } $description += "startTime: $($startTime.ToString('o')); `nendTime: $($endTime.ToString('o')); `n" $volumeTypes | ForEach-Object { Get-DashboardVolumesJson -volumeType $_ -startTime $startTime.ToString('o') -endTime $endTime.ToString('o') -timeGrain $timeGrain -description $description -volumes $volumes | ConvertTo-Json -Depth 100 | Format-Json > $($outputLocation.TrimEnd('\') + '\' + "DashboardVolume" + $_ + "_customTime.json") Write-Host "$($outputLocation.TrimEnd('\') + '\' + "DashboardVolume" + $_ + "_customTime.json") finished." } } else { $description += "duration: $duration; `n" $durationTotalMilliseconds = ([System.Xml.XmlConvert]::ToTimeSpan($duration)).TotalMilliseconds $volumeTypes | ForEach-Object { Get-DashboardVolumesJson -duration $durationTotalMilliseconds -timeGrain $timeGrain -description $description -volumes $volumes -volumeType $_ | ConvertTo-Json -Depth 100 | Format-Json > $($outputLocation.TrimEnd('\') + '\' + "DashboardVolume" + $_ + "_" + $duration + ".json") Write-Host "$($outputLocation.TrimEnd('\') + '\' + "DashboardVolume" + $_ + "_" + $duration + ".json") finished." } } } function Get-AzureStackResourceId { [CmdletBinding()] param ( ) try { Write-Host "Getting resource Id from AzureRmSubscription." $adminSubscription = $script:context.Subscription $location = Get-AzureRmLocation -DefaultProfile $script:context -ErrorAction Stop -Verbose } catch { Write-Error $_ throw "Please login in AzureRm or Az account. If still happens, check your environment settings in psm1 file." } "subscriptions/$($adminSubscription.Id)/resourceGroups/System.$($location.location)/providers/Microsoft.Fabric.Admin/fabricLocations/$($location.location)" } function Get-AzureStackVolumes { [CmdletBinding()] param ( ) try { Write-Host "Getting volumes data from ARM." $location = Get-AzureRmLocation -DefaultProfile $script:context -ErrorAction Stop -Verbose $location = $location.Location $scaleUnits = Get-AzureRmResource -DefaultProfile $script:context -ResourceName $location -ResourceType Microsoft.Fabric.Admin/fabricLocations/scaleunits -ResourceGroupName "System.$($location)" -ApiVersion "2016-05-01" $sotrageSubSystems = Get-AzureRmResource -DefaultProfile $script:context -ResourceName $scaleUnits.Name -ResourceType Microsoft.Fabric.Admin/fabricLocations/scaleunits/storageSubSystems -ResourceGroupName "System.$($location)" -ApiVersion "2018-10-01" $volumes = Get-AzureRmResource -DefaultProfile $script:context -ResourceName $sotrageSubSystems.Name -ResourceType Microsoft.Fabric.Admin/fabricLocations/scaleunits/storageSubSystems/volumes -ResourceGroupName "System.$($location)" -ApiVersion "2018-10-01" } catch { Write-Error $_.ToString() Write-Error "Cannot fetch data from ARM." } $volumes } <# .SYNOPSIS Transform volumes into dictionary of list of tuple to show volume name namemaps, eg: $volumesByType["ObjStore"][2] = Tupe["Volume11", "Obj_store2"] . #> function Get-volumesByType { [CmdletBinding()] param ( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [array]$volumes = ( Get-AzureStackVolumes ) ) Write-Host "Analyzing volumes data." if ($volumes.Count -eq 0) { return } $volumesByType = @{} $volumeTypes | ForEach-Object { $volumesByType.$_ = New-Object 'Collections.Generic.List[Tuple[String,String]]' } $volumes | ForEach-Object { $labelPrefix = [regex]::match($_.properties.volumeLabel, '(.*)_.*').Groups[1].Value if ($volumeTypes.Contains($labelPrefix)) { $volumeLocalName = [regex]::match($_.properties.volumeLocalName, '.*\/(.*)').Groups[1].Value $volumesByType.$labelPrefix.add([Tuple]::Create($volumeLocalName, $_.properties.volumeLabel)) } } $volumeTypes | ForEach-Object { $volumesByType.$_ = $volumesByType.$_ | Sort-Object Item2 } $volumesByType } <# .SYNOPSIS Transform template json into PsCustomObject. #> function Initialize-TilePsCustomObject { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet("Throughput", "Count", "Latency", "Capacity")] [string]$metricType, [Parameter(ParameterSetName="relativeTime")] [Double]$duration = 86400000, [Parameter(ParameterSetName="absoluteTime")] [string]$startTime, [Parameter(ParameterSetName="absoluteTime")] [string]$endTime, [Parameter(Mandatory = $false)] [string]$timeGrain = "PT15M", [Parameter(Mandatory = $true)] [string]$resourceId ) if ($metricType -eq "Capacity") { $tileTemplate = $Script:capacityTemplate.Replace("<resourceIdToBeReplaced>", '/' + $resourceId) | ConvertFrom-Json } else { if ($capacityOnly -eq $false) { $tileTemplate = $Script:tileTemplate.Replace("<resourceIdToBeReplaced>", '/' + $resourceId) | ConvertFrom-Json } } # set tile size $tileTemplate.position.colSpan = $tileColSpan $tileTemplate.position.rowSpan = $tileRowSpan $templateChart = $tileTemplate.metadata.inputs[0].value.charts[0] # set time range $templateChart.timeContext.psobject.properties.remove("relative") $templateChart.timeContext.psobject.properties.remove("absolute") if ($PSCmdlet.ParameterSetName -eq "relativeTime") { $templateChart.timeContext | Add-Member -MemberType NoteProperty -Name "relative" -Value ([psCustomObject]@{'duration'=$duration}) } else { $templateChart.timeContext | Add-Member -MemberType NoteProperty -Name "absolute" -Value ([psCustomObject]@{'startTime'=$startTime; 'endTime'=$endTime}) } # set time granularity $chartGrainMap = @{'Automatic' = 1; 'PT1M' = 2; 'PT1H' = 3; 'P1D' = 4; 'PT5M' = 7; 'PT15M' = 8; 'PT30M' = 9; 'PT6H' = 10; 'PT12H' = 11} $templateChart.itemDataModel.appliedISOGrain = $templateChart.timeContext.options.appliedISOGrain = $timeGrain $templateChart.timeContext.options.grain = $chartGrainMap.$timeGrain $tileTemplate } <# .SYNOPSIS Get individual tile PsCustomObjects. #> function Get-TilePsCustomObject { param ( [Parameter(Mandatory = $true)] [psCustomObject]$tileTemplate, [Parameter(Mandatory = $true)] [ValidateSet("Throughput", "Count", "Latency", "Capacity")] [string]$metricType, [Parameter(Mandatory = $true)] [string]$tileName, [Parameter(Mandatory = $true)] [string]$filterVolumeName, [Parameter(Mandatory = $true)] [int]$positionX, [Parameter(Mandatory = $true)] [int]$positionY ) # deep copy $tileTemplate = $tileTemplate | ConvertTo-Json -Depth 100 | ConvertFrom-Json $templateChart = $tileTemplate.metadata.inputs[0].value.charts[0] #change aggregation type $aggregationTypeofMetric = @{'Throughput' = 'Sum'; 'Count' = 'Sum'; 'Latency' = 'Avg'; 'Capacity' = 'Avg'} $aggregationType = $aggregationTypeofMetric.$metricType if ($aggregationType -eq "Sum") { $templateChart.metrics | ForEach-Object { $_.aggregationType = 4 } $templateChart.itemDataModel.metrics | ForEach-Object {$_.metricAggregation = 1 } } else { $templateChart.metrics | ForEach-Object { $_.aggregationType = 1 } $templateChart.itemDataModel.metrics | ForEach-Object {$_.metricAggregation = 4 } } switch ($metricType) { Capacity { } Default { #change metrics name $templateChart.metrics[0].name = $templateChart.itemDataModel.metrics[0].id.name.id = "VolumeOperations" + $( if ($metricType -eq "Count") {""} else {$metricType} ) + "Read" $templateChart.metrics[1].name = $templateChart.itemDataModel.metrics[1].id.name.id = "VolumeOperations" + $( if ($metricType -eq "Count") {""} else {$metricType} ) + "Write" $templateChart.itemDataModel.metrics[0].id.name.displayName = $metricType + "Read" $templateChart.itemDataModel.metrics[1].id.name.displayName = $metricType + "Write" } } #change tile name $templateChart.title = $templateChart.itemDataModel.title = $tileName #change filter $templateChart.itemDataModel.filters.OperandFilters[0].OperandSelectedValues[0] = $filterVolumeName # change position $tileTemplate.position.x = $positionX $tileTemplate.position.Y = $positionY $tileTemplate } <# .SYNOPSIS Get dashboard json in PsCustomObject format. #> function Get-DashboardVolumesJson { [CmdletBinding(DefaultParameterSetName="relativeTime")] param ( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [array]$volumes = ( Get-AzureStackVolumes ), [Parameter(Mandatory = $false)] [ValidateSet("ObjStore", "Infrastructure", "VmTemp")] [string]$volumeType = "ObjStore", [Parameter(ParameterSetName="relativeTime")] [Double]$duration = 86400000, [Parameter(ParameterSetName="absoluteTime")] [string]$startTime, [Parameter(ParameterSetName="absoluteTime")] [string]$endTime, [Parameter(Mandatory = $false)] [string]$timeGrain = "PT15M", [Parameter(Mandatory = $false)] [string]$description = "" ) if ($volumes.Count -eq 0) { return } $volumesByType = Get-volumesByType -volumes $volumes Write-Host "Generating dashboard json." $resourceId = ( Get-AzureStackResourceId ) $dashboardBody = $Script:dashboardBody.Replace("<resourceIdToBeReplaced>", '/' + $resourceId) | ConvertFrom-Json # change dashboard title/name $dashboardBody.name = $dashboardBody.tags."hidden-title" = $volumeType + " Volumes Operation Performance" $Templates = @{} $metricTypes | ForEach-Object { if ($PSCmdlet.ParameterSetName -eq "relativeTime") { $Templates[$_] = Initialize-TilePsCustomObject -metricType $_ -duration $duration -timeGrain $timeGrain -resourceId $resourceId } else { $Templates[$_] = Initialize-TilePsCustomObject -metricType $_ -startTime $startTime -endTime $endTime -timeGrain $timeGrain -resourceId $resourceId } } # deprecated tiles # set markDown board content # $dashboardBody.properties.lenses."0".parts."0".metadata.settings.content.settings.content += $aggregationType + " Volume Operations " + $metricType + " by `n" + $description # the tile of total performance # $tileJson = $tileTemplate | ConvertTo-Json -depth 100 | ConvertFrom-Json # $tileJson.metadata.inputs[0].value.charts[0].itemDataModel.psobject.properties.remove("filters") # $dashboardBody.properties.lenses."0".parts | Add-Member -MemberType NoteProperty -Name "1" -Value $tileJson $dashboardBody.properties.lenses."0".parts = [PSCustomObject]@{} # create tiles if ($capacityOnly -eq $true) { $tileColCount = 3 for ($($tileIndex = 0; $tileNum = 0); $tileIndex -lt $volumesByType.$volumeType.Count; $tileIndex++) { $positionY = $tileRowSpan * ([math]::floor($tileIndex/$tileColCount)) $positionX = $tileColSpan * ($tileIndex%$tileColCount) $thisMetricType = $metricTypes[0] $tileName = $volumesByType.$volumeType[$tileIndex].Item2 + " " + $metricTypes[0] $filterVolumeName = $volumesByType.$volumeType[$tileIndex].Item2 $tileJsonObj = Get-TilePsCustomObject -tileTemplate $Templates[$metricTypes[0]] -tileName $tileName -positionX $positionX -positionY $positionY -filterVolumeName $filterVolumeName -metricType $thisMetricType # $chart.itemDataModel.filters.OperandFilters[0].OperandSelectedValues[0] = $volumesByType.($volumeTypes[$rowNum])[$colNum].Item1 $dashboardBody.properties.lenses."0".parts | Add-Member -MemberType NoteProperty -Name $tileNum -Value $tileJsonObj $tileNum++ } } else { for ($($rowNum = 0; $tileNum = 0); $rowNum -lt $volumesByType.$volumeType.Count; $rowNum++) { $positionY = $tileRowSpan * $rowNum for ($colNum = 0; $colNum -lt $metricTypes.Count; $colNum++) { $posotionX = $tileColSpan * $colNum $thisMetricType = $metricTypes[$colNum] $tileName = $volumesByType.$volumeType[$rowNum].Item2 + " " + $( if ($thisMetricType -eq "Count") {"Operation"} else {""} ) + $metricTypes[$colNum] $filterVolumeName = $( if ($thisMetricType -eq "Capacity") { $volumesByType.$volumeType[$rowNum].Item2 } else { $volumesByType.$volumeType[$rowNum].Item1 } ) $tileJsonObj = Get-TilePsCustomObject -tileTemplate $Templates[$metricTypes[$colNum]] -tileName $tileName -positionX $posotionX -positionY $positionY -filterVolumeName $filterVolumeName -metricType $thisMetricType # $chart.itemDataModel.filters.OperandFilters[0].OperandSelectedValues[0] = $volumesByType.($volumeTypes[$rowNum])[$colNum].Item1 $dashboardBody.properties.lenses."0".parts | Add-Member -MemberType NoteProperty -Name $tileNum -Value $tileJsonObj $tileNum++ } } } $dashboardBody } # Formats JSON in a nicer format than the built-in ConvertTo-Json does. # To reduce JSON output file size for 12 and 16 node stamps. function Format-Json([Parameter(Mandatory, ValueFromPipeline)][String] $json) { $indent = 0; ($json -Split "`n" | ForEach-Object { if ($_ -match '[\}\]]\s*,?\s*$') { # This line ends with ] or }, decrement the indentation level $indent-- } $line = (' ' * $indent) + $($_.TrimStart() -replace '": (["{[])', '": $1' -replace ': ', ': ') if ($_ -match '[\{\[]\s*$') { # This line ends with [ or {, increment the indentation level $indent++ } $line }) -Join "`n" } #========Module Initalize========# $context = $null #size of tile $tileColSpan = 6 $tileRowSpan = 4 # If you want to add new metrics, adapt function Initialize-TilePsCustomObject and Get-TilePsCustomObject, then register here if ($capacityOnly -eq $True) { $metricTypes = @('Capacity') } else { $metricTypes = @('Capacity', 'Throughput', 'Count', 'Latency') } switch ($volumeType) { object { $volumeTypes = @("ObjStore") } infrastructure { $volumeTypes = @("Infrastructure") } vmtemp { $volumeTypes = @("VmTemp") } Default { $volumeTypes = @("ObjStore", "Infrastructure", "VmTemp") } } if (!((Test-Path -Path ($jsonTemplateLocation.TrimEnd('\') + '\dashboardBody.json')) -and (Test-Path -Path ($jsonTemplateLocation.TrimEnd('\') + '\tileTemplate.json' )))) { throw "Template location not exist." } try { $dashboardBody = Get-Content ($jsonTemplateLocation.TrimEnd('\') + '\dashboardBody.json') | Out-String $tileTemplate = Get-Content ($jsonTemplateLocation.TrimEnd('\') + '\tileTemplate.json') | Out-String $capacityTemplate = Get-Content ($jsonTemplateLocation.TrimEnd('\') + '\capacityTemplate.json') | Out-String } catch { Write-Error $_ throw "Template not exist" } if ($PSCmdlet.ParameterSetName -eq "absoluteTime") { Save-AzureStackVolumesPerformanceDashboardJson -DefaultProfile $DefaultProfile -startTime $startTime -endTime $endTime -timeGrain $timeGrain -outputLocation $outputLocation } else { Save-AzureStackVolumesPerformanceDashboardJson -DefaultProfile $DefaultProfile -duration $duration -timeGrain $timeGrain -outputLocation $outputLocation }