<#
.SYNOPSIS
Get Azure Cost Analysis information, sorted by Service.
.DESCRIPTION
Get Azure Cost Analysis information, using REST API calls.
.EXAMPLE
Get-AzCostAnalysis
.EXAMPLE
Get-AzCostAnalysis -SubscriptionID <string>
.CONTEXT
Windows Virtual Desktops
.NOTES
Version: 0.1
Author: Esther Barthel, MSc
Creation Date: 2020-10-25
Updated: 2020-10-25
Purpose: WVD Administration, through REST API calls
Copyright (c) cognition IT. All rights reserved.
#>
[CmdletBinding()]
Param()
# ------------------------------------
# | WVD Functions for REST API calls |
# ------------------------------------
#region Global Variables
$wvdApiVersion = "2019-12-10-preview"
#endregion Global Variables
#region Windows Presentation Foundation (WPF) form to store WVD Service Principal information
[string]$mainformXAML = @'
<Window x:Class="wvdSP_Input_Form.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Esther_s_Input_Form"
mc:Ignorable="d"
Title="Enter the WVD Service Principal (SP) details" Height="389.336" Width="617.103">
<Grid>
<TextBox x:Name="textboxTenantId" HorizontalAlignment="Left" Height="31" Margin="176,50,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="398"/>
<Label Content="SP Tenant ID" HorizontalAlignment="Left" Height="30" Margin="29,51,0,0" VerticalAlignment="Top" Width="117"/>
<TextBox x:Name="textboxAppId" HorizontalAlignment="Left" Height="30" Margin="176,118,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="398"/>
<Label Content="SP App ID" HorizontalAlignment="Left" Height="30" Margin="29,118,0,0" VerticalAlignment="Top" Width="117"/>
<PasswordBox x:Name="textboxAppSecret" HorizontalAlignment="Left" Height="31" Margin="176,192,0,0" VerticalAlignment="Top" Width="398"/>
<Label Content="SP App Secret" HorizontalAlignment="Left" Height="30" Margin="29,193,0,0" VerticalAlignment="Top" Width="117"/>
<Button x:Name="buttonOK" Content="OK" HorizontalAlignment="Left" Height="46" Margin="29,274,0,0" VerticalAlignment="Top" Width="175" IsDefault="True"/>
<Button x:Name="buttonCancel" Content="Cancel" HorizontalAlignment="Left" Height="46" Margin="244,274,0,0" VerticalAlignment="Top" Width="175" IsDefault="True"/>
</Grid>
</Window>
'@
function Invoke-WVDSPCredentialsForm {
# Created by Guy Leech - @guyrleech 17/05/2020
Param
(
[Parameter(Mandatory=$true)]
$inputXaml
)
$form = $null
$inputXML = $inputXaml -replace 'mc:Ignorable="d"' , '' -replace 'x:N' ,'N' -replace '^<Win.*' , '<Window'
[xml]$xaml = $inputXML
if( $xaml )
{
$reader = New-Object -TypeName Xml.XmlNodeReader -ArgumentList $xaml
try
{
$form = [Windows.Markup.XamlReader]::Load( $reader )
}
catch
{
Throw "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .NET is installed.`n$_"
}
$xaml.SelectNodes( '//*[@Name]' ) | ForEach-Object `
{
Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name) -Scope Global
}
}
else
{
Throw "Failed to convert input XAML to WPF XML"
}
$form
}
#endregion WPF form
function Get-AzSPStoredCredentials {
<#
.SYNOPSIS
Retrieve the Azure Service Principal Stored Credentials.
.DESCRIPTION
Retrieve the Azure Service Principal Stored Credentials from a stored credentials file.
.EXAMPLE
Get-AzSPStoredCredentials
.CONTEXT
Azure
.NOTES
Version: 0.1
Author: Esther Barthel, MSc
Creation Date: 2020-08-03
Purpose: WVD Administration, through REST API calls
Copyright (c) cognition IT. All rights reserved.
#>
[CmdletBinding()]
Param()
#region function settings
# Stored Credentials XML file
$System = "AZ"
$strAzSPCredFolder = "$([environment]::GetFolderPath('CommonApplicationData'))\ControlUp\ScriptSupport"
$AzSPCredentials = $null
#endregion
Write-Verbose ""
Write-Verbose "----------------------------- "
Write-Verbose "| Get Azure SP Credentials: | "
Write-Verbose "----------------------------- "
Write-Verbose ""
If (Test-Path -Path "$($strAzSPCredFolder)\$($env:USERNAME)_$($System)_Cred.xml")
{
try
{
$AzSPCredentials = Import-Clixml -Path "$strAzSPCredFolder\$($env:USERNAME)_$($System)_Cred.xml"
}
catch
{
Write-Error ("The required PSCredential object could not be loaded. " + $_)
}
}
Else
{
Write-Error "The Azure Service Principal Credentials file stored for this user ($($env:USERNAME)) cannot be found. `nCreate the file with the Set-AzSPCredentials script action (prerequisite)."
Exit
}
return $AzSPCredentials
}
function Get-AzBearerToken {
<#
.SYNOPSIS
Retrieve the Azure Bearer Token for an authentication session.
.DESCRIPTION
Retrieve the Azure Bearer Token for an authentication session, using a REST API call.
.EXAMPLE
Get-AzBearerToken -SPCredentials <PSCredentialObject> -TenantID <string>
.CONTEXT
Azure
.NOTES
Version: 0.1
Author: Esther Barthel, MSc
Creation Date: 2020-03-22
Updated: 2020-05-08
Created a separate Azure Credentials function to support ARM architecture and REST API scripted actions
Purpose: WVD Administration, through REST API calls
Copyright (c) cognition IT. All rights reserved.
#>
[CmdletBinding()]
Param(
[Parameter(
Position=0,
Mandatory=$true,
HelpMessage='Enter the Service Principal credentials'
)]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.PSCredential] $SPCredentials,
[Parameter(
Position=1,
Mandatory=$true,
HelpMessage='Enter the Tenant ID'
)]
[ValidateNotNullOrEmpty()]
[string] $TenantID
)
#region Prep variables
# URL for REST API call to authenticate with Azure (using the TenantID parameter)
$uri = "https://login.microsoftonline.com/$TenantID/oauth2/token"
# Create the Invoke-RestMethod Body (using the SPCredentials parameter)
$body = @{
grant_type="client_credentials";
client_Id=$($SPCredentials.UserName);
client_Secret=$($SPCredentials.GetNetworkCredential().Password);
resource="https://management.azure.com"
}
#debug: $body
# Create the Invoke-RestMethod parameters
$invokeRestMethodParams = @{
Uri = $uri
Body = $body
Method = "POST"
ContentType = "application/x-www-form-urlencoded"
}
#endregion
try
{
$response = $null
# Make the REST API call with the created parameters
$response = Invoke-RestMethod @invokeRestMethodParams
}
catch
{
Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
}
# return the JSON response
return $response
}
function Get-AzSubscription {
<#
.SYNOPSIS
Retrieve the Azure Subscription information.
.DESCRIPTION
Retrieve the Azure Subscription information, using a REST API call.
.EXAMPLE
Get-AzSubscription -BearerToken <string> -SubscriptionID <string>
.CONTEXT
Azure
.NOTES
Version: 0.1
Author: Esther Barthel, MSc
Creation Date: 2020-09-20
Updated: 2020-09-20
Created a separate Azure Credentials function to support ARM architecture and REST API scripted actions
Purpose: WVD Administration, through REST API calls
Copyright (c) cognition IT. All rights reserved.
#>
[CmdletBinding()]
Param(
[Parameter(
Position=0,
Mandatory=$true,
HelpMessage='Enter a valid bearer token'
)]
[ValidateNotNullOrEmpty()]
[string] $BearerToken,
[Parameter(
Position=1,
Mandatory=$false,
HelpMessage='Enter a subscriptionID'
)]
[string] $SubscriptionID
)
#region Prep variables
# URL for REST API call to authenticate with Azure (using the TenantID parameter)
$uri = "https://management.azure.com/subscriptions?api-version=2020-01-01"
# Create the Invoke-RestMethod Header (using the bearertoken parameter)
$header = @{
"Authorization"="Bearer $BearerToken";
"Content-Type" = "application/json"
}
#debug: $header
# Create the Invoke-RestMethod parameters
$invokeRestMethodParams = @{
Uri = $uri
Method = "GET"
Headers = $header
}
#debug: $invokeRestMethodParams
#endregion
try
{
$response = $null
# Make the REST API call with the created parameters
$response = Invoke-RestMethod @invokeRestMethodParams
}
catch
{
Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
}
# filter the response if a SubscriptionID was provided
If (!([string]::IsNullOrEmpty($subScriptionID)))
{
$results = ($response.value).Where({$_.subscriptionId -like "$subScriptionID"})
}
else
{
$results = $response.value
}
return $results
}
function Get-AzCostAnalysisActualService () {
<#
.SYNOPSIS
Get Azure Cost Analysis information on the actual costs for this month sorted by Service.
.DESCRIPTION
Get Azure Cost Analysis information on the actual costs for this month sorted by Service, using a REST API call.
.EXAMPLE
Get-AzCostAnalysisActualService -BearerToken <string> -SubscriptionID <string>
.CONTEXT
Azure
.NOTES
Version: 0.1
Author: Esther Barthel, MSc
Creation Date: 2020-10-25
Updated: 2020-10-25
Created a separate Azure Cost Analysis function for the costs of this month, sorted by Service
Purpose: WVD Administration, through REST API calls
Copyright (c) cognition IT. All rights reserved.
#>
[CmdletBinding()]
Param(
[Parameter(
Position=0,
Mandatory=$true,
HelpMessage='Enter a valid bearer token'
)]
[ValidateNotNullOrEmpty()]
[string] $BearerToken,
[Parameter(
Position=1,
Mandatory=$true,
HelpMessage='Enter the Subscription ID'
)]
[ValidateNotNullOrEmpty()]
[string] $SubscriptionID
)
#region Prep variables
# URL for REST API call to list hostpools, based on given subscription ID
$uri = "https://management.azure.com/subscriptions/$SubscriptionID/providers/Microsoft.CostManagement/query`?api-version=2019-11-01"#&`$top=5000"
# Create the Invoke-RestMethod Header (using the bearertoken parameter)
$header = @{
"Authorization"="Bearer $BearerToken";
"Content-Type" = "application/json"
}
#debug: $header
# Create Custom time period for this month
#$time = (Get-Date).AddMonths(-1).ToString("yyyy-MM")
$time = $((Get-Date).AddMonths(0).ToString("yyyy-MM"))
$lastdayofmonth = "$([System.DateTime]::DaysInMonth((Get-Date).AddMonths(0).Year,(Get-Date).AddMonths(0).Month))"
# Create the JSON formatted body
$body=@{
"type"= "ActualCost";
"dataSet"= @{
"granularity"= "None";
"aggregation"= @{
"totalCost"= @{"name"= "Cost";"function"= "Sum"}
};
"grouping"= @(
@{"type"="Dimension";"name"="ServiceName"}
)
};
"timeframe"="Custom";
"timePeriod"=@{"from"="$time`-01T00:00:00+00:00";"to"="$time`-$lastdayofmonth`T23:59:59+00:00"}
}
$bodyJSON = ConvertTo-Json -InputObject $body -Depth 10
# Create the Invoke-RestMethod parameters
$invokeRestMethodParams = @{
Uri = $uri
Method = "POST"
Headers = $header
Body = $bodyJSON
}
#debug: $invokeRestMethodParams
#endregion
try
{
$response = $null
# Make the REST API call with the created parameters
$response = Invoke-RestMethod @invokeRestMethodParams
}
catch
{
Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
}
#debug: $response
$results = $response.properties
return $results
}
function Get-AzCostAnalysisForecastService () {
<#
.SYNOPSIS
Get Azure Cost Analysis information on the forecasted costs for this month sorted by Service.
.DESCRIPTION
Get Azure Cost Analysis information on the forecasted costs for this month sorted by Service, using a REST API call.
.EXAMPLE
Get-AzCostAnalysisForecastService -BearerToken <string> -SubscriptionID <string>
.CONTEXT
Azure
.NOTES
Version: 0.1
Author: Esther Barthel, MSc
Creation Date: 2020-10-25
Updated: 2020-10-25
Created a separate Azure Cost Analysis function to support ARM architecture and REST API scripted actions
Purpose: WVD Administration, through REST API calls
Copyright (c) cognition IT. All rights reserved.
#>
[CmdletBinding()]
Param(
[Parameter(
Position=0,
Mandatory=$true,
HelpMessage='Enter a valid bearer token'
)]
[ValidateNotNullOrEmpty()]
[string] $BearerToken,
[Parameter(
Position=1,
Mandatory=$true,
HelpMessage='Enter the Subscription ID'
)]
[ValidateNotNullOrEmpty()]
[string] $SubscriptionID
)
#region Prep variables
# URL for REST API call to list hostpools, based on given subscription ID
$uri = "https://management.azure.com/subscriptions/$SubscriptionID/providers/Microsoft.CostManagement/forecast`?api-version=2019-11-01"#&`$top=5000"
# Create the Invoke-RestMethod Header (using the bearertoken parameter)
$header = @{
"Authorization"="Bearer $BearerToken";
"Content-Type" = "application/json"
}
#debug: $header
# Create Custom time period for this month
#$time = (Get-Date).AddMonths(-1).ToString("yyyy-MM")
$time = $((Get-Date).AddMonths(0).ToString("yyyy-MM"))
$lastdayofmonth = "$([System.DateTime]::DaysInMonth((Get-Date).AddMonths(0).Year,(Get-Date).AddMonths(0).Month))"
# Create the JSON formatted body
$body=@{
"type"= "ActualCost";
"dataSet"= @{
"granularity"= "None";
"aggregation"= @{
"totalCost"= @{"name"= "Cost";"function"= "Sum"}
};
"grouping"= @(
@{"type"="Dimension";"name"="ServiceName"}
)
};
"timeframe"="Custom";
"timePeriod"=@{"from"="$time`-01T00:00:00+00:00";"to"="$time`-$lastdayofmonth`T23:59:59+00:00"};
"includeActualCost"="false";
"includeFreshPartialCost"="false"
}
$bodyJSON = ConvertTo-Json -InputObject $body -Depth 10
# Create the Invoke-RestMethod parameters
$invokeRestMethodParams = @{
Uri = $uri
Method = "POST"
Headers = $header
Body = $bodyJSON
}
#debug: $invokeRestMethodParams
#endregion
try
{
$response = $null
# Make the REST API call with the created parameters
$response = Invoke-RestMethod @invokeRestMethodParams
}
catch
{
Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
}
#debug: $response
$results = $response.properties
return $results
}
#region ControlUp Script Standards - version 0.2
#Requires -Version 5.1
# Configure a larger output width for the ControlUp PowerShell console
[int]$outputWidth = 400
# Altering the size of the PS Buffer
$PSWindow = (Get-Host).UI.RawUI
$WideDimensions = $PSWindow.BufferSize
$WideDimensions.Width = $outputWidth
$PSWindow.BufferSize = $WideDimensions
# Ensure Debug information is shown, without the confirmation question after each Write-Debug
If ($PSBoundParameters['Debug']) {$DebugPreference = "Continue"}
If ($PSBoundParameters['Verbose']) {$VerbosePreference = "Continue"}
$ErrorActionPreference = "Stop"
#endregion
#-----------------#
# Script Workflow #
#-----------------#
Write-Output ""
# Script output
If ($azSPCredentials = Get-AzSPStoredCredentials)
{
#debug: $azSPCredentials
# Sign in to Azure with a Service Principal with Contributor Role at Subscription level and retrieve the brearer token
try
{
$azBearerToken = $null
$azBearerToken = Get-AzBearerToken -SPCredentials $azSPCredentials.spCreds -TenantID $($azSPCredentials.tenantID).ToString()
#debug: $azBearerToken
}
catch
{
Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
Exit
}
# Retrieve the Subscription information for the Service Principal (that is logged on)
$azSubscription = $null
$azSubscription = Get-AzSubscription -bearerToken $($azBearerToken.access_token)
#debug: Write-Output "DEBUG INFO - subscriptionID: $($azSubscription.subscriptionId)"
# Retrieve the Cost Analysis - Actual Costs details
$costAnalysisResults = Get-AzCostAnalysisActualService -bearerToken $($azBearerToken.access_token) -SubscriptionID $($azSubscription.subscriptionId.Split("/")[-1])
# Build output dataset, based on custom object
$dataResults = @()
for ($row=0;$row -le (($costAnalysisResults.rows.Count)-1); $row++)
{
$htrow = New-Object psobject
for ($col=0;$col -le (($costAnalysisResults.columns.Count)-1); $col++)
{
$htrow | Add-Member -Type NoteProperty -Name "$($costAnalysisResults.columns[$col].name)" -Value "$($costAnalysisResults.rows[$row][$col])"
}
$dataResults += $htrow
}
# Present the Cost information
Write-Host "* Actual costs for this month, sorted by Service: " -ForegroundColor Yellow
$totalCosts = $([math]::Round((($dataResults | Measure-Object -Property Cost -Sum).Sum),2))
$dataResults | Select ServiceName,
Currency,
Cost,
@{Name='Costs'; Expression={$([math]::Round($_.Cost,2))}},
@{Name='Service Name'; Expression={$_.ServiceName}},
@{Name='Percentage'; Expression={$([math]::Round((($_.Cost/$totalCosts)*100),0))}} | `
Sort Costs -Descending | `
Format-Table "Service Name",
@{Name='Costs '; Expression={"$($_.Currency){0,10:N2}" -f($($_.Costs))}; Align="right"},
@{Name='Percentage'; Expression={"{0:N0} %" -f($($_.Percentage))}; Align="right"} -AutoSize
# | Sort Costs -Descending | Format-Table Costs, "Service Name", @{Name='CostStatus'; Expression={"ActualCosts"}} -AutoSize
# Retrieve the Cost Analysis - Forecast details
$forecastAnalysisResults = Get-AzCostAnalysisForecastService -bearerToken $($azBearerToken.access_token) -SubscriptionID $($azSubscription.subscriptionId.Split("/")[-1]) #-ResourceGroupName $ResourceGroupName
# Build output dataset, based on custom object
$forecastResults = @()
for ($row=0;$row -le (($forecastAnalysisResults.rows.Count)-1); $row++)
{
$fhtrow = New-Object psobject
for ($col=0;$col -le (($forecastAnalysisResults.columns.Count)-1); $col++)
{
$fhtrow | Add-Member -Type NoteProperty -Name "$($forecastAnalysisResults.columns[$col].name)" -Value "$($forecastAnalysisResults.rows[$row][$col])"
}
$forecastResults += $fhtrow
}
# Present the Forecast information
Write-Host "* Forecast of the costs for the remainder of this month, sorted by Service: " -ForegroundColor Yellow
$totalForecasts = $([math]::Round((($forecastResults | Measure-Object -Property Cost -Sum).Sum),2))
$forecastResults | Select Currency,
Cost,
CostStatus |
Sort Cost |
Format-Table @{Name='Costs '; Expression={"$($_.Currency) {0,8:N2}" -f($([math]::Round($_.Cost,2)))};Align="left"},
@{Name='Cost Status'; Expression={"$($_.CostStatus)"};Align="right"} -AutoSize
# Present the Summary information
Write-Host "* Summary for this month: " -ForegroundColor Yellow
Write-Host " - Actual Costs (incl. today): " -ForegroundColor Cyan -NoNewline
Write-Host ( "$($dataResults[0].Currency) {0:N2} " -f($([math]::Round((($dataResults | Measure-Object -Property Cost -Sum).Sum),2))) ) -ForegroundColor Yellow
Write-Host " - Predicted Costs (incl. Forecast): " -ForegroundColor Cyan -NoNewline
Write-Host ( "$($forecastResults[0].Currency) {0:N2} " -f($([math]::Round( (($forecastResults | Measure-Object -Property Cost -Sum).Sum) + (($dataResults | Measure-Object -Property Cost -Sum).Sum),2))) ) -ForegroundColor Yellow -NoNewline
Write-Host ( "(`+ " + "{0:N2}" -f($([math]::Round((($forecastResults | Measure-Object -Property Cost -Sum).Sum),2))) + ")" ) -ForegroundColor Yellow
Write-Output ""
}
else
{
Write-Warning "No Azure Credentials could be retrieved from the stored credentials file for this user."
}
# SIG # Begin signature block
# MIINHAYJKoZIhvcNAQcCoIINDTCCDQkCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU8Iv0vrdZzrXb9qyLJ4HnCug4
# BhigggpeMIIFJjCCBA6gAwIBAgIQCyXBE0rAWScxh3bGfykLTjANBgkqhkiG9w0B
# AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz
# c3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDgxMDAwMDAwMFoXDTIzMDgx
# NTEyMDAwMFowYzELMAkGA1UEBhMCTkwxDzANBgNVBAcTBkxlbW1lcjEVMBMGA1UE
# ChMMY29nbml0aW9uIElUMRUwEwYDVQQLEwxDb2RlIFNpZ25pbmcxFTATBgNVBAMT
# DGNvZ25pdGlvbiBJVDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL2y
# YRbztz9wTtatpSJ5NMD0JZtDPhuFqAVTjoGc1jvn68M41zlGgi8fVvEccaH3nDTT
# 6T8edgFuEbsZVHZGmY109zHOPwXX+Zvp3T+Hk2Ys8Liwwirr6xw9dlneBu85j8gd
# Mamz+mNjzpyBg1eVlD7cV1JAL3oAXgONRiebdpD6DPvd3melPmeg84Un3VV6+W8M
# 8Y0Pec+TbxIda18Lr4DqnIl0a/Suk8kQ2DzZXDXoK+MCfA6zsqyEOSY5yI5OwdU0
# 93LC2PHFEKEkIogBlCiD0UQDbamPdu7wZnTAHPTDfifdMhCPLBA0y4pj4jm6ggFE
# 3ZuQMR/yU8JXSwy72ZECAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrEuXsqCqOl
# 6nEDwGD5LfZldQ5YMB0GA1UdDgQWBBR2SeoVDh3RxGqV5iamn/FFU6J65zAOBgNV
# HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAwbjA1oDOg
# MYYvaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5j
# cmwwNaAzoDGGL2h0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQt
# Y3MtZzEuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEW
# HGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEF
# BQcBAQR4MHYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBO
# BggrBgEFBQcwAoZCaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
# U0hBMkFzc3VyZWRJRENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJ
# KoZIhvcNAQELBQADggEBAL4Zda674x5WLL8B059a9cxnUIC05LcjD/3hkCLZgbMa
# krDrfsjNpA+KpMiTv2TW5pDRCXGJirJO27XRTojr2F8+gJAyIB+8ZLiyKmy3IcCV
# DXjjb6i/4TiGbDmGL3Ctl5pmWRpksnr3TKSMyxz2OogLS6w9pgRdA1hgJSfZMV+a
# KRrd4iW5YWKIwFZlYDeQqpBBtQ6ujzgQ/04FcmjyOlNch4hofJVLauzkSb1Tnzt1
# 6TyT2pJ9BzasoOlxYEFhn0ikXndlKVBb7gpFInqSf5DJtaVRIXojj0eqN6LZroUz
# 62m2YeR29uC06xcdF7fjo+YKxe+kdApdPfX0Nx9Moc8wggUwMIIEGKADAgECAhAE
# CRgbX9W7ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUw
# EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x
# JDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIx
# MjAwMDBaFw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE
# aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMT
# KERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0G
# CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmx
# OttE9X/lqJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfT
# xvspJ8fTeyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6Ygs
# IJWuHEqHCN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tK
# tel05iv+bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0
# xY4PwaLoLFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGj
# ggHNMIIByTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNV
# HSUEDDAKBggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0
# dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2Vy
# dHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYD
# VR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
# QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCG
# SAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29t
# L0NQUzAKBghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1Dlgw
# HwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQAD
# ggEBAD7sDVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBV
# N7Dh9tGSdQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEb
# Bw6RFfu6r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZ
# cbEMj7uo+MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRr
# sutmQ9qzsIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKU
# GIUukpHqaGxEMrJmoecYpJpkUe8xggIoMIICJAIBATCBhjByMQswCQYDVQQGEwJV
# UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
# Y29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29kZSBTaWdu
# aW5nIENBAhALJcETSsBZJzGHdsZ/KQtOMAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3
# AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG
# AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBR+SYLJCJ/F
# +H1abXkWckwKg259ljANBgkqhkiG9w0BAQEFAASCAQCfcRgMO5gsDFd1Koy4Lyoj
# O4SEH/ghkKt4cPd+alAbEusV0D9hoOkFQfTp2pNtp/GTffOIGULznP0whhhRE3pg
# yfk6iCG2J+fPhEGxw4i6wsLDIPNbOk/TikLhAnoW3+WxPCs5acr3y1RM1WiUm8Fz
# fFuaa57C7etYRUtXYHbh3a+cXJCTH327ZcAiWWFKk6gaZoat7b1u68KmPQS3USSX
# v0E0mFxqjOnRgFPvCBjebJemQVpJUSXl4pKrb/Xmu4Ag/UfxGJ5yJfTa+UnyzPpH
# VpYgss5GEfLGMszKL4Y7JBy5d1iGphF5A8FsywQ5WWRfGuklpHZU7HvI/NJXGv2M
# SIG # End signature block