Azure Cost Analysis – Costs per Service

This Azure Script Based Action allows you to to retrieve detailed information on actual and forecast costs of the Azure Subscription your Service Principal has access too.
README: https://support.controlup.com/hc/en-us/articles/360011378518#h_01EE8GMB35X9Y20MXV22N1GGE9
Version 1.1.2
Created on 2020-12-06
Modified on 2020-12-13
Created by Esther Barthel, MSc
Downloads: 40

The Script Copy Script Copied to clipboard
<#
.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