master 143b27eac1ea cached
107 files
1.9 MB
472.0k tokens
1 requests
Download .txt
Showing preview only (1,987K chars total). Download the full file or copy to clipboard to get everything.
Repository: helderpinto/AzureOptimizationEngine
Branch: master
Commit: 143b27eac1ea
Files: 107
Total size: 1.9 MB

Directory structure:
gitextract_09u_p9cv/

├── .github/
│   └── workflows/
│       ├── continuous-deployment-dev-new.yml
│       ├── continuous-deployment-dev.yml
│       └── continuous-deployment.yml
├── .gitignore
├── Deploy-AzureOptimizationEngine.ps1
├── LICENSE
├── README.md
├── Reset-AutomationSchedules.ps1
├── Setup-BenefitsUsageDependencies.ps1
├── Setup-DataCollectionRules.ps1
├── Setup-LogAnalyticsWorkspaces.ps1
├── Suppress-Recommendation.ps1
├── azuredeploy-nested.bicep
├── azuredeploy.bicep
├── custom-recommendations-types.json
├── docs/
│   ├── configuring-workspaces.md
│   ├── customizing-aoe.md
│   └── suppressing-recommendations.md
├── model/
│   ├── filters-table.sql
│   ├── loganalyticsingestcontrol-initialize.sql
│   ├── loganalyticsingestcontrol-table.sql
│   ├── loganalyticsingestcontrol-upgrade.sql
│   ├── recommendations-sp.sql
│   ├── recommendations-table.sql
│   ├── sqlserveringestcontrol-initialize.sql
│   └── sqlserveringestcontrol-table.sql
├── perfcounters.json
├── queries/
│   ├── rbac-spns-keys-expiring.kql
│   ├── rbac-spns-roles-aad-all.kql
│   ├── rbac-spns-roles-arm-all.kql
│   ├── rbac-spns-roles-arm-privileged.kql
│   ├── rbac-users-roles-aad-all.kql
│   ├── rbac-users-roles-arm-privileged.kql
│   ├── rbac-users-roles-guests-privileged.kql
│   └── rbac-users-roles-guests.kql
├── runbooks/
│   ├── data-collection/
│   │   ├── Export-AADObjectsToBlobStorage.ps1
│   │   ├── Export-ARGAppGatewayPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGAppServicePlanPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGLoadBalancerPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGManagedDisksPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGNICPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGNSGPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGPublicIpPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGResourceContainersPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGSqlDatabasePropertiesToBlobStorage.ps1
│   │   ├── Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGVMSSPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGVNetPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1
│   │   ├── Export-AdvisorRecommendationsToBlobStorage.ps1
│   │   ├── Export-AzMonitorMetricsToBlobStorage.ps1
│   │   ├── Export-ConsumptionToBlobStorage.ps1
│   │   ├── Export-PolicyComplianceToBlobStorage.ps1
│   │   ├── Export-PriceSheetToBlobStorage.ps1
│   │   ├── Export-RBACAssignmentsToBlobStorage.ps1
│   │   ├── Export-ReservationsPriceToBlobStorage.ps1
│   │   ├── Export-ReservationsUsageToBlobStorage.ps1
│   │   ├── Export-SavingsPlansUsageToBlobStorage.ps1
│   │   └── Ingest-OptimizationCSVExportsToLogAnalytics.ps1
│   ├── maintenance/
│   │   └── CleanUp-OlderRecommendationsFromSqlServer.ps1
│   ├── recommendations/
│   │   ├── Ingest-RecommendationsToLogAnalytics.ps1
│   │   ├── Ingest-RecommendationsToSQLServer.ps1
│   │   ├── Ingest-SuppressionsToLogAnalytics.ps1
│   │   ├── Recommend-AADExpiringCredentialsToBlobStorage.ps1
│   │   ├── Recommend-ARMOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-AdvisorAsIsToBlobStorage.ps1
│   │   ├── Recommend-AdvisorCostAugmentedToBlobStorage.ps1
│   │   ├── Recommend-AppServiceOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-DiskOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-SqlDbOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-StorageAccountOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-UnattachedDisksToBlobStorage.ps1
│   │   ├── Recommend-UnusedAppGWsToBlobStorage.ps1
│   │   ├── Recommend-UnusedLoadBalancersToBlobStorage.ps1
│   │   ├── Recommend-VMOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-VMSSOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-VMsHighAvailabilityToBlobStorage.ps1
│   │   └── Recommend-VNetOptimizationsToBlobStorage.ps1
│   └── remediations/
│       ├── Remediate-AdvisorRightSizeFiltered.ps1
│       ├── Remediate-LongDeallocatedVMsFiltered.ps1
│       └── Remediate-UnattachedDisksFiltered.ps1
├── upgrade-manifest.json
└── views/
    ├── AzureOptimizationEngine.pbix
    ├── powerbi-query.m
    └── workbooks/
        ├── benefits-simulation.bicep
        ├── benefits-simulation.json
        ├── benefits-usage.bicep
        ├── benefits-usage.json
        ├── blockblobstorage-usage.bicep
        ├── blockblobstorage-usage.json
        ├── costs-growing.bicep
        ├── costs-growing.json
        ├── identities-roles.bicep
        ├── identities-roles.json
        ├── policy-compliance.bicep
        ├── policy-compliance.json
        ├── recommendations.bicep
        ├── recommendations.json
        ├── reservations-potential.bicep
        ├── reservations-potential.json
        ├── reservations-usage.bicep
        ├── reservations-usage.json
        ├── resources-inventory.bicep
        ├── resources-inventory.json
        ├── savingsplans-usage.bicep
        └── savingsplans-usage.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/continuous-deployment-dev-new.yml
================================================
name: AOE Continuous Deployment (DEV NEW)
on: 
  workflow_dispatch:
  push:
    branches:
      - dev
permissions:
      id-token: write
      contents: read      
jobs:
  AOE-CD:
    environment: devnew
    runs-on: ubuntu-latest
    env:
      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      AOE_SQL_ADMIN: ${{ secrets.AOE_SQL_ADMIN }}
      AOE_SQL_PASSWD: ${{ secrets.AOE_SQL_PASSWD }}
      AOE_LOCATION: ${{ secrets.AOE_LOCATION }}
      AOE_NAMEPREFIX: ${{ secrets.AOE_NAMEPREFIX }}
    steps:
      - run: echo "This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!"
      - name: Installing modules
        shell: pwsh
        run: |
          Set-PSRepository PSGallery -InstallationPolicy Trusted
          Install-Module -Name Az.Accounts,Az.Resources,Az.Storage,Az.OperationalInsights,Az.Sql,Az.Automation,Microsoft.Graph.Authentication,Microsoft.Graph.Identity.DirectoryManagement -Force
      - name: Check out repository code
        uses: actions/checkout@v3
      - name: Login via Az module
        uses: azure/login@hf_447_release
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          enable-AzPSSession: true 
      - name: Create Deployment Settings JSON file
        run: |
          echo '{
            "SubscriptionId": "'"$AZURE_SUBSCRIPTION_ID"'",
            "NamePrefix": "'"$AOE_NAMEPREFIX$(date '+%Y%m%d%H')"'",
            "WorkspaceReuse": "n",
            "DeployWorkbooks": "y",
            "SqlAdmin": "'"$AOE_SQL_ADMIN"'",
            "SqlPass": "'"$AOE_SQL_PASSWD"'",
            "TargetLocation": "'"$AOE_LOCATION"'",
            "DeployBenefitsUsageDependencies": "n"
          }' > ./deploymentSettings.json          
      - name: Testing PowerShell script call
        shell: pwsh
        run: |
          ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/helderpinto/AzureOptimizationEngine/dev/azuredeploy.bicep"
      - run: echo "🍏 This job's status is ${{ job.status }}."


================================================
FILE: .github/workflows/continuous-deployment-dev.yml
================================================
name: AOE Continuous Deployment (DEV)
on: 
  workflow_dispatch:
  push:
    branches:
      - dev
permissions:
      id-token: write
      contents: read      
jobs:
  AOE-CD:
    environment: dev
    runs-on: ubuntu-latest
    env:
      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      AOE_SQL_ADMIN: ${{ secrets.AOE_SQL_ADMIN }}
      AOE_SQL_PASSWD: ${{ secrets.AOE_SQL_PASSWD }}
      AOE_LOCATION: ${{ secrets.AOE_LOCATION }}
      AOE_NAMEPREFIX: ${{ secrets.AOE_NAMEPREFIX }}
      AOE_WORKSPACENAME: ${{ secrets.AOE_WORKSPACENAME }}
      AOE_WORKSPACERG: ${{ secrets.AOE_WORKSPACERG }}
    steps:
      - run: echo "This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!"
      - name: Installing modules
        shell: pwsh
        run: |
          Set-PSRepository PSGallery -InstallationPolicy Trusted
          Install-Module -Name Az.Accounts,Az.Resources,Az.Storage,Az.OperationalInsights,Az.Sql,Az.Automation,Microsoft.Graph.Authentication,Microsoft.Graph.Identity.DirectoryManagement -Force
      - name: Check out repository code
        uses: actions/checkout@v3
      - name: Login via Az module
        uses: azure/login@hf_447_release
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          enable-AzPSSession: true 
      - name: Create Deployment Settings JSON file
        run: |
          echo '{
            "SubscriptionId": "'"$AZURE_SUBSCRIPTION_ID"'",
            "NamePrefix": "'"$AOE_NAMEPREFIX"'",
            "WorkspaceReuse": "y",
            "WorkspaceName": "'"$AOE_WORKSPACENAME"'",
            "WorkspaceResourceGroupName": "'"$AOE_WORKSPACERG"'",
            "DeployWorkbooks": "y",
            "SqlAdmin": "'"$AOE_SQL_ADMIN"'",
            "SqlPass": "'"$AOE_SQL_PASSWD"'",
            "TargetLocation": "'"$AOE_LOCATION"'",
            "DeployBenefitsUsageDependencies": "n"
          }' > ./deploymentSettings.json          
      - name: Testing PowerShell script call
        shell: pwsh
        run: |
          ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/helderpinto/AzureOptimizationEngine/dev/azuredeploy.bicep" -DoPartialUpgrade
      - run: echo "🍏 This job's status is ${{ job.status }}."


================================================
FILE: .github/workflows/continuous-deployment.yml
================================================
name: AOE Continuous Deployment (PROD)
on: 
  workflow_dispatch:
  push:
    branches:
      - master
permissions:
      id-token: write
      contents: read      
jobs:
  AOE-CD:
    environment: prod
    runs-on: ubuntu-latest
    env:
      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      AOE_SQL_ADMIN: ${{ secrets.AOE_SQL_ADMIN }}
      AOE_SQL_PASSWD: ${{ secrets.AOE_SQL_PASSWD }}
      AOE_LOCATION: ${{ secrets.AOE_LOCATION }}
      AOE_NAMEPREFIX: ${{ secrets.AOE_NAMEPREFIX }}
    steps:
      - run: echo "This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!"
      - name: Installing modules
        shell: pwsh
        run: |
          Set-PSRepository PSGallery -InstallationPolicy Trusted
          Install-Module -Name Az.Accounts,Az.Resources,Az.Storage,Az.OperationalInsights,Az.Sql,Az.Automation,Microsoft.Graph.Authentication,Microsoft.Graph.Identity.DirectoryManagement -Force
      - name: Check out repository code
        uses: actions/checkout@v3
      - name: Login via Az module
        uses: azure/login@hf_447_release
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          enable-AzPSSession: true 
      - name: Create Deployment Settings JSON file
        run: |
          echo '{
            "SubscriptionId": "'"$AZURE_SUBSCRIPTION_ID"'",
            "NamePrefix": "'"$AOE_NAMEPREFIX"'",
            "WorkspaceReuse": "n",
            "DeployWorkbooks": "y",
            "SqlAdmin": "'"$AOE_SQL_ADMIN"'",
            "SqlPass": "'"$AOE_SQL_PASSWD"'",
            "TargetLocation": "'"$AOE_LOCATION"'",
            "DeployBenefitsUsageDependencies": "n"
          }' > ./deploymentSettings.json          
      - name: Testing PowerShell script call
        shell: pwsh
        run: |
          ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json
      - run: echo "🍏 This job's status is ${{ job.status }}."


================================================
FILE: .gitignore
================================================
# Deployment state file
last-deployment-state.json
# Database connection settings (for the Suppress-Recommendation.ps1 helper script)
database-connection-settings.json
# Silent deployment settings file
deployment-settings-*.json

================================================
FILE: Deploy-AzureOptimizationEngine.ps1
================================================
param (
    [Parameter(Mandatory = $false)]
    [string] $TemplateUri,

    [Parameter(Mandatory = $false)]
    [string] $AzureEnvironment = "AzureCloud",

    [Parameter(Mandatory = $false)]
    [switch] $DoPartialUpgrade,

    [Parameter(Mandatory = $false)]
    [switch] $IgnoreNamingAvailabilityErrors,

    [Parameter(Mandatory = $false)]
    [string] $SilentDeploymentSettingsPath,

    [Parameter(Mandatory = $false)]
    [hashtable] $ResourceTags = @{}
)

function ConvertTo-Hashtable {
    [CmdletBinding()]
    [OutputType('hashtable')]
    param (
        [Parameter(ValueFromPipeline)]
        $InputObject
    )
 
    process {
        if ($null -eq $InputObject) {
            return $null
        }
 
        if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
            $collection = @(
                foreach ($object in $InputObject) {
                    ConvertTo-Hashtable -InputObject $object
                }
            ) 
            Write-Output -NoEnumerate $collection
        } elseif ($InputObject -is [psobject]) { 
            $hash = @{}
            foreach ($property in $InputObject.PSObject.Properties) {
                $hash[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value
            }
            $hash
        } else {
            $InputObject
        }
    }
}

function Test-SqlPasswordComplexity {
    param (
        [string]$Username,    
        [string]$Password
    )

    # Check if the username is present in the password
    if ($Password -match $Username) {
        throw "SQL password cannot contain the SQL username."
        return $false
    }

    # Password must be minimum 8 characters, contains at least one uppercase, lowercase letter, contains at least one digit, contains at least one special character
    $regex = '^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\da-zA-Z]).{8,}$'
    if ($Password -match $regex) {
        Write-Host "SQL password is valid." -ForegroundColor Green
        return $true
    } else {
        throw "Password does not meet the complexity requirements."
        return $false
    }
}

$ErrorActionPreference = "Stop"

#region Deployment environment settings

$lastDeploymentStatePath = ".\last-deployment-state.json"
$deploymentOptions = @{}
$silentDeploy = $false

# Check if silent deployment settings file exists
if(-not([string]::IsNullOrEmpty($SilentDeploymentSettingsPath)) -and (Test-Path -Path $SilentDeploymentSettingsPath))
{
    $silentDeploy = $true
    # Get the deployment details from the silent deployment settings file
    $silentDepOptions = Get-Content -Path $SilentDeploymentSettingsPath | ConvertFrom-Json
    Write-Host "Silent deployment options found." -ForegroundColor Green
    $silentDepOptions = ConvertTo-Hashtable -InputObject $silentDepOptions
    $silentDepOptions.Keys | ForEach-Object {
        $deploymentOptions[$_] = $silentDepOptions[$_]
    }

    # Validate the silent deployment settings
    if (-not($deploymentOptions["SubscriptionId"]))
    {
        throw "SubscriptionId is required for silent deployment."
    }
    if (-not($deploymentOptions["NamePrefix"]))
    {
        throw "NamePrefix is required for silent deployment. Set to 'EmptyNamePrefix' to use own naming convention and specify the needed resource names."
    }
    if ($deploymentOptions["NamePrefix"].Length -gt 21) {
        throw "Name prefix length is larger than the 21 characters limit ($($deploymentOptions["NamePrefix"]))"
    }
    if ($deploymentOptions["NamePrefix"] -eq "EmptyNamePrefix")
    {
        if (-not($deploymentOptions["ResourceGroupName"]))
        {
            throw "ResourceGroupName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
        }
        if (-not($deploymentOptions["StorageAccountName"]))
        {
            throw "StorageAccountName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
        }
        if (-not($deploymentOptions["AutomationAccountName"]))
        {
            throw "AutomationAccountName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
        }
        if (-not($deploymentOptions["SqlServerName"]))
        {
            throw "SqlServerName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
        }
        if (-not($deploymentOptions["SqlDatabaseName"]))
        {
            throw "SqlDatabaseName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'."
        }
    }
    if (-not($deploymentOptions["WorkspaceReuse"]) -or ($deploymentOptions["WorkspaceReuse"] -ne "y" -and $deploymentOptions["WorkspaceReuse"] -ne "n"))
    {
        throw "WorkspaceReuse set to 'y' or 'n' is required for silent deployment."
    }
    if ($deploymentOptions["WorkspaceReuse"] -eq "y")
    {
        if (-not($deploymentOptions["WorkspaceName"]))
        {
            throw "WorkspaceName is required for silent deployment when WorkspaceReuse is set to 'y'."
        }
        if (-not($deploymentOptions["WorkspaceResourceGroupName"]))
        {
            throw "WorkspaceResourceGroupName is required for silent deployment when WorkspaceReuse is set to 'y'."
        }
    }
    if (-not($deploymentOptions["DeployWorkbooks"]) -or ($deploymentOptions["DeployWorkbooks"] -ne "y" -and $deploymentOptions["DeployWorkbooks"] -ne "n"))
    {
        throw "DeployWorkbooks set to 'y' or 'n' is required for silent deployment."
    }
    if (-not($deploymentOptions["SqlAdmin"]))
    {
        throw "SqlAdmin is required for silent deployment."
    }
    if (-not($deploymentOptions["SqlPass"]))
    {
        throw "SqlPass is required for silent deployment."
    }
    if (-not($deploymentOptions["TargetLocation"]))
    {
        throw "TargetLocation is required for silent deployment."
    }
    if (-not($deploymentOptions["DeployBenefitsUsageDependencies"]))
    {
        throw "DeployBenefitsUsageDependencies is required for silent deployment."
    }
    if ($deploymentOptions["DeployBenefitsUsageDependencies"] -eq "y")
    {
        if (-not($deploymentOptions["CustomerType"]))
        {
            throw "CustomerType is required for silent deployment when DeployBenefitsUsageDependencies is set to 'y'."
        }
        if (-not($deploymentOptions["BillingAccountId"]))
        {
            throw "BillingAccountId is required for silent deployment when DeployBenefitsUsageDependencies is set to 'y'."
        }
        if (-not($deploymentOptions["CurrencyCode"]))
        {
            throw "CurrencyCode is required for silent deployment when DeployBenefitsUsageDependencies is set to 'y'."
        }
        if ($deploymentOptions["CustomerType"] -eq "MCA")
        {
            if (-not($deploymentOptions["BillingProfileId"]))
            {
                throw "BillingProfileId is required for silent deployment when CustomerType is set to 'MCA'."
            }
        }
    }
}

if ((Test-Path -Path $lastDeploymentStatePath) -and !$silentDeploy)
{
    $depOptions = Get-Content -Path $lastDeploymentStatePath | ConvertFrom-Json
    Write-Host $depOptions -ForegroundColor Green
    $depOptionsReuse = Read-Host "Found last deployment options above. Do you want to repeat/upgrade last deployment (Y/N)?"
    if ("Y", "y" -contains $depOptionsReuse)
    {
        foreach ($property in $depOptions.PSObject.Properties)
        {
            $deploymentOptions[$property.Name] = $property.Value
        }    
    }
}

$GitHubOriginalUri = "https://raw.githubusercontent.com/helderpinto/AzureOptimizationEngine/master/azuredeploy.bicep"

if ([string]::IsNullOrEmpty($TemplateUri)) {
    $TemplateUri = $GitHubOriginalUri
}

$isTemplateAvailable = $false

try {
    Invoke-WebRequest -Uri $TemplateUri | Out-Null
    $isTemplateAvailable = $true
}
catch {
    Write-Host "The template URL ($TemplateUri) is not available. Please, put it in a publicly accessible HTTPS location." -ForegroundColor Red
}

if (!$isTemplateAvailable) {
    throw "Terminating due to template unavailability."
}

if (-not((Test-Path -Path "./azuredeploy.bicep") -and (Test-Path -Path "./azuredeploy-nested.bicep"))) {
    throw "Terminating due to template unavailability. Please, change directory to where azuredeploy.bicep and azuredeploy-nested.bicep are located."
}

$cloudDetails = Get-AzEnvironment -Name $AzureEnvironment

$ctx = Get-AzContext
if (-not($ctx)) {
    Connect-AzAccount -Environment $AzureEnvironment
    $ctx = Get-AzContext
}
else {
    if ($ctx.Environment.Name -ne $AzureEnvironment) {
        Disconnect-AzAccount -ContextName $ctx.Name
        Connect-AzAccount -Environment $AzureEnvironment
        $ctx = Get-AzContext
    }
}

#endregion

#region Azure subscription choice

Write-Host "Getting Azure subscriptions..." -ForegroundColor Yellow
$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" -and $_.SubscriptionPolicies.QuotaId -notlike "AAD*" }

if ($subscriptions.Count -gt 1) {

    $selectedSubscription = -1
    for ($i = 0; $i -lt $subscriptions.Count; $i++)
    {
        if (-not($deploymentOptions["SubscriptionId"]))
        {
            Write-Output "[$i] $($subscriptions[$i].Name)"    
        }
        else
        {
            if ($subscriptions[$i].Id -eq $deploymentOptions["SubscriptionId"])
            {
                $selectedSubscription = $i
                break
            }
        }
    }
    if (-not($deploymentOptions["SubscriptionId"]))
    {
        $lastSubscriptionIndex = $subscriptions.Count - 1
        while ($selectedSubscription -lt 0 -or $selectedSubscription -gt $lastSubscriptionIndex) {
            Write-Output "---"
            $selectedSubscription = [int] (Read-Host "Please, select the target subscription for this deployment [0..$lastSubscriptionIndex]")
        }    
    }
    if ($selectedSubscription -eq -1)
    {
        throw "The selected subscription does not exist. Check if you are logged in with the right Microsoft Entra ID user."        
    }
}
else
{
    if ($subscriptions.Count -ne 0)
    {
        $selectedSubscription = 0
    }
    else
    {
        throw "No valid subscriptions found. Only EA, MCA, PAYG or MSDN subscriptions are supported currently."
    }
}

if ($subscriptions.Count -eq 0) {
    throw "No subscriptions found. Check if you are logged in with the right Microsoft Entra ID account."
}

$subscriptionId = $subscriptions[$selectedSubscription].Id

if (-not($deploymentOptions["SubscriptionId"]))
{
    $deploymentOptions["SubscriptionId"] = $subscriptionId
}

if ($ctx.Subscription.Id -ne $subscriptionId) {
    $ctx = Select-AzSubscription -SubscriptionId $subscriptionId
}

#endregion

#region Resource naming options
if($silentDeploy)
{
    $workspaceReuse = $deploymentOptions["WorkspaceReuse"]
}
else { 
    $workspaceReuse = $null 
}

$deploymentNameTemplate = "{0}" + (Get-Date).ToString("yyMMddHHmmss")
$resourceGroupNameTemplate = "{0}-rg"
$storageAccountNameTemplate = "{0}sa"
$laWorkspaceNameTemplate = "{0}-la"
$automationAccountNameTemplate = "{0}-auto"
$sqlServerNameTemplate = "{0}-sql"

$nameAvailable = $true
if (-not($deploymentOptions["NamePrefix"]))
{
    do
    {
        $namePrefix = Read-Host "Please, enter a unique name prefix for the deployment (max. 21 chars) or existing prefix if updating deployment. If you want instead to individually name all resources, just press ENTER"
        if (-not($namePrefix))
        {
            $namePrefix = "EmptyNamePrefix"
        }
    } 
    while ($namePrefix.Length -gt 21)
    $deploymentOptions["NamePrefix"] = $namePrefix
}
else {
    if ($deploymentOptions["NamePrefix"] -eq "EmptyNamePrefix")
    {
        $namePrefix = $null
    }
    else
    {
        $namePrefix = $deploymentOptions["NamePrefix"]            
    }
}

if (-not($deploymentOptions["WorkspaceReuse"]))
{
    if ($null -eq $workspaceReuse) {
        $workspaceReuse = Read-Host "Are you going to reuse an existing Log Analytics workspace (Y/N)?"
    }
    $deploymentOptions["WorkspaceReuse"] = $workspaceReuse
}
else
{
    $workspaceReuse = $deploymentOptions["WorkspaceReuse"]
}

if (-not($deploymentOptions["ResourceGroupName"]))
{
    if ([string]::IsNullOrEmpty($namePrefix) -or $namePrefix -eq "EmptyNamePrefix") {
        $resourceGroupName = Read-Host "Please, enter the new or existing Resource Group for this deployment"
        $deploymentName = $deploymentNameTemplate -f $resourceGroupName
        $storageAccountName = Read-Host "Enter the Storage Account name"
        $automationAccountName = Read-Host "Automation Account name"
        $sqlServerName = Read-Host "Azure SQL Server name"
        $sqlDatabaseName = Read-Host "Azure SQL Database name"
        if ("N", "n" -contains $workspaceReuse) {
            $laWorkspaceName = Read-Host "Log Analytics Workspace"
        }
    }
    else {
        $deploymentName = $deploymentNameTemplate -f $namePrefix
        $resourceGroupName = $resourceGroupNameTemplate -f $namePrefix
        $storageAccountName = $storageAccountNameTemplate -f $namePrefix
        $automationAccountName = $automationAccountNameTemplate -f $namePrefix
        $sqlServerName = $sqlServerNameTemplate -f $namePrefix                    
        if ("Y", "y" -contains $workspaceReuse -and $silentDeploy) {
            $laWorkspaceName = $deploymentOptions["WorkspaceName"]
        }
        else {
            $laWorkspaceName = $laWorkspaceNameTemplate -f $namePrefix
        }
        $sqlDatabaseName = "azureoptimization"
    }

    $deploymentOptions["ResourceGroupName"] = $resourceGroupName
    $deploymentOptions["StorageAccountName"] = $storageAccountName
    $deploymentOptions["AutomationAccountName"] = $automationAccountName
    $deploymentOptions["SqlServerName"] = $sqlServerName
    $deploymentOptions["SqlDatabaseName"] = $sqlDatabaseName
    $deploymentOptions["WorkspaceName"] = $laWorkspaceName
}
else
{
    # With a silent deploy, overrule any custom resource naming if a NamePrefix is provided
    if($silentDeploy -and ![string]::IsNullOrEmpty($namePrefix) -and $namePrefix -ne "EmptyNamePrefix")
    {
        $deploymentName = $deploymentNameTemplate -f $namePrefix
        $resourceGroupName = $resourceGroupNameTemplate -f $namePrefix
        $storageAccountName = $storageAccountNameTemplate -f $namePrefix
        $automationAccountName = $automationAccountNameTemplate -f $namePrefix
        $sqlServerName = $sqlServerNameTemplate -f $namePrefix
        if ("Y", "y" -contains $workspaceReuse) {
            $laWorkspaceName = $deploymentOptions["WorkspaceName"]
        }
        else {
            $laWorkspaceName = $laWorkspaceNameTemplate -f $namePrefix
        }
        $sqlDatabaseName = "azureoptimization"
    }
    else {
        $resourceGroupName = $deploymentOptions["ResourceGroupName"]
        $storageAccountName = $deploymentOptions["StorageAccountName"]
        $automationAccountName = $deploymentOptions["AutomationAccountName"]
        $sqlServerName = $deploymentOptions["SqlServerName"]
        $sqlDatabaseName = $deploymentOptions["SqlDatabaseName"]        
        $laWorkspaceName = $deploymentOptions["WorkspaceName"]        
        $deploymentName = $deploymentNameTemplate -f $resourceGroupName
    }
}
#endregion

#region Resource naming availability checks
Write-Host "Checking name prefix availability..." -ForegroundColor Green

Write-Host "...for the Storage Account..." -ForegroundColor Green
$sa = Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName -ErrorAction SilentlyContinue
if ($null -eq $sa) {
    $saNameResult = Get-AzStorageAccountNameAvailability -Name $storageAccountName
    if (-not($saNameResult.NameAvailable)) {
        $nameAvailable = $false
        Write-Host "$($saNameResult.Message)" -ForegroundColor Red
    }    
}
else {
    Write-Host "(The Storage Account was already deployed)" -ForegroundColor Green
}

if ("N", "n" -contains $workspaceReuse) {
    Write-Host "...for the Log Analytics workspace..." -ForegroundColor Green

    $logAnalyticsReuse = $false
    $laWorkspaceResourceGroup = $resourceGroupName

    $la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $resourceGroupName -Name $laWorkspaceName -ErrorAction SilentlyContinue
    if ($null -eq $la) {
        $laNameResult = Invoke-WebRequest -Uri "https://portal.loganalytics.io/api/workspaces/IsWorkspaceExists?name=$laWorkspaceName"
        if ($laNameResult.Content -eq "true") {
            $nameAvailable = $false
            Write-Host "The Log Analytics workspace $laWorkspaceName is already taken." -ForegroundColor Red
        }
    }
    else {
        Write-Host "(The Log Analytics Workspace was already deployed)" -ForegroundColor Green
    }
}
else {
    $logAnalyticsReuse = $true
}

Write-Host "...for the Azure SQL Server..." -ForegroundColor Green
$sql = Get-AzSqlServer -ResourceGroupName $resourceGroupName -Name $sqlServerName -ErrorAction SilentlyContinue
if ($null -eq $sql -and -not($sqlServerName -like "*.database.*") -and -not($IgnoreNamingAvailabilityErrors)) {

    $SqlServerNameAvailabilityUriPath = "/subscriptions/$subscriptionId/providers/Microsoft.Sql/checkNameAvailability?api-version=2014-04-01"
    $body = "{`"name`": `"$sqlServerName`", `"type`": `"Microsoft.Sql/servers`"}"
    $sqlNameResult = (Invoke-AzRestMethod -Path $SqlServerNameAvailabilityUriPath -Method POST -Payload $body).Content | ConvertFrom-Json
    
    if (-not($sqlNameResult.available)) {
        $nameAvailable = $false
        Write-Host "$($sqlNameResult.message) ($sqlServerName)" -ForegroundColor Red
    }
}
else {
    Write-Host "(The SQL Server was already deployed)" -ForegroundColor Green
}

if (-not($nameAvailable) -and -not($IgnoreNamingAvailabilityErrors))
{
    throw "Please, fix naming issues. Terminating execution."
}

Write-Host "Chosen resource names are available for all services" -ForegroundColor Green
#endregion

#region Additional resource options (LA reused, region, SQL user)
if (-not($deploymentOptions["WorkspaceResourceGroupName"]))
{
    if ("Y", "y" -contains $workspaceReuse) {
        $laWorkspaceName = Read-Host "Please, enter the name of the Log Analytics workspace to be reused"
        $laWorkspaceResourceGroup = Read-Host "Please, enter the name of the resource group containing Log Analytics $laWorkspaceName"
        $la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $laWorkspaceResourceGroup -Name $laWorkspaceName -ErrorAction SilentlyContinue
        if (-not($la)) {
            throw "Could not find $laWorkspaceName in resource group $laWorkspaceResourceGroup for the chosen subscription. Aborting."
        }        
        $deploymentOptions["WorkspaceName"] = $laWorkspaceName
        $deploymentOptions["WorkspaceResourceGroupName"] = $laWorkspaceResourceGroup
    }    
}
else
{
    $laWorkspaceResourceGroup = $deploymentOptions["WorkspaceResourceGroupName"]
}

$rg = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue 

if (-not($deploymentOptions["TargetLocation"]))
{
    if (-not($rg.Location)) {
        Write-Host "Getting Azure locations..." -ForegroundColor Green
        $locations = Get-AzLocation | Where-Object { $_.Providers -contains "Microsoft.Automation" -and $_.Providers -contains "Microsoft.Sql" `
                                                        -and $_.Providers -contains "Microsoft.OperationalInsights" `
                                                        -and $_.Providers -contains "Microsoft.Storage"} | Sort-Object -Property Location
        
        for ($i = 0; $i -lt $locations.Count; $i++) {
            Write-Output "[$i] $($locations[$i].location)"    
        }
        $selectedLocation = -1
        $lastLocationIndex = $locations.Count - 1
        while ($selectedLocation -lt 0 -or $selectedLocation -gt $lastLocationIndex) {
            Write-Output "---"
            $selectedLocation = [int] (Read-Host "Please, select the target location for this deployment [0..$lastLocationIndex]")
        }
        
        $targetLocation = $locations[$selectedLocation].location    
    }
    else {
        $targetLocation = $rg.Location    
    }
    
    $deploymentOptions["TargetLocation"] = $targetLocation
}
else
{
    $targetLocation = $deploymentOptions["TargetLocation"]    
}

if (-not($deploymentOptions["SqlAdmin"]))
{
    $sqlAdmin = Read-Host "Please, input the SQL Admin username"
    $deploymentOptions["SqlAdmin"] = $sqlAdmin
}
else
{
    $sqlAdmin = $deploymentOptions["SqlAdmin"]    
}
if (-not($deploymentOptions["SqlPass"]))
{
    $sqlPass = Read-Host "Please, input the SQL Admin ($sqlAdmin) password" -AsSecureString
}
else
{
    $sqlPass = $deploymentOptions["SqlPass"]
    if(Test-SqlPasswordComplexity -Username $sqlAdmin -Password $sqlPass -ErrorAction SilentlyContinue)
    {
        Write-Host "Password complexity check passed" -ForegroundColor Green
        $sqlPass = ConvertTo-SecureString -AsPlainText $sqlPass -Force
    }
    else
    {
        throw "SQL password complexity check failed. Please, fix the password and try again."
    }
}
#endregion

#region Partial upgrade dependent resource checks
if (-not($DoPartialUpgrade))
{
    $upgrading = $false
}
else
{
    $upgrading = $true

    if ($null -ne $rg)
    {
        if ($upgrading -and $null -ne $sa) 
        {
            $containers = Get-AzStorageContainer -Context $sa.Context
        }
        else
        {
            $upgrading = $false    
            Write-Host "Did not find the $storageAccountName Storage Account." -ForegroundColor Yellow
        }
    
        if ($upgrading -and $null -ne $sql)
        {
            $databases = Get-AzSqlDatabase -ServerName $sql.ServerName -ResourceGroupName $resourceGroupName
            if (-not($databases | Where-Object { $_.DatabaseName -eq $sqlDatabaseName}))
            {
                $upgrading = $false
                Write-Host "Did not find the $sqlDatabaseName database." -ForegroundColor Yellow
            }
        }
        else
        {
            if (-not($IgnoreNamingAvailabilityErrors))
            {
                $upgrading = $false    
                Write-Host "Did not find the $sqlServerName SQL Server." -ForegroundColor Yellow    
            }
        }
    
        $auto = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName -Name $automationAccountName -ErrorAction SilentlyContinue
        if ($null -ne $auto)
        {
            $runbooks = Get-AzAutomationRunbook -ResourceGroupName $resourceGroupName `
                -AutomationAccountName $auto.AutomationAccountName | Where-Object { $_.Name.StartsWith('Export') }
            if ($runbooks.Count -lt 3)
            {
                $upgrading = $false    
                Write-Host "Did not find existing runbooks in the $automationAccountName Automation Account." -ForegroundColor Yellow
            }
        }
        else
        {
            $upgrading = $false    
            Write-Host "Did not find the $automationAccountName Automation Account." -ForegroundColor Yellow
        }
    }
    else
    {
        $upgrading = $false    
    }        
}
#endregion

$deploymentMessage = "Deploying Azure Optimization Engine to subscription"
if ($upgrading)
{
    Write-Host "Looks like this deployment was already done in the past. We will only upgrade runbooks, modules, schedules, variables, storage and the database." -ForegroundColor Yellow
    $deploymentMessage = "Upgrading Azure Optimization Engine in subscription"
}

if ($silentDeploy)
{
    $continueInput = "Y"
}
else
{
    $continueInput = Read-Host "$deploymentMessage $($subscriptions[$selectedSubscription].Name). Continue (Y/N)?"
}
if ("Y", "y" -contains $continueInput) {

    # If we deploy silently, be sure to strip the SQL password from the output
    if ($silentDeploy)
    {
        $deploymentOptions.Remove("SqlPass")
    }
    $deploymentOptions | ConvertTo-Json | Out-File -FilePath $lastDeploymentStatePath -Force
    #region Computing schedules base time
    $baseTime = (Get-Date).ToUniversalTime().ToString("u")
    $upgradingSchedules = $false
    $schedules = Get-AzAutomationSchedule -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -ErrorAction SilentlyContinue
    if ($schedules.Count -gt 0) {
        $upgradingSchedules = $true
        $originalBaseTime = ($schedules | Where-Object { $_.Name.EndsWith("Weekly") } | Sort-Object -Property StartTime | Select-Object -First 1).StartTime.AddHours(-1.25).DateTime
        $now = (Get-Date).ToUniversalTime()
        $diff = $now.AddHours(-1.25) - $originalBaseTime
        $nextWeekDays = [Math]::Ceiling($diff.TotalDays / 7) * 7
        $baseTime = $now.AddHours(-1.25).AddDays($nextWeekDays - $diff.TotalDays).ToString("u")
        Write-Host "Existing schedules found. Keeping original base time: $baseTime." -ForegroundColor Green
    }
    else {
        Write-Host "Automation schedules base time automatically set to $baseTime." -ForegroundColor Green
    }
    #endregion

    if (-not($upgrading))
    {
        #region Template-based deployment
        $jobSchedules = Get-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -ErrorAction SilentlyContinue
        if ($jobSchedules.Count -gt 0) {
            Write-Host "Unregistering previous runbook schedules associations from $automationAccountName..." -ForegroundColor Green
            foreach ($jobSchedule in $jobSchedules) {
                if ($jobSchedule.ScheduleName.StartsWith("AzureOptimization")) {
                    Unregister-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                        -JobScheduleId $jobSchedule.JobScheduleId -Force
                }
            }    
        }
    
        Write-Host "Deploying Azure Optimization Engine resources..." -ForegroundColor Green
        $deploymentTries = 0
        $maxDeploymentTries = 2
        $deploymentSucceeded = $false
        do {
            $deploymentTries++
            try {
                $deployment = New-AzDeployment -TemplateFile ".\azuredeploy.bicep" -templateLocation $TemplateUri.Replace("azuredeploy.bicep", "") -Location $targetLocation -rgName $resourceGroupName -Name $deploymentName `
                    -projectLocation $targetlocation -logAnalyticsReuse $logAnalyticsReuse -baseTime $baseTime `
                    -logAnalyticsWorkspaceName $laWorkspaceName -logAnalyticsWorkspaceRG $laWorkspaceResourceGroup `
                    -storageAccountName $storageAccountName -automationAccountName $automationAccountName `
                    -sqlServerName $sqlServerName -sqlDatabaseName $sqlDatabaseName -cloudEnvironment $AzureEnvironment `
                    -sqlAdminLogin $sqlAdmin -sqlAdminPassword $sqlPass -resourceTags $ResourceTags -WarningAction SilentlyContinue
                $deploymentSucceeded = $true
            }
            catch {
                if ($deploymentTries -ge $maxDeploymentTries) {
                    Write-Host "Failed deployment. Stop trying." -ForegroundColor Yellow
                    throw $_
                }
                Write-Host "Failed deployment. Trying once more..." -ForegroundColor Yellow
            }            
        } while (-not($deploymentSucceeded) -and $deploymentTries -lt $maxDeploymentTries)

        $spnId = $deployment.Outputs['automationPrincipalId'].Value 
        #endregion
    }
    else
    {
        #region Partial upgrade deployment
        $upgradeManifest = Get-Content -Path "./upgrade-manifest.json" | ConvertFrom-Json
        Write-Host "Creating missing storage account containers..." -ForegroundColor Green
        $upgradeContainers = $upgradeManifest.dataCollection.container
        foreach ($container in $upgradeContainers)
        {
            if (-not($container -in $containers.Name))
            {
                New-AzStorageContainer -Name $container -Context $sa.Context -Permission Off | Out-Null
                Write-Host "$container container created."
            }
        }

        Write-Host "Importing runbooks..." -ForegroundColor Green
        $allRunbooks = $upgradeManifest.baseIngest.runbook + $upgradeManifest.dataCollection.runbook + $upgradeManifest.recommendations.runbook + $upgradeManifest.remediations.runbook
        $runbookBaseUri = $TemplateUri.Replace("azuredeploy.bicep", "")
        $topTemplateJson = "{ `"`$schema`": `"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#`", " + `
            "`"contentVersion`": `"1.0.0.0`", `"resources`": ["
        $bottomTemplateJson = "] }"
        $runbookDeploymentTemplateJson = $topTemplateJson
        for ($i = 0; $i -lt $allRunbooks.Count; $i++)
        {
            try {
                Invoke-WebRequest -Uri ($runbookBaseUri + $allRunbooks[$i].name) | Out-Null
                $runbookName = [System.IO.Path]::GetFilenameWithoutExtension($allRunbooks[$i].name)
                $runbookJson = "{ `"name`": `"$automationAccountName/$runbookName`", `"type`": `"Microsoft.Automation/automationAccounts/runbooks`", " + `
                "`"apiVersion`": `"2018-06-30`", `"location`": `"$targetLocation`", `"tags`": $($ResourceTags | ConvertTo-Json), `"properties`": { " + `
                "`"runbookType`": `"PowerShell`", `"logProgress`": false, `"logVerbose`": false, " + `
                "`"publishContentLink`": { `"uri`": `"$runbookBaseUri$($allRunbooks[$i].name)`", `"version`": `"$($allRunbooks[$i].version)`" } } }"
                $runbookDeploymentTemplateJson += $runbookJson
                if ($i -lt $allRunbooks.Count - 1)
                {
                    $runbookDeploymentTemplateJson += ", "
                }
                Write-Host "$($allRunbooks[$i].name) imported."
            }
            catch {
                Write-Host "$($allRunbooks[$i].name) not imported (not found)." -ForegroundColor Yellow
            }
        }
        $runbookDeploymentTemplateJson += $bottomTemplateJson
        $runbooksTemplatePath = "./aoe-runbooks-deployment.json"
        $runbookDeploymentTemplateJson | Out-File -FilePath $runbooksTemplatePath -Force
        Write-Host "Executing runbooks deployment..." -ForegroundColor Green
        New-AzResourceGroupDeployment -TemplateFile $runbooksTemplatePath -ResourceGroupName $resourceGroupName -Name ($deploymentNameTemplate -f "runbooks") | Out-Null
        Remove-Item -Path $runbooksTemplatePath -Force
        Write-Host "Runbooks update deployed."

        Write-Host "Importing modules..." -ForegroundColor Green
        $allModules = $upgradeManifest.modules
        $modulesDeploymentTemplateJson = $topTemplateJson
        for ($i = 0; $i -lt $allModules.Count; $i++)
        {
            $moduleJson = "{ `"name`": `"$automationAccountName/$($allModules[$i].name)`", `"type`": `"Microsoft.Automation/automationAccounts/modules`", " + `
                "`"apiVersion`": `"2018-06-30`", `"location`": `"$targetLocation`", `"tags`": $($ResourceTags | ConvertTo-Json), `"properties`": { " + `
                "`"contentLink`": { `"uri`": `"$($allModules[$i].url)`" } } "
            if ($allModules[$i].name -ne "Az.Accounts" -and $allModules[$i].name -ne "Microsoft.Graph.Authentication")
            {
                $moduleJson += ", `"dependsOn`": [ `"Az.Accounts`", `"Microsoft.Graph.Authentication`" ]"
            }
            $moduleJson += "}"
            $modulesDeploymentTemplateJson += $moduleJson
            if ($i -lt $allModules.Count - 1)
            {
                $modulesDeploymentTemplateJson += ", "
            }
            Write-Host "$($allModules[$i].name) imported."
        }
        $modulesDeploymentTemplateJson += $bottomTemplateJson
        $modulesTemplatePath = "./aoe-modules-deployment.json"
        $modulesDeploymentTemplateJson | Out-File -FilePath $modulesTemplatePath -Force
        Write-Host "Executing modules deployment..." -ForegroundColor Green
        New-AzResourceGroupDeployment -TemplateFile $modulesTemplatePath -ResourceGroupName $resourceGroupName -Name ($deploymentNameTemplate -f "modules") | Out-Null
        Remove-Item -Path $modulesTemplatePath -Force
        Write-Host "Modules update deployed."

        Write-Host "Updating schedules..." -ForegroundColor Green
        $allSchedules = $upgradeManifest.schedules

        $allScheduledRunbooks = Get-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName
        $exportHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Export") })[0].HybridWorker
        $ingestHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Ingest") })[0].HybridWorker
        $recommendHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Recommend") })[0].HybridWorker
        if ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Remediate") })
        {
            $remediateHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Remediate") })[0].HybridWorker
        }
        
        $hybridWorkerOption = "None"
        if ($exportHybridWorkerOption -or $ingestHybridWorkerOption -or $recommendHybridWorkerOption -or $remediateHybridWorkerOption) {
            $hybridWorkerOption = "Export: $exportHybridWorkerOption; Ingest: $ingestHybridWorkerOption; Recommend: $recommendHybridWorkerOption; Remediate: $remediateHybridWorkerOption"
        }      
        Write-Host "Current Hybrid Worker option: $hybridWorkerOption" -ForegroundColor Green            

        $dataIngestRunbookName = [System.IO.Path]::GetFileNameWithoutExtension(($upgradeManifest.baseIngest | Where-Object { $_.source -eq "dataCollection"}).runbook.name)
        $dataExportsToMultiSchedule = $upgradeManifest.dataCollection | Where-Object { $_.exportSchedules.Count -gt 0 }
        $recommendationsProcessingRunbooks = $upgradeManifest.baseIngest | Where-Object { $_.source -eq "recommendations" -or $_.source -eq "maintenance"}

        foreach ($schedule in $allSchedules)
        {
            if (-not($schedules | Where-Object { $_.Name -eq $schedule.name }))
            {
                $scheduleStartTime = (Get-Date $baseTime).Add([System.Xml.XmlConvert]::ToTimeSpan($schedule.offset))
                $scheduleNow = (Get-Date).ToUniversalTime()

                if ($schedule.frequency -eq "Hour")
                {
                    if ($scheduleNow.AddMinutes(5) -gt $scheduleStartTime)
                    {
                        $hoursDiff = ($scheduleNow - $scheduleStartTime).Hours + 1
                        $scheduleStartTime = $scheduleStartTime.AddHours($hoursDiff)
                    }

                    New-AzAutomationSchedule -Name $schedule.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
                        -StartTime $scheduleStartTime -HourInterval 1 | Out-Null
                }
                if ($schedule.frequency -eq "Day")
                {
                    if ($scheduleNow.AddMinutes(5) -gt $scheduleStartTime)
                    {
                        $scheduleStartTime = $scheduleStartTime.AddDays(1)
                    }

                    New-AzAutomationSchedule -Name $schedule.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
                        -StartTime $scheduleStartTime -DayInterval 1 | Out-Null
                }
                if ($schedule.frequency -eq "Week")
                {
                    if ($scheduleNow.AddMinutes(5) -gt $scheduleStartTime)
                    {
                        $scheduleStartTime = $scheduleStartTime.AddDays(7)
                    }

                    New-AzAutomationSchedule -Name $schedule.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
                        -StartTime $scheduleStartTime -WeekInterval 1 | Out-Null
                }
                Write-Host "$($schedule.name) schedule created."
            }

            $scheduledRunbooks = Get-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                -ScheduleName $schedule.name

            $dataExportsToSchedule = ($upgradeManifest.dataCollection + $upgradeManifest.recommendations) | Where-Object { $_.exportSchedule -eq $schedule.name }
            foreach ($dataExport in $dataExportsToSchedule)
            {
                $runbookName = [System.IO.Path]::GetFileNameWithoutExtension($dataExport.runbook.name)
                $runbookType = $runbookName.Split("-")[0]
                switch ($runbookType)
                {
                    "Export" {
                        $hybridWorkerName = $exportHybridWorkerOption
                    }
                    "Recommend" {
                        $hybridWorkerName = $recommendHybridWorkerOption
                    }
                    "Ingest" {
                        $hybridWorkerName = $ingestHybridWorkerOption
                    }
                    "Remediate" {
                        $hybridWorkerName = $remediateHybridWorkerOption
                    }
                    Default {
                        $hybridWorkerName = $null
                    }
                }

                if (-not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $runbookName}))
                {
                    if ($hybridWorkerName)
                    {
                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                            -RunbookName $runbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName | Out-Null
                    }
                    else
                    {
                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                            -RunbookName $runbookName -ScheduleName $schedule.name | Out-Null                        
                    }
                    Write-Host "Added $($schedule.name) schedule to $hybridWorkerName $runbookName runbook"
                }
            }

            foreach ($dataExport in $dataExportsToMultiSchedule)
            {
                $exportSchedule = $dataExport.exportSchedules | Where-Object { $_.schedule -eq $schedule.name }
                if ($exportSchedule)
                {
                    $runbookName = [System.IO.Path]::GetFileNameWithoutExtension($dataExport.runbook.name)
                    $runbookType = $runbookName.Split("-")[0]
                    switch ($runbookType)
                    {
                        "Export" {
                            $hybridWorkerName = $exportHybridWorkerOption
                        }
                        "Recommend" {
                            $hybridWorkerName = $recommendHybridWorkerOption
                        }
                        "Ingest" {
                            $hybridWorkerName = $ingestHybridWorkerOption
                        }
                        "Remediate" {
                            $hybridWorkerName = $remediateHybridWorkerOption
                        }
                        Default {
                            $hybridWorkerName = $null
                        }
                    }
                    
                    if (-not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $runbookName -and $_.ScheduleName -eq $schedule.name}))
                    {   
                        $params = @{}
                        $exportSchedule.parameters.PSObject.Properties | ForEach-Object {
                            $params[$_.Name] = $_.Value
                        }                                
    
                        if ($hybridWorkerName)
                        {
                            Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                                -RunbookName $runbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName -Parameters $params | Out-Null
                        }
                        else
                        {
                            Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                                -RunbookName $runbookName -ScheduleName $schedule.name -Parameters $params | Out-Null                        
                        }
                        Write-Host "Added $($schedule.name) schedule to $hybridWorkerName $runbookName runbook."
                    }    
                }
            }

            $dataIngestToSchedule = $upgradeManifest.dataCollection | Where-Object { $_.ingestSchedule -eq $schedule.name }
            foreach ($dataIngest in $dataIngestToSchedule)
            {
                $hybridWorkerName = $ingestHybridWorkerOption
    
                if (-not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $dataIngestRunbookName}))
                {
                    $params = @{"StorageSinkContainer"=$dataIngest.container}

                    if ($hybridWorkerName)
                    {
                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                            -RunbookName $dataIngestRunbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName -Parameters $params | Out-Null
                    }
                    else
                    {
                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                            -RunbookName $dataIngestRunbookName -ScheduleName $schedule.name -Parameters $params | Out-Null                        
                    }
                    Write-Host "Added $($schedule.name) schedule to $hybridWorkerName $dataIngestRunbookName runbook."
                }
            }

            foreach ($recommendationsProcessingRunbook in $recommendationsProcessingRunbooks)
            {
                $runbookName = [System.IO.Path]::GetFileNameWithoutExtension($recommendationsProcessingRunbook.runbook.name)
                $hybridWorkerName = $ingestHybridWorkerOption
    
                if ($recommendationsProcessingRunbook.schedule -eq $schedule.name -and -not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $runbookName}))
                {
                    if ($hybridWorkerName)
                    {
                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                            -RunbookName $runbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName | Out-Null
                    }
                    else
                    {
                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                            -RunbookName $runbookName -ScheduleName $schedule.name | Out-Null                        
                    }
                    Write-Host "Added $($schedule.name) schedule to $hybridWorkerName $runbookName runbook."
                }
            }
        }

        Write-Host "Updating variables..." -ForegroundColor Green
        $allVariables = $upgradeManifest.dataCollection.requiredVariables + $upgradeManifest.recommendations.requiredVariables + $upgradeManifest.remediations.requiredVariables
        foreach ($variable in $allVariables)
        {
            $existingVariables = Get-AzAutomationVariable -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName
            if (-not($existingVariables | Where-Object { $_.Name -eq $variable.name }))
            {
                New-AzAutomationVariable -Name $variable.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
                    -Value $variable.defaultValue -Encrypted $false | Out-Null
                Write-Host "$($variable.name) variable created."
            }
        }

        Write-Host "Force-updating variables..." -ForegroundColor Green
        $forceUpdateVariables = $upgradeManifest.overwriteVariables
        foreach ($variable in $forceUpdateVariables)
        {
            Set-AzAutomationVariable -Name $variable.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
                -Value $variable.value -Encrypted $false | Out-Null
            Write-Host "$($variable.name) variable updated."
        }

        Write-Host "Removing deprecated runbooks..." -ForegroundColor Green
        $deprecatedRunbooks = $upgradeManifest.deprecatedRunbooks
        foreach ($deprecatedRunbook in $deprecatedRunbooks)
        {
            Remove-AzAutomationRunbook -AutomationAccountName $automationAccountName -Name $deprecatedRunbook -ResourceGroupName $resourceGroupName -Force -ErrorAction SilentlyContinue
        }
        #endregion
    }

    #region Schedules reset
    if ($upgradingSchedules) {
        $schedules = Get-AzAutomationSchedule -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName
        $dailySchedules = $schedules | Where-Object { $_.Frequency -eq "Day" -or $_.Frequency -eq "Hour" }
        Write-Host "Fixing daily schedules after upgrade..." -ForegroundColor Green
        foreach ($schedule in $dailySchedules) {
            $now = (Get-Date).ToUniversalTime()
            $newStartTime = [System.DateTimeOffset]::Parse($now.ToString("yyyy-MM-ddT00:00:00Z"))
            $newStartTime = $newStartTime.AddHours($schedule.StartTime.Hour).AddMinutes($schedule.StartTime.Minute)
            if ($newStartTime.AddMinutes(-5) -lt $now) {
                $newStartTime = $newStartTime.AddDays(1)
            }
            $expiryTime = $schedule.ExpiryTime.ToString("yyyy-MM-ddTHH:mm:ssZ")
            $startTime = $newStartTime.ToString("yyyy-MM-ddTHH:mm:ssZ")
            $automationPath = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/schedules/$($schedule.Name)?api-version=2015-10-31"
            $body = "{
                `"name`": `"$($schedule.Name)`",
                `"properties`": {
                  `"description`": `"$($schedule.Description)`",
                  `"startTime`": `"$startTime`",
                  `"expiryTime`": `"$expiryTime`",
                  `"interval`": 1,
                  `"frequency`": `"$($schedule.Frequency.ToString())`",
                  `"advancedSchedule`": {}
                }
              }"
            Invoke-AzRestMethod -Path $automationPath -Method PUT -Payload $body | Out-Null
        }
    }
    #endregion
    
    #region Deployment date Automation variable
    Write-Host "Checking Azure Automation variable referring to the initial Azure Optimization Engine deployment date..." -ForegroundColor Green
    $deploymentDateVariableName = "AzureOptimization_DeploymentDate"
    $deploymentDateVariable = Get-AzAutomationVariable -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name $deploymentDateVariableName -ErrorAction SilentlyContinue
    
    if ($null -eq $deploymentDateVariable) {
        $deploymentDate = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd")
        Write-Host "Setting initial deployment date ($deploymentDate)..." -ForegroundColor Green
        New-AzAutomationVariable -Name $deploymentDateVariableName -Description "The date of the initial engine deployment" `
            -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Value $deploymentDate -Encrypted $false
    }
    #endregion

    #region Open SQL Server firewall rule
    if (-not($sqlServerName -like "*.database.*"))
    {
        $myPublicIp = (Invoke-WebRequest -uri "https://ifconfig.me/ip").Content.Trim()
        if (-not($myPublicIp -like "*.*.*.*"))
        {
            $myPublicIp = (Invoke-WebRequest -uri "https://ipv4.icanhazip.com").Content.Trim()
            if (-not($myPublicIp -like "*.*.*.*"))
            {
                $myPublicIp = (Invoke-WebRequest -uri "https://ipinfo.io/ip").Content.Trim()
            }
        }

        Write-Host "Opening SQL Server firewall temporarily to your public IP ($myPublicIp)..." -ForegroundColor Green
        $tempFirewallRuleName = "InitialDeployment"            
        New-AzSqlServerFirewallRule -ResourceGroupName $resourceGroupName -ServerName $sqlServerName -FirewallRuleName $tempFirewallRuleName -StartIpAddress $myPublicIp -EndIpAddress $myPublicIp -ErrorAction Continue | Out-Null
    }
    #endregion
    
    #region SQL Database model deployment
    Write-Host "Deploying SQL Database model..." -ForegroundColor Green
    
    $sqlPassPlain = (New-Object PSCredential "user", $sqlPass).GetNetworkCredential().Password     
    if (-not($sqlServerName -like "*.database.*"))
    {
        $sqlServerEndpoint = "$sqlServerName$($cloudDetails.SqlDatabaseDnsSuffix)"
    }
    else 
    {
        $sqlServerEndpoint = $sqlServerName
    }
    $databaseName = $sqlDatabaseName
    $SqlTimeout = 60
    $tries = 0
    $connectionSuccess = $false
    do {
        $tries++
        try {
    
    
            $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
            $Conn.Open() 
    
            $createTableQuery = Get-Content -Path "./model/loganalyticsingestcontrol-table.sql"
            $Cmd = new-object system.Data.SqlClient.SqlCommand
            $Cmd.Connection = $Conn
            $Cmd.CommandTimeout = $SqlTimeout
            $Cmd.CommandText = $createTableQuery
            $Cmd.ExecuteReader()
            $Conn.Close()
    
            $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
            $Conn.Open() 
    
            $initTableQuery = Get-Content -Path "./model/loganalyticsingestcontrol-initialize.sql"
            $Cmd = new-object system.Data.SqlClient.SqlCommand
            $Cmd.Connection = $Conn
            $Cmd.CommandTimeout = $SqlTimeout
            $Cmd.CommandText = $initTableQuery
            $Cmd.ExecuteReader()
            $Conn.Close()
    
            $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
            $Conn.Open() 
    
            $upgradeTableQuery = Get-Content -Path "./model/loganalyticsingestcontrol-upgrade.sql"
            $Cmd = new-object system.Data.SqlClient.SqlCommand
            $Cmd.Connection = $Conn
            $Cmd.CommandTimeout = $SqlTimeout
            $Cmd.CommandText = $upgradeTableQuery
            $Cmd.ExecuteReader()
            $Conn.Close()
    
            $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
            $Conn.Open() 
    
            $createTableQuery = Get-Content -Path "./model/sqlserveringestcontrol-table.sql"
            $Cmd = new-object system.Data.SqlClient.SqlCommand
            $Cmd.Connection = $Conn
            $Cmd.CommandTimeout = $SqlTimeout
            $Cmd.CommandText = $createTableQuery
            $Cmd.ExecuteReader()
            $Conn.Close()

            $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
            $Conn.Open() 
    
            $initTableQuery = Get-Content -Path "./model/sqlserveringestcontrol-initialize.sql"
            $Cmd = new-object system.Data.SqlClient.SqlCommand
            $Cmd.Connection = $Conn
            $Cmd.CommandTimeout = $SqlTimeout
            $Cmd.CommandText = $initTableQuery
            $Cmd.ExecuteReader()
            $Conn.Close()

            $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
            $Conn.Open() 
    
            $createTableQuery = Get-Content -Path "./model/recommendations-table.sql"
            $Cmd = new-object system.Data.SqlClient.SqlCommand
            $Cmd.Connection = $Conn
            $Cmd.CommandTimeout = $SqlTimeout
            $Cmd.CommandText = $createTableQuery
            $Cmd.ExecuteReader()
            $Conn.Close()

            $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
            $Conn.Open() 
    
            $createTableQuery = Get-Content -Path "./model/recommendations-sp.sql"
            $Cmd = new-object system.Data.SqlClient.SqlCommand
            $Cmd.Connection = $Conn
            $Cmd.CommandTimeout = $SqlTimeout
            $Cmd.CommandText = $createTableQuery
            $Cmd.ExecuteReader()
            $Conn.Close()

            $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
            $Conn.Open() 
    
            $createTableQuery = Get-Content -Path "./model/filters-table.sql"
            $Cmd = new-object system.Data.SqlClient.SqlCommand
            $Cmd.Connection = $Conn
            $Cmd.CommandTimeout = $SqlTimeout
            $Cmd.CommandText = $createTableQuery
            $Cmd.ExecuteReader()
            $Conn.Close()

            $connectionSuccess = $true
        }
        catch {
            Write-Host "Failed to contact SQL at try $tries." -ForegroundColor Yellow
            Write-Host $Error[0] -ForegroundColor Yellow
            Start-Sleep -Seconds ($tries * 20)
        }    
    } while (-not($connectionSuccess) -and $tries -lt 3)
    
    if (-not($connectionSuccess)) {
        if (-not($sqlServerName -like "*.database.*"))
        {
            Write-Host "Deleting temporary SQL Server firewall rule..." -ForegroundColor Green
            Remove-AzSqlServerFirewallRule -FirewallRuleName $tempFirewallRuleName -ResourceGroupName $resourceGroupName -ServerName $sqlServerName -ErrorAction Continue | Out-Null
        }    
        throw "Could not establish connection to SQL."
    }
    #endregion
    
    #region Close SQL Server firewall rule
    if (-not($sqlServerName -like "*.database.*"))
    {
        Write-Host "Deleting temporary SQL Server firewall rule..." -ForegroundColor Green
        Remove-AzSqlServerFirewallRule -FirewallRuleName $tempFirewallRuleName -ResourceGroupName $resourceGroupName -ServerName $sqlServerName -ErrorAction Continue  | Out-Null
    }    
    #endregion

    #region Workbooks deployment
    if (-not($deploymentOptions["DeployWorkbooks"]))
    {
        $deployWorkbooks = Read-Host "Do you want to deploy the workbooks with additional insights (recommended)? (Y/N)"
    }
    else
    {
        $deployWorkbooks = $deploymentOptions["DeployWorkbooks"]
    }
    if ("Y", "y" -contains $deployWorkbooks) {
        $deploymentOptions["DeployWorkbooks"] = "Y"
        $deploymentOptions | ConvertTo-Json | Out-File -FilePath $lastDeploymentStatePath -Force
        Write-Host "Publishing workbooks..." -ForegroundColor Green
        $workbooks = Get-ChildItem -Path "./views/workbooks/" | Where-Object { $_.Name.EndsWith(".bicep") }
        $la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $laWorkspaceResourceGroup -Name $laWorkspaceName
        foreach ($workbook in $workbooks)
        {
            $workbookFileName = [System.IO.Path]::GetFileNameWithoutExtension($workbook.Name)
            Write-Host "Deploying $workbookFileName workbook..."
            try {
                New-AzResourceGroupDeployment -TemplateFile $workbook.FullName -ResourceGroupName $resourceGroupName -Name ($deploymentNameTemplate -f $workbookFileName) `
                    -workbookSourceId $la.ResourceId -resourceTags $ResourceTags -WarningAction SilentlyContinue | Out-Null        
            }
            catch {
                Write-Host "Failed to deploy the workbook. If you are upgrading AOE, please remove first the $workbookFileName workbook from the $laWorkspaceName Log Analytics workspace and then re-deploy." -ForegroundColor Yellow            
            }
        }
    }
    #endregion

    if (!$silentDeploy)
    {
        #region Grant Microsoft Entra ID role to AOE principal
        if ($null -eq $spnId)
        {
            $auto = Get-AzAutomationAccount -Name $automationAccountName -ResourceGroupName $resourceGroupName
            $spnId = $auto.Identity.PrincipalId
            if ($null -eq $spnId)
            {
                $runAsConnection = Get-AzAutomationConnection -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name AzureRunAsConnection -ErrorAction SilentlyContinue
                $runAsAppId = $runAsConnection.FieldDefinitionValues.ApplicationId
                if ($runAsAppId)
                {
                    $runAsServicePrincipal = Get-AzADServicePrincipal -ApplicationId $runAsAppId
                    $spnId = $runAsServicePrincipal.Id
                }
            }
        }

        try
        {
            Import-Module Microsoft.Graph.Authentication
            Import-Module Microsoft.Graph.Identity.DirectoryManagement

            Write-Host "Granting Microsoft Entra ID Global Reader role to the Automation Account (requires administrative permissions in Microsoft Entra and MS Graph PowerShell SDK >= 2.4.0)..." -ForegroundColor Green

            #workaround for https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/888
            $localPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile)
            if (-not(get-item "$localPath\.graph\" -ErrorAction SilentlyContinue))
            {
                New-Item -Type Directory "$localPath\.graph"
            }
            
            switch ($cloudEnvironment) {
                "AzureUSGovernment" {  
                    $graphEnvironment = "USGov"
                    break
                }
                "AzureChinaCloud" {  
                    $graphEnvironment = "China"
                    break
                }
                "AzureGermanCloud" {  
                    $graphEnvironment = "Germany"
                    break
                }
                Default {
                    $graphEnvironment = "Global"
                }
            }
            
            Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Directory","Directory.Read.All" -UseDeviceAuthentication -Environment $graphEnvironment -NoWelcome
            
            $globalReaderRole = Get-MgDirectoryRole -ExpandProperty Members -Property Id,Members,DisplayName,RoleTemplateId `
                | Where-Object { $_.RoleTemplateId -eq "f2ef992c-3afb-46b9-b7cf-a126ee74c451" }
            $globalReaders = $globalReaderRole.Members.Id
            if (-not($globalReaders -contains $spnId))
            {
                New-MgDirectoryRoleMemberByRef -DirectoryRoleId $globalReaderRole.Id -BodyParameter @{"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$spnId"}
                Start-Sleep -Seconds 5
                $globalReaderRole = Get-MgDirectoryRole -ExpandProperty Members -Property Id,Members,DisplayName,RoleTemplateId `
                    | Where-Object { $_.RoleTemplateId -eq "f2ef992c-3afb-46b9-b7cf-a126ee74c451" }
                $globalReaders = $globalReaderRole.Members.Id
                if ($globalReaders -contains $spnId)
                {
                    Write-Host "Role granted." -ForegroundColor Green
                }
                else
                {
                    throw "Error when trying to grant Global Reader role"
                }
            }
            else
            {
                Write-Host "Role was already granted before." -ForegroundColor Green            
            }        
        }
        catch
        {
            Write-Host $Error[0] -ForegroundColor Yellow
            Write-Host "Could not grant role. If you want Microsoft Entra-based recommendations, please grant the Global Reader role manually to the $automationAccountName managed identity or, for previous versions of AOE, to the Run As Account principal." -ForegroundColor Red
        }
        #endregion
    }
    else
    {
        Write-Host "Could not grant role. If you want Microsoft Entra-based recommendations, please grant the Global Reader role manually to the $automationAccountName managed identity or, for previous versions of AOE, to the Run As Account principal." -ForegroundColor Red
    }

    Write-Host "Azure Optimization Engine deployment completed! We're almost there..." -ForegroundColor Green

    #region Benefits Usage dependencies
    if (-not($deploymentOptions["DeployBenefitsUsageDependencies"]))
    {
        $benefitsUsageDependenciesOption = Read-Host "Do you also want to deploy the dependencies for the Azure Benefits usage workbooks (EA/MCA customers only + agreement administrator role required)? (Y/N)"
    } 
    else 
    {
        $benefitsUsageDependenciesOption = $deploymentOptions["DeployBenefitsUsageDependencies"]
    }
    if ("Y", "y" -contains $benefitsUsageDependenciesOption) 
    {
        $deploymentOptions["DeployBenefitsUsageDependencies"] = $benefitsUsageDependenciesOption        
        $automationAccount = Get-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AutomationAccountName
        $principalId = $automationAccount.Identity.PrincipalId
        $tenantId = $automationAccount.Identity.TenantId

        $mcaBillingAccountIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}"
        $mcaBillingProfileIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+)"
        
        if (-not($deploymentOptions["CustomerType"]))
        {   
            $customerType = Read-Host "Are you an Enterprise Agreement (EA) or Microsoft Customer Agreement (MCA) customer? Please, type EA or MCA"
            $deploymentOptions["CustomerType"] = $customerType        
        }
        else 
        {
            $customerType = $deploymentOptions["CustomerType"]
        }
        
        switch ($customerType) {
            "EA" {  
                if (-not($deploymentOptions["BillingAccountId"]))
                {
                    $billingAccountId = Read-Host "Please, enter your Enterprise Agreement Billing Account ID (e.g. 12345678)"
                    $deploymentOptions["BillingAccountId"] = $billingAccountId
                }
                else 
                {
                    $billingAccountId = $deploymentOptions["BillingAccountId"]
                }
                try
                {
                    [int32]::Parse($billingAccountId) | Out-Null
                }
                catch
                {
                    throw "The Enterprise Agreement Billing Account ID must be a number (e.g. 12345678)."
                }
                Write-Host "Granting the Enterprise Enrollment Reader role to the AOE Managed Identity..." -ForegroundColor Green
                $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments?api-version=2019-10-01-preview"
                $roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri
                if (-not($roleAssignmentResponse.StatusCode -eq 200))
                {
                    throw "The Enterprise Enrollment Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
                }
                $roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value
                if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq "/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e" }))
                {
                    $billingRoleAssignmentName = ([System.Guid]::NewGuid()).Guid
                    $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments/$($billingRoleAssignmentName)?api-version=2019-10-01-preview"
                    $body = "{`"properties`": {`"principalId`":`"$principalId`",`"principalTenantId`":`"$tenantId`",`"roleDefinitionId`":`"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e`"}}"
                    $roleAssignmentResponse = Invoke-AzRestMethod -Method PUT -Uri $uri -Payload $body
                    if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))
                    {
                        throw "The Enterprise Enrollment Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
                    }
                }
                else
                {
                    Write-Host "Role was already granted before." -ForegroundColor Green
                }
                break
            }
            "MCA" {
                if (-not($deploymentOptions["BillingAccountId"]))
                {
                    $billingAccountId = Read-Host "Please, enter your Microsoft Customer Agreement Billing Account ID (e.g. <guid>:<guid>_YYYY-MM-DD)"
                    $deploymentOptions["BillingAccountId"] = $billingAccountId
                }
                else 
                {
                    $billingAccountId = $deploymentOptions["BillingAccountId"]
                }
                if (-not($billingAccountId -match $mcaBillingAccountIdRegex))
                {
                    throw "The Microsoft Customer Agreement Billing Account ID must be in the format <guid>:<guid>_YYYY-MM-DD."
                }
                if (-not($deploymentOptions["BillingProfileId"]))
                {
                    $billingProfileId = Read-Host "Please, enter your Billing Profile ID (e.g. ABCD-DEF-GHI-JKL)"
                    $deploymentOptions["BillingProfileId"] = $billingProfileId
                }
                else 
                {
                    $billingProfileId = $deploymentOptions["BillingProfileId"]
                }
                if (-not($billingProfileId -match $mcaBillingProfileIdRegex))
                {
                    throw "The Microsoft Customer Agreement Billing Profile ID must be in the format ABCD-DEF-GHI-JKL."
                }
                Write-Host "Granting the Billing Profile Reader role to the AOE Managed Identity..." -ForegroundColor Green
                $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleAssignments?api-version=2019-10-01-preview"
                $roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri
                if (-not($roleAssignmentResponse.StatusCode -eq 200))
                {
                    throw "The Billing Profile Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
                }
                $roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value
                if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq "/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002" }))
                {
                    $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/createBillingRoleAssignment?api-version=2020-12-15-privatepreview"
                    $body = "{`"principalId`":`"$principalId`",`"principalTenantId`":`"$tenantId`",`"roleDefinitionId`":`"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002`"}"
                    $roleAssignmentResponse = Invoke-AzRestMethod -Method POST -Uri $uri -Payload $body
                    if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))
                    {
                        throw "The Billing Profile Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
                    }    
                }
                else
                {
                    Write-Host "Role was already granted before." -ForegroundColor Green
                }
                break
            }
            Default {
                throw "Only EA and MCA customers are supported at this time."
            }
        }
        
        Write-Output "Setting up the Billing Account ID variable..."
        $billingAccountIdVarName = "AzureOptimization_BillingAccountID"
        $billingAccountIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -ErrorAction SilentlyContinue
        if (-not($billingAccountIdVar))
        {
            New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null
        }
        else
        {
            Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null
        }
        
        if ($billingProfileId)
        {
            Write-Output "Setting up the Billing Profile ID variable..."
            $billingProfileIdVarName = "AzureOptimization_BillingProfileID"
            $billingProfileIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -ErrorAction SilentlyContinue
            if (-not($billingProfileIdVar))
            {
                New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null
            }
            else
            {
                Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null
            }    
        }    

        if (-not $deploymentOptions["CurrencyCode"])
        {
            $currencyCode = Read-Host "Please, enter your consumption currency code (e.g. EUR, USD, etc.)"
            $deploymentOptions["CurrencyCode"] = $currencyCode
        }
        else 
        {
            $currencyCode = $deploymentOptions["CurrencyCode"]
        }

        $deploymentOptions | ConvertTo-Json | Out-File -FilePath $lastDeploymentStatePath -Force

        Write-Output "Setting up the consumption currency code variable..."
        $currencyCodeVarName = "AzureOptimization_RetailPricesCurrencyCode"
        $currencyCodeVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -ErrorAction SilentlyContinue
        if (-not($currencyCodeVar))
        {
            New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null
        }
        else
        {
            Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null
        }
    }    
    #endregion

    Write-Host "Deployment fully completed!" -ForegroundColor Green
}
else {
    Write-Host "Deployment cancelled." -ForegroundColor Red
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Hélder Pinto

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Welcome to the Azure Optimization Engine - now a FinOps Toolkit tool! 🔍

👋 Thank you for your interest in the Azure Optimization Engine! We've migrated the Azure Optimization Engine repository to a new home - the Microsoft [FinOps Toolkit](https://aka.ms/AzureOptimizationEngine). For historical reasons, the older code will remain here, but the most recent version and new releases will be made in the FinOps Toolkit repository going forward. Here's what you need to know:

1. **We are collecting feedback in preparation for AOE v2**: Answer this [anonymous feedback form](https://forms.office.com/r/fLeJS8Rd2E) and contribute to the [discussion about the evolution of AOE](https://github.com/microsoft/finops-toolkit/discussions/753).

1. **Start Using the FinOps Toolkit**: If you were using the Azure Optimization Engine, it's time to switch! The [FinOps Toolkit](https://aka.ms/AzureOptimizationEngine) provides advanced solutions, automation scripts, and learning resources to accelerate your FinOps journey.

2. **Open Issues Here**: Have questions, feedback, or need assistance? Open an issue in the [FinOps Toolkit repository](https://github.com/microsoft/finops-toolkit/issues), and our community and maintainers will be happy to help. 🙌

3. **Explore Our Tools**: Besides the Azure Optimization Engine, check out all the other available tools, including FinOps hubs, Power BI reports, cost optimization workbooks, and more. [Explore the FinOps Toolkit](https://aka.ms/finops/toolkit)

4. **Stay Updated**: Follow our [FinOps blog](https://techcommunity.microsoft.com/t5/finops-blog/bg-p/FinOpsBlog) for the latest news and announcements.

Let's make cloud cost management easier together! 🌟


================================================
FILE: Reset-AutomationSchedules.ps1
================================================
param(
    [Parameter(Mandatory = $false)] 
    [String] $AzureEnvironment = "AzureCloud",

    [Parameter(Mandatory = $true)] 
    [String] $AutomationAccountName,

    [Parameter(Mandatory = $true)] 
    [String] $ResourceGroupName
)

$ErrorActionPreference = "Stop"

$ctx = Get-AzContext
if (-not($ctx)) {
    Connect-AzAccount -Environment $AzureEnvironment
    $ctx = Get-AzContext
}
else {
    if ($ctx.Environment.Name -ne $AzureEnvironment) {
        Disconnect-AzAccount -ContextName $ctx.Name
        Connect-AzAccount -Environment $AzureEnvironment
        $ctx = Get-AzContext
    }
}

try {
    $scheduledRunbooks = Get-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName
}
catch {
    throw "$AutomationAccountName Automation Account not found in Resource Group $ResourceGroupName in Subscription $($ctx.Subscription.Name). If we are not in the right subscription, use Set-AzContext to switch to the correct one."    
}

if (-not($scheduledRunbooks)) {
    throw "The $AutomationAccountName Automation Account does not contain any scheduled runbook. It might not be associated to the Azure Optimization Engine."
}

$subscriptionId = (Get-AzContext).Subscription.Id

$schedules = Get-AzAutomationSchedule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName
$weeklySchedules = $schedules | Where-Object { $_.Name.StartsWith("AzureOptimization") -and $_.Name.EndsWith("Weekly") }
if ($weeklySchedules.Count -gt 0) {
    $originalBaseTime = ($weeklySchedules | Sort-Object -Property StartTime | Select-Object -First 1).StartTime.AddHours(-1.25).DateTime
    $now = (Get-Date).ToUniversalTime()
    $diff = $now.AddHours(-1.25) - $originalBaseTime
    $nextWeekDays = [Math]::Ceiling($diff.TotalDays / 7) * 7
    $baseDateTime = $now.AddHours(-1.25).AddDays($nextWeekDays - $diff.TotalDays)
    $baseTimeStr = $baseDateTime.ToString("u")
    Write-Host "Existing schedules found. Weekly base time is $($baseDateTime.DayOfWeek) at $($baseDateTime.ToString('T')) (UTC)." -ForegroundColor Green
}
else {
    throw "The $AutomationAccountName Automation Account does not contain Azure Optimization Engine schedules."
}

$newBaseTimeStr = Read-Host "Please, enter a new base time for the *weekly* schedules in UTC (YYYY-MM-dd HH:mm:ss). If you want to keep the current one, just press ENTER"
if (-not($newBaseTimeStr)) {
    $newBaseTimeStr = $baseTimeStr
}
else {
    try {
        $newBaseTimeStr += "Z"
        $newBaseTime = [DateTime]::Parse($newBaseTimeStr)
    }
    catch {
        throw "$newBaseTimeStr is an invalid base time. Use the following format: YYYY-MM-dd HH:mm:ss. For example: 1977-09-08 06:14:15"
    }
    if ($newBaseTime -lt (Get-Date).ToUniversalTime().AddHours(-1)) {
        throw "$newBaseTimeStr is an invalid base time. It can't be sooner than $((Get-Date).ToUniversalTime().AddHours(-1).ToString('u'))"
    }
}

$baseTimeUtc = [DateTime]::Parse($newBaseTimeStr).ToUniversalTime()

if ($newBaseTimeStr -ne $baseTimeStr) {
    Write-Host "Updating current base schedule to every $($baseTimeUtc.DayOfWeek) at $($baseTimeUtc.TimeOfDay.ToString()) UTC..." -ForegroundColor Green
    $continueInput = Read-Host "Continue (Y/N)?"

    if ("Y", "y" -contains $continueInput) {
        $upgradeManifest = Get-Content -Path "./upgrade-manifest.json" | ConvertFrom-Json
        $manifestSchedules = $upgradeManifest.schedules

        foreach ($schedule in $schedules) {
            $manifestSchedule = $manifestSchedules | Where-Object { $_.name -eq $schedule.Name }
            if ($manifestSchedule) {
                if ($schedule.Frequency -eq "Week") {
                    $newStartTime = $baseTimeUtc.Add([System.Xml.XmlConvert]::ToTimeSpan($manifestSchedule.offset))
                }
                else {
                    $now = (Get-Date).ToUniversalTime()
                    $newStartTime = [System.DateTimeOffset]::Parse($now.ToString("yyyy-MM-ddT00:00:00Z"))
                    $newStartTime = $newStartTime.AddHours($baseTimeUtc.Hour).AddMinutes($baseTimeUtc.Minute).AddSeconds($baseTimeUtc.Second)
                    if ($newStartTime -lt $now.AddMinutes(15))
                    {
                        $newStartTime = $newStartTime.AddDays(1)
                    }
                    $newStartTime = $newStartTime.Add([System.Xml.XmlConvert]::ToTimeSpan($manifestSchedule.offset))                
                }
                $expiryTime = $schedule.ExpiryTime.ToString("yyyy-MM-ddTHH:mm:ssZ")
                $startTime = $newStartTime.ToString("yyyy-MM-ddTHH:mm:ssZ")
                $automationPath = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/schedules/$($schedule.Name)?api-version=2015-10-31"
                $body = "{
                `"name`": `"$($schedule.Name)`",
                `"properties`": {
                  `"description`": `"$($schedule.Description)`",
                  `"startTime`": `"$startTime`",
                  `"expiryTime`": `"$expiryTime`",
                  `"interval`": $($schedule.Interval),
                  `"frequency`": `"$($schedule.Frequency)`",
                  `"advancedSchedule`": {}
                }
              }"
                Invoke-AzRestMethod -Path $automationPath -Method PUT -Payload $body | Out-Null    
                Write-Host "Re-scheduled $($schedule.Name)." -ForegroundColor Blue
            }
            else {
                Write-Host "$($schedule.Name) not found in schedules manifest." -ForegroundColor Yellow
            }
        }
    }
    else
    {
        throw "Interrupting schedules reset due to user input."   
    }
}
else {
    Write-Host "Kept current base schedule (every $($baseTimeUtc.DayOfWeek) at $($baseTimeUtc.TimeOfDay.ToString()) UTC)." -ForegroundColor Green
}

$exportHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Export") })[0].HybridWorker
$ingestHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Ingest") })[0].HybridWorker
$recommendHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Recommend") })[0].HybridWorker
if ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Remediate") })
{
    $remediateHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith("Remediate") })[0].HybridWorker
}

$hybridWorkerOption = "None"
if ($exportHybridWorkerOption -or $ingestHybridWorkerOption -or $recommendHybridWorkerOption -or $remediateHybridWorkerOption) {
    $hybridWorkerOption = "Export: $exportHybridWorkerOption; Ingest: $ingestHybridWorkerOption; Recommend: $recommendHybridWorkerOption; Remediate: $remediateHybridWorkerOption"
}

Write-Host "Current Hybrid Worker option: $hybridWorkerOption" -ForegroundColor Green

$newHybridWorker = Read-Host "If you want all schedules to use the same Hybrid Worker, please enter the Hybrid Worker Group name (if you want to keep the current option, just press ENTER)"

if ($newHybridWorker)
{
    $hybridWorker = Get-AzAutomationHybridWorkerGroup -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name $newHybridWorker -ErrorAction SilentlyContinue
    if (-not($hybridWorker))
    {
        throw "Hybrid Worker $newHybridWorker was not found in Automation Account $automationAccountName."   
    }

    Write-Host "Updating Hybrid Worker Group in every runbook schedule to $newHybridWorker..."    
    $continueInput = Read-Host "Continue (Y/N)?"

    if ("Y", "y" -contains $continueInput)
    {
        Write-Host "Re-registering previous runbook schedules associations from $automationAccountName..." -ForegroundColor Green
        foreach ($jobSchedule in $scheduledRunbooks) {
            if ($jobSchedule.ScheduleName.StartsWith("AzureOptimization")) {
                $automationPath = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/jobSchedules/$($jobSchedule.JobScheduleId)?api-version=2015-10-31"
                $jobSchedule = (Invoke-AzRestMethod -Path $automationPath -Method GET).Content | ConvertFrom-Json
                Unregister-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                    -JobScheduleId $jobSchedule.id.Split('/')[10] -Force
                $params = @{}
                $jobSchedule.properties.parameters.PSObject.Properties | ForEach-Object {
                    $params[$_.Name] = $_.Value
                }                                                
                Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `
                    -RunbookName $jobSchedule.properties.runbook.name -ScheduleName $jobSchedule.properties.schedule.name -RunOn $newHybridWorker -Parameters $params | Out-Null
                Write-Host "Re-registered $($jobSchedule.properties.runbook.name) for schedule $($jobSchedule.properties.schedule.name)." -ForegroundColor Blue
            }
        }        
    }
    else
    {
        throw "Interrupting schedules reset due to user input."   
    }
}
else
{
    Write-Host "Kept current Hybrid Worker option: $hybridWorkerOption" -ForegroundColor Green
}

Write-Host "DONE" -ForegroundColor Green

================================================
FILE: Setup-BenefitsUsageDependencies.ps1
================================================
param(
    [Parameter(Mandatory = $false)] 
    [String] $AzureEnvironment = "AzureCloud",

    [Parameter(Mandatory = $true)] 
    [String] $AutomationAccountName,

    [Parameter(Mandatory = $true)] 
    [String] $ResourceGroupName
)

$ErrorActionPreference = "Stop"

$ctx = Get-AzContext
if (-not($ctx)) {
    Connect-AzAccount -Environment $AzureEnvironment
    $ctx = Get-AzContext
}
else {
    if ($ctx.Environment.Name -ne $AzureEnvironment) {
        Disconnect-AzAccount -ContextName $ctx.Name
        Connect-AzAccount -Environment $AzureEnvironment
        $ctx = Get-AzContext
    }
}

try {
    $scheduledRunbooks = Get-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName
}
catch {
    throw "$AutomationAccountName Automation Account not found in Resource Group $ResourceGroupName in Subscription $($ctx.Subscription.Name). If we are not in the right subscription, use Set-AzContext to switch to the correct one."    
}

if (-not($scheduledRunbooks)) {
    throw "The $AutomationAccountName Automation Account does not contain any scheduled runbook. It might not be associated to the Azure Optimization Engine."
}

$automationAccount = Get-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AutomationAccountName
$principalId = $automationAccount.Identity.PrincipalId
$tenantId = $automationAccount.Identity.TenantId

if (-not($principalId))
{
    throw "The $AutomationAccountName Automation Account does not have a managed identity and probably is not associated to the latest version of Azure Optimization Engine. Please, upgrade it before setting up benefits usage dependencies (more details: https://github.com/helderpinto/AzureOptimizationEngine#upgrade)."
}

$pricesheetSchedule = Get-AzAutomationSchedule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name "AzureOptimization_ExportPricesWeekly" -ErrorAction SilentlyContinue
if (-not($pricesheetSchedule)) {
    throw "The Azure Optimization Engine is in an older version. Please, upgrade it before setting up benefits usage dependencies (more details: https://github.com/helderpinto/AzureOptimizationEngine#upgrade)."
}

$mcaBillingAccountIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}"
$mcaBillingProfileIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+)"

$customerType = Read-Host "Are you an Enterprise Agreement (EA), Microsoft Customer Agreement (MCA), or other type (Other) of customer? Please, type EA, MCA, or Other"

switch ($customerType) {
    "EA" {  
        $billingAccountId = Read-Host "Please, enter your Enterprise Agreement Billing Account ID (e.g. 12345678)"
        try
        {
            [int32]::Parse($billingAccountId) | Out-Null
        }
        catch
        {
            throw "The Enterprise Agreement Billing Account ID must be a number (e.g. 12345678)."
        }
        Write-Host "Granting the Enterprise Enrollment Reader role to the AOE Managed Identity..." -ForegroundColor Green
        $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments?api-version=2019-10-01-preview"
        $roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri
        if (-not($roleAssignmentResponse.StatusCode -eq 200))
        {
            throw "The Enterprise Enrollment Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
        }
        $roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value
        if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq "/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e" }))
        {
            $billingRoleAssignmentName = ([System.Guid]::NewGuid()).Guid
            $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments/$($billingRoleAssignmentName)?api-version=2019-10-01-preview"
            $body = "{`"properties`": {`"principalId`":`"$principalId`",`"principalTenantId`":`"$tenantId`",`"roleDefinitionId`":`"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e`"}}"
            $roleAssignmentResponse = Invoke-AzRestMethod -Method PUT -Uri $uri -Payload $body
            if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))
            {
                throw "The Enterprise Enrollment Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
            }
        }
        else
        {
            Write-Host "Role was already granted before." -ForegroundColor Green
        }
        break
    }
    "MCA" {
        $billingAccountId = Read-Host "Please, enter your Microsoft Customer Agreement Billing Account ID (e.g. <guid>:<guid>_YYYY-MM-DD)"
        if (-not($billingAccountId -match $mcaBillingAccountIdRegex))
        {
            throw "The Microsoft Customer Agreement Billing Account ID must be in the format <guid>:<guid>_YYYY-MM-DD."
        }
        $billingProfileId = Read-Host "Please, enter your Billing Profile ID (e.g. ABCD-DEF-GHI-JKL)"
        if (-not($billingProfileId -match $mcaBillingProfileIdRegex))
        {
            throw "The Microsoft Customer Agreement Billing Profile ID must be in the format ABCD-DEF-GHI-JKL."
        }
        Write-Host "Granting the Billing Profile Reader role to the AOE Managed Identity..." -ForegroundColor Green
        $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleAssignments?api-version=2019-10-01-preview"
        $roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri
        if (-not($roleAssignmentResponse.StatusCode -eq 200))
        {
            throw "The Billing Profile Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
        }
        $roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value
        if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq "/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002" }))
        {
            $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/createBillingRoleAssignment?api-version=2020-12-15-privatepreview"
            $body = "{`"principalId`":`"$principalId`",`"principalTenantId`":`"$tenantId`",`"roleDefinitionId`":`"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002`"}"
            $roleAssignmentResponse = Invoke-AzRestMethod -Method POST -Uri $uri -Payload $body
            if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))
            {
                throw "The Billing Profile Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)"
            }    
        }
        else
        {
            Write-Host "Role was already granted before." -ForegroundColor Green
        }
        break
    }
    Default {
        throw "Only EA and MCA customers are supported at this time."
    }
}

Write-Output "Setting up the Billing Account ID variable..."
$billingAccountIdVarName = "AzureOptimization_BillingAccountID"
$billingAccountIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -ErrorAction SilentlyContinue
if (-not($billingAccountIdVar))
{
    New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null
}
else
{
    Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null
}

if ($billingProfileId)
{
    Write-Output "Setting up the Billing Profile ID variable..."
    $billingProfileIdVarName = "AzureOptimization_BillingProfileID"
    $billingProfileIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -ErrorAction SilentlyContinue
    if (-not($billingProfileIdVar))
    {
        New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null
    }
    else
    {
        Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null
    }    
}

$currencyCode = Read-Host "Please, enter your consumption currency code (e.g. EUR, USD, etc.)"
Write-Output "Setting up the consumption currency code variable..."
$currencyCodeVarName = "AzureOptimization_RetailPricesCurrencyCode"
$currencyCodeVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -ErrorAction SilentlyContinue
if (-not($currencyCodeVar))
{
    New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null
}
else
{
    Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null
}

Write-Host "DONE" -ForegroundColor Green

================================================
FILE: Setup-DataCollectionRules.ps1
================================================
param(
    [Parameter(Mandatory = $false)] 
    [String] $AzureEnvironment = "AzureCloud",

    [Parameter(Mandatory = $true)] 
    [String] $DestinationWorkspaceResourceId,

    [Parameter(Mandatory = $false)] 
    [int] $IntervalSeconds = 60,

    [Parameter(Mandatory = $false)]
    [hashtable] $ResourceTags = @{}
)

$ErrorActionPreference = "Stop"

$ctx = Get-AzContext
if (-not($ctx)) {
    Connect-AzAccount -Environment $AzureEnvironment
    $ctx = Get-AzContext
}
else {
    if ($ctx.Environment.Name -ne $AzureEnvironment) {
        Disconnect-AzAccount -ContextName $ctx.Name
        Connect-AzAccount -Environment $AzureEnvironment
        $ctx = Get-AzContext
    }
}

$lastDeploymentStatePath = ".\last-deployment-state.json"
$deploymentOptions = @{}

$perfCounters = Get-Content -Path ".\perfcounters.json" | ConvertFrom-Json 

if ((Test-Path -Path $lastDeploymentStatePath))
{
    $depOptions = Get-Content -Path $lastDeploymentStatePath | ConvertFrom-Json
    Write-Host $depOptions -ForegroundColor Green
    $depOptionsReuse = Read-Host "Found last deployment options above. Do you want to create Data Collection Rules (DCRs) reusing the last deployment options (Y/N)?"
    if ("Y", "y" -contains $depOptionsReuse)
    {
        foreach ($property in $depOptions.PSObject.Properties)
        {
            $deploymentOptions[$property.Name] = $property.Value
        }    
    }
}

Write-Host "Getting Azure subscriptions..." -ForegroundColor Yellow
$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" -and $_.SubscriptionPolicies.QuotaId -notlike "AAD*" }

if ($subscriptions.Count -gt 1) {

    $selectedSubscription = -1
    for ($i = 0; $i -lt $subscriptions.Count; $i++)
    {
        if (-not($deploymentOptions["SubscriptionId"]))
        {
            Write-Output "[$i] $($subscriptions[$i].Name)"    
        }
        else
        {
            if ($subscriptions[$i].Id -eq $deploymentOptions["SubscriptionId"])
            {
                $selectedSubscription = $i
                break
            }
        }
    }
    if (-not($deploymentOptions["SubscriptionId"]))
    {
        $lastSubscriptionIndex = $subscriptions.Count - 1
        while ($selectedSubscription -lt 0 -or $selectedSubscription -gt $lastSubscriptionIndex) {
            Write-Output "---"
            $selectedSubscription = [int] (Read-Host "Please, select the target subscription for this deployment [0..$lastSubscriptionIndex]")
        }    
    }
    if ($selectedSubscription -eq -1)
    {
        throw "The selected subscription does not exist. Check if you are logged in with the right Microsoft Entra ID user."        
    }
}
else
{
    if ($subscriptions.Count -ne 0)
    {
        $selectedSubscription = 0
    }
    else
    {
        throw "No valid subscriptions found. Only EA, MCA, PAYG or MSDN subscriptions are supported currently."
    }
}

if ($subscriptions.Count -eq 0) {
    throw "No subscriptions found. Check if you are logged in with the right Microsoft Entra ID account."
}

$subscriptionId = $subscriptions[$selectedSubscription].Id

if ($ctx.Subscription.SubscriptionId -ne $DestinationWorkspaceResourceId.Split('/')[2])
{
    $ctx = Set-AzContext -SubscriptionId $DestinationWorkspaceResourceId.Split('/')[2]
}

$la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $DestinationWorkspaceResourceId.Split('/')[4] -Name $DestinationWorkspaceResourceId.Split('/')[8] -ErrorAction SilentlyContinue

if (-not($la))
{
    throw "The destination workspace ($DestinationWorkspaceResourceId) does not exist. Check if you are logged in with the right Microsoft Entra ID user."
}

if (-not($deploymentOptions["NamePrefix"]))
{
    do
    {
        $namePrefix = Read-Host "Please, enter a unique name prefix for the DCRs or existing prefix if updating deployment. If you want instead to individually name all DCRs, just press ENTER"
        if (-not($namePrefix))
        {
            $namePrefix = "EmptyNamePrefix"
        }
    } 
    while ($namePrefix.Length -gt 21)
}
else {
    if ($deploymentOptions["NamePrefix"] -eq "EmptyNamePrefix")
    {
        $namePrefix = $null
    }
    else
    {
        $namePrefix = $deploymentOptions["NamePrefix"]            
    }
}

$windowsDcrNameTemplate = "{0}-windows-dcr"
$linuxDcrNameTemplate = "{0}-linux-dcr"

if (-not($deploymentOptions["ResourceGroupName"]))
{

    $resourceGroupName = Read-Host "Please, enter the new or existing Resource Group for this deployment"
}
else
{
    $resourceGroupName = $deploymentOptions["ResourceGroupName"]
}

if ($ctx.Subscription.SubscriptionId -ne $subscriptionId)
{
    $ctx = Set-AzContext -SubscriptionId $subscriptionId
}

$rg = Get-AzResourceGroup -Name $resourceGroupName

if ([string]::IsNullOrEmpty($namePrefix) -or $namePrefix -eq "EmptyNamePrefix") {
    $windowsDcrName = Read-Host "Enter the Windows DCR name"
    $linuxDcrName = Read-Host "Enter the Linux DCR name"
}
else {
    $windowsDcrName = $windowsDcrNameTemplate -f $namePrefix            
    $linuxDcrName = $linuxDcrNameTemplate -f $namePrefix
}

if (-not($deploymentOptions["TargetLocation"]))
{
    if (-not($rg.Location)) {
        Write-Host "Getting Azure locations..." -ForegroundColor Green
        $locations = Get-AzLocation | Where-Object { $_.Providers -contains "Microsoft.Insights" } | Sort-Object -Property Location
        
        for ($i = 0; $i -lt $locations.Count; $i++) {
            Write-Output "[$i] $($locations[$i].location)"    
        }
        $selectedLocation = -1
        $lastLocationIndex = $locations.Count - 1
        while ($selectedLocation -lt 0 -or $selectedLocation -gt $lastLocationIndex) {
            Write-Output "---"
            $selectedLocation = [int] (Read-Host "Please, select the target location for this deployment [0..$lastLocationIndex]")
        }
        
        $targetLocation = $locations[$selectedLocation].location    
    }
    else {
        $targetLocation = $rg.Location    
    }
}
else
{
    $targetLocation = $deploymentOptions["TargetLocation"]    
}

$windowsPerfCounters = @()
foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq "Windows"})) {
    $windowsPerfCounters += $ExecutionContext.InvokeCommand.ExpandString('"\\$($perfCounter.objectName)($($perfCounter.instance))\\$($perfCounter.counterName)"')
}

$linuxPerfCounters = @()
foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq "Linux"})) {
    $linuxPerfCounters += $ExecutionContext.InvokeCommand.ExpandString('"\\$($perfCounter.objectName)($($perfCounter.instance))\\$($perfCounter.counterName)"')
}

$windowsDcrBody = @'
{
    "dataSources": {
        "performanceCounters": [
            {
                "streams": [
                    "Microsoft-Perf"
                ],
                "samplingFrequencyInSeconds": $IntervalSeconds,
                "counterSpecifiers": [
                    $($windowsPerfCounters -join ",")
                ],
                "name": "perfCounterDataSource$IntervalSeconds"
            }
        ]
    },
    "destinations": {
        "logAnalytics": [
            {
                "workspaceResourceId": "$destinationWorkspaceResourceId",
                "workspaceId": "$($la.Properties.CustomerId)",
                "name": "la--1138206996"
            }
        ]
    },
    "dataFlows": [
        {
            "streams": [
                "Microsoft-Perf"
            ],
            "destinations": [
                "la--1138206996"
            ]
        }
    ]
}
'@

Write-Output "Creating Windows DCR..."
$windowsDcrBody = $ExecutionContext.InvokeCommand.ExpandString($windowsDcrBody) | ConvertFrom-Json
New-AzResource -ResourceType "Microsoft.Insights/dataCollectionRules" -ResourceGroupName $resourceGroupName -Location $targetLocation -Name $windowsDcrName -PropertyObject $windowsDcrBody -ApiVersion "2021-04-01" -Tag $ResourceTags -Kind "Windows" -Force | Out-Null

$linuxDcrBody = @'
{
    "dataSources": {
        "performanceCounters": [
            {
                "streams": [
                    "Microsoft-Perf"
                ],
                "samplingFrequencyInSeconds": $IntervalSeconds,
                "counterSpecifiers": [
                    $($linuxPerfCounters -join ",")
                ],
                "name": "perfCounterDataSource$IntervalSeconds"
            }
        ]
    },
    "destinations": {
        "logAnalytics": [
            {
                "workspaceResourceId": "$destinationWorkspaceResourceId",
                "workspaceId": "$($la.Properties.CustomerId)",
                "name": "la--1138206996"
            }
        ]
    },
    "dataFlows": [
        {
            "streams": [
                "Microsoft-Perf"
            ],
            "destinations": [
                "la--1138206996"
            ]
        }
    ]
}
'@

Write-Output "Creating Linux DCR..."
$linuxDcrBody = $ExecutionContext.InvokeCommand.ExpandString($linuxDcrBody) | ConvertFrom-Json
New-AzResource -ResourceType "Microsoft.Insights/dataCollectionRules" -ResourceGroupName $resourceGroupName -Location $targetLocation -Name $linuxDcrName -PropertyObject $linuxDcrBody -ApiVersion "2021-04-01" -Tag $ResourceTags -Kind "Linux" -Force | Out-Null

Write-Host -ForegroundColor Green "Deployment completed successfully"

================================================
FILE: Setup-LogAnalyticsWorkspaces.ps1
================================================
param(
    [Parameter(Mandatory = $false)] 
    [String] $AzureEnvironment = "AzureCloud",

    [Parameter(Mandatory = $false)] 
    [String[]] $WorkspaceIds,

    [Parameter(Mandatory = $false)]
    [switch] $AutoFix,

    [Parameter(Mandatory = $false)] 
    [int] $IntervalSeconds = 60
)

$ErrorActionPreference = "Stop"

$ctx = Get-AzContext
if (-not($ctx)) {
    Connect-AzAccount -Environment $AzureEnvironment
    $ctx = Get-AzContext
}
else {
    if ($ctx.Environment.Name -ne $AzureEnvironment) {
        Disconnect-AzAccount -ContextName $ctx.Name
        Connect-AzAccount -Environment $AzureEnvironment
        $ctx = Get-AzContext
    }
}

$wsIds = foreach ($workspaceId in $WorkspaceIds)
{
    "'$workspaceId'"
}
if ($wsIds)
{
    $wsIds = $wsIds -join ","
    $whereWsIds = " and properties.customerId in ($wsIds)"
}

$perfCounters = Get-Content -Path ".\perfcounters.json" | ConvertFrom-Json 

$ARGPageSize = 1000

$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"}

$argQuery = "resources | where type =~ 'microsoft.operationalinsights/workspaces'$whereWsIds | order by id"

$workspaces = (Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions).data

Write-Output "Found $($workspaces.Count) workspaces."

$laQuery = "Heartbeat | where TimeGenerated > ago(1d) and ComputerEnvironment == 'Azure' | distinct Computer | summarize AzureComputersCount = count()"

foreach ($workspace in $workspaces) {
    $laQueryResults = $null
    $results = $null
    $laQueryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspace.properties.customerId -Query $laQuery -Timespan (New-TimeSpan -Days 1) -ErrorAction Continue
    if ($laQueryResults)
    {
        $results = [System.Linq.Enumerable]::ToArray($laQueryResults.Results)
        Write-Output "$($workspace.name) ($($workspace.properties.customerId)): $($results.AzureComputersCount) Azure computers connected."    
    }
    else
    {
        Write-Output "$($workspace.name) ($($workspace.properties.customerId)): could not validate connected computers."
    }
    if ($results.AzureComputersCount -gt 0)
    {
        if ($ctx.Subscription.SubscriptionId -ne $workspace.subscriptionId)
        {
            $ctx = Set-AzContext -SubscriptionId $workspace.subscriptionId
        }
        $dsWindows = Get-AzOperationalInsightsDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name -Kind WindowsPerformanceCounter
        foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq "Windows"})) {
            if (-not($dsWindows | Where-Object { $_.Properties.ObjectName -eq $perfCounter.objectName -and $_.Properties.InstanceName -eq $perfCounter.instance `
                -and $_.Properties.CounterName -eq $perfCounter.counterName}))
            {
                Write-Output "Missing $($perfCounter.objectName)($($perfCounter.instance))\$($perfCounter.counterName)"
                if ($AutoFix)
                {
                    Write-Output "Fixing..."
                    $dsName = "DataSource_WindowsPerformanceCounter_$(New-Guid)"
                    New-AzOperationalInsightsWindowsPerformanceCounterDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name `
                        -Name $dsName -ObjectName $perfCounter.objectName -CounterName $perfCounter.counterName -InstanceName $perfCounter.instance `
                        -IntervalSeconds $IntervalSeconds -Force | Out-Null
                }
            }
        }

        $missingLinuxCounters = @()
        $dsLinux = Get-AzOperationalInsightsDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name -Kind LinuxPerformanceObject
        foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq "Linux"})) {
            if (-not($dsLinux | Where-Object { $_.Properties.ObjectName -eq $perfCounter.objectName -and $_.Properties.InstanceName -eq $perfCounter.instance `
                -and ($_.Properties.PerformanceCounters | Where-Object { $_.CounterName -eq $perfCounter.counterName }) }))
            {
                Write-Output "Missing $($perfCounter.objectName)($($perfCounter.instance))\$($perfCounter.counterName)"
                if ($AutoFix)
                {
                    $missingLinuxCounters += $perfCounter
                }
            }
        }

        if ($AutoFix)
        {
            $fixedLinuxCounters = @()
            $existingLinuxObjects = ($dsLinux | Select-Object -ExpandProperty Properties | Select-Object -Property ObjectName).ObjectName
            foreach ($linuxObject in $existingLinuxObjects) {
                $missingObjectCounters = $missingLinuxCounters | Where-Object { $_.objectName -eq $linuxObject }
                $originalDataSource = $dsLinux | Where-Object { $_.Properties.ObjectName -eq $linuxObject }
                foreach ($perfCounter in $missingObjectCounters) {
                    $fixedLinuxCounters += $perfCounter
                    $newCounterName = New-Object -TypeName Microsoft.Azure.Commands.OperationalInsights.Models.PerformanceCounterIdentifier -Property @{CounterName = $perfCounter.counterName}
                    $originalDataSource.Properties.PerformanceCounters.Add($newCounterName)
                }
                if ($missingObjectCounters)
                {
                    Write-Output "Fixing $linuxObject object..."
                    Set-AzOperationalInsightsDataSource -DataSource $originalDataSource | Out-Null
                }
            }
            $missingObjects = ($missingLinuxCounters | Select-Object -Property objectName -Unique).objectName
            $fixedObjects = ($fixedLinuxCounters | Select-Object -Property objectName -Unique).objectName
            $missingObjects = $missingObjects | Where-Object { -not($_ -in $fixedObjects) }
            foreach ($linuxObject in $missingObjects) {
                $missingObjectCounters = $missingLinuxCounters | Where-Object { $_.objectName -eq $linuxObject }
                $missingInstance = ($missingObjectCounters | Select-Object -Property instance -Unique -First 1).instance
                $missingCounterNames = ($missingObjectCounters).counterName
    
                Write-Output "Adding $linuxObject object..."
                New-AzOperationalInsightsLinuxPerformanceObjectDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name `
                    -Name "DataSource_LinuxPerformanceObject_$(New-Guid)" -ObjectName $linuxObject -InstanceName $missingInstance -IntervalSeconds $IntervalSeconds `
                    -CounterNames $missingCounterNames -Force | Out-Null
            }    
        }
    }
}

================================================
FILE: Suppress-Recommendation.ps1
================================================
param(
    [Parameter(Mandatory = $true)] 
    [String] $RecommendationId
)

$ErrorActionPreference = "Stop"

function Test-IsGuid
{
    [OutputType([bool])]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]$ObjectGuid
    )

    # Define verification regex
    [regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$'

    # Check guid against regex
    return $ObjectGuid -match $guidRegex
}

if (-not(Test-IsGuid -ObjectGuid $RecommendationId))
{
    Write-Host "The provided recommendation Id is invalid. Must be a valid GUID." -ForegroundColor Red
    Exit
}

$databaseConnectionSettingsPath = ".\database-connection-settings.json"
$dbConnectionSettings = @{}

if (Test-Path -Path $databaseConnectionSettingsPath)
{
    $dbSettings = Get-Content -Path $databaseConnectionSettingsPath | ConvertFrom-Json
    Write-Host $dbSettings -ForegroundColor Green
    $dbSettingsReuse = Read-Host "Found existing database connection settings. Do you want to reuse them (Y/N)?"
    if ("Y", "y" -contains $dbSettingsReuse)
    {
        foreach ($property in $dbSettings.PSObject.Properties)
        {
            $dbConnectionSettings[$property.Name] = $property.Value
        }    
    }
}

if (-not($dbConnectionSettings["DatabaseServer"]))
{
    $databaseServer = Read-Host "Please, enter the AOE Azure SQL server hostname (e.g., xpto.database.windows.net)"
    $dbConnectionSettings["DatabaseServer"] = $databaseServer
}
else
{
    $databaseServer = $dbConnectionSettings["DatabaseServer"]
}

if (-not($dbConnectionSettings["DatabaseName"]))
{
    $databaseName = Read-Host "Please, enter the AOE Azure SQL Database name (e.g., azureoptimization)"
    $dbConnectionSettings["DatabaseName"] = $databaseName
}
else
{
    $databaseName = $dbConnectionSettings["DatabaseName"]
}

if (-not($dbConnectionSettings["DatabaseUser"]))
{
    $databaseUser = Read-Host "Please, enter the AOE database user name"
    $dbConnectionSettings["DatabaseUser"] = $databaseUser
}
else
{
    $databaseUser = $dbConnectionSettings["DatabaseUser"]
}

$sqlPass = Read-Host "Please, input the password for the $databaseUser SQL user" -AsSecureString
$sqlPassPlain = (New-Object PSCredential "user", $sqlPass).GetNetworkCredential().Password
$sqlPassPlain = $sqlPassPlain.Replace("'", "''")

$SqlTimeout = 120
$recommendationsTable = "Recommendations"
$suppressionsTable = "Filters"

Write-Host "Opening connection to the database..." -ForegroundColor Green

$tries = 0
$connectionSuccess = $false
do {
    $tries++
    try {
        $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$databaseServer,1433;Database=$databaseName;User ID=$databaseUser;Password='$sqlPassPlain';Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
        $Conn.Open() 
        $Cmd=new-object system.Data.SqlClient.SqlCommand
        $Cmd.Connection = $Conn
        $Cmd.CommandTimeout = $SqlTimeout
        $Cmd.CommandText = "SELECT * FROM [dbo].[$recommendationsTable] WHERE RecommendationId = '$RecommendationId'"
    
        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
        $sqlAdapter.SelectCommand = $Cmd
        $controlRows = New-Object System.Data.DataTable
        $sqlAdapter.Fill($controlRows) | Out-Null            
        $connectionSuccess = $true
    }
    catch {
        Write-Host "Failed to contact SQL at try $tries." -ForegroundColor Yellow
        Write-Host $Error[0] -ForegroundColor Yellow
        Write-Output "Waiting $($tries * 20) seconds..."
        Start-Sleep -Seconds ($tries * 20)
    }    
} while (-not($connectionSuccess) -and $tries -lt 3)

if (-not($connectionSuccess))
{
    throw "Could not establish connection to SQL."
}

$Conn.Close()    
$Conn.Dispose()            

if (-not($controlRows.RecommendationId))
{
    Write-Host "The provided recommendation Id was not found. Please, try again with a valid GUID." -ForegroundColor Red
    Exit
}

Write-Host "You are suppressing the recommendation with the below details" -ForegroundColor Green
Write-Host "Recommendation: $($controlRows.RecommendationDescription)" -ForegroundColor Blue
Write-Host "Recommendation sub-type id: $($controlRows.RecommendationSubTypeId)" -ForegroundColor Blue
Write-Host "Category: $($controlRows.Category)" -ForegroundColor Blue
Write-Host "Instance Name: $($controlRows.InstanceName)" -ForegroundColor Blue
Write-Host "Resource Group: $($controlRows.ResourceGroup)" -ForegroundColor Blue
Write-Host "Subscription Id: $($controlRows.SubscriptionGuid)" -ForegroundColor Blue
Write-Host "Please, choose the suppression type" -ForegroundColor Green
Write-Host "[E]xclude - this recommendation type will be completely excluded from the engine and will no longer be generated for any resource" -ForegroundColor Green
Write-Host "[D]ismiss - this recommendation will be dismissed for the scope to be chosen next (instance, resource group or subscription)" -ForegroundColor Green
Write-Host "[S]nooze - this recommendation will be postponed for the duration (in days) and scope to be chosen next (instance, resource group or subscription)" -ForegroundColor Green
Write-Host "[C]ancel - no action will be taken" -ForegroundColor Green
$suppOption = Read-Host "Enter your choice (E, D, S or C)"

if ("E", "e" -contains $suppOption)
{
    $suppressionType = "Exclude"
}
elseif ("D", "d" -contains $suppOption)
{
    $suppressionType = "Dismiss"
}
elseif ("S", "s" -contains $suppOption)
{
    $suppressionType = "Snooze"
}
else
{
    Write-Host "Cancelling.. No action will be taken." -ForegroundColor Green    
    Exit
}

if ($suppressionType -in ("Dismiss", "Snooze"))
{
    Write-Host "Please, choose the scope for the suppression" -ForegroundColor Green
    Write-Host "[S]ubscription ($($controlRows.SubscriptionGuid))" -ForegroundColor Green
    Write-Host "[R]esource Group ($($controlRows.ResourceGroup))" -ForegroundColor Green
    Write-Host "[I]nstance ($($controlRows.InstanceName))" -ForegroundColor Green
    $scopeOption = Read-Host "Enter your choice (S, R, or I)"

    if ("S", "s" -contains $scopeOption)
    {
        $scope = $controlRows.SubscriptionGuid
    }
    elseif ("R", "r" -contains $scopeOption)
    {
        $scope = $controlRows.ResourceGroup
    }
    elseif ("I", "i" -contains $scopeOption)
    {
        $scope = $controlRows.InstanceId
    }
    else
    {
        Write-Host "Wrong input. No action will be taken." -ForegroundColor Red
        Exit
    }
}

$snoozeDays = 0
if ($suppressionType -eq "Snooze")
{
    Write-Host "Please, enter the number of days the recommendation will be snoozed" -ForegroundColor Green
    $snoozeDays = Read-Host "Number of days (min. 14)"
    if (-not($snoozeDays -ge 14))
    {
        Write-Host "Wrong snooze days. No action will be taken." -ForegroundColor Red
        Exit
    }
}

$author = Read-Host "Please enter your name"
$notes = Read-Host "Please enter a reason for this suppression"

Write-Host "You are about to suppress this recommendation" -ForegroundColor Yellow
Write-Host "Recommendation: $($controlRows.RecommendationDescription)" -ForegroundColor Blue
Write-Host "Suppression type: $suppressionType" -ForegroundColor Blue
if ($suppressionType -in ("Dismiss", "Snooze"))
{
    Write-Host "Scope: $scope" -ForegroundColor Blue    
}
if ($suppressionType -eq "Snooze")
{
    Write-Host "Snooze days: $snoozeDays" -ForegroundColor Blue    
}
Write-Host "Author: $author" -ForegroundColor Blue
Write-Host "Reason: $notes" -ForegroundColor Blue
$continueInput = Read-Host "Do you want to continue (Y/N)?"
if ("Y", "y" -contains $continueInput) 
{
    if ($scope)
    {
        $scope = "'$scope'"
    }
    else
    {
        $scope = "NULL"    
    }

    if ($snoozeDays -ge 14)
    {
        $now = (Get-Date).ToUniversalTime()
        $endDate = "'$($now.Add($snoozeDays).ToString("yyyy-MM-ddTHH:mm:00Z"))'"
    }
    else {
        $endDate = "NULL"
    }

    $sqlStatement = "INSERT INTO [$suppressionsTable] VALUES (NEWID(), '$($controlRows.RecommendationSubTypeId)', '$suppressionType', $scope, GETDATE(), $endDate, '$author', '$notes', 1)"

    $Conn2 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$databaseServer,1433;Database=$databaseName;User ID=$databaseUser;Password='$sqlPassPlain';Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") 
    $Conn2.Open() 
    
    $Cmd=new-object system.Data.SqlClient.SqlCommand
    $Cmd.Connection = $Conn2
    $Cmd.CommandText = $sqlStatement
    $Cmd.CommandTimeout=120 
    try
    {
        $Cmd.ExecuteReader()
    }
    catch
    {
        Write-Output "Failed statement: $sqlStatement"
        throw
    }
    
    $Conn2.Close()                

    Write-Host "Suppression sucessfully added." -ForegroundColor Green
}
else
{
    Write-Host "No action was taken." -ForegroundColor Green
}

$dbConnectionSettings | ConvertTo-Json | Out-File -FilePath $databaseConnectionSettingsPath -Force

================================================
FILE: azuredeploy-nested.bicep
================================================
param projectLocation string
param templateLocation string

param storageAccountName string
param automationAccountName string
param sqlServerName string
param sqlDatabaseName string
param logAnalyticsReuse bool
param logAnalyticsWorkspaceName string
param logAnalyticsWorkspaceRG string
param logAnalyticsRetentionDays int
param sqlBackupRetentionDays int
param sqlAdminLogin string

@secure()
param sqlAdminPassword string
param cloudEnvironment string
param authenticationOption string
param baseTime string
param resourceTags object
param contributorRoleAssignmentGuid string

param argDiskExportJobId string = newGuid()
param argVhdExportJobId string = newGuid()
param argVmExportJobId string = newGuid()
param argVmssExportJobId string = newGuid()
param argAvailSetExportJobId string = newGuid()
param advisorExportJobId string = newGuid()
param consumptionExportJobId string = newGuid()
param aadObjectsExportJobId string = newGuid()
param argLoadBalancersExportJobId string = newGuid()
param argAppGWsExportJobId string = newGuid()
param rbacExportJobId string = newGuid()
param argResContainersExportJobId string = newGuid()
param argNICExportJobId string = newGuid()
param argNSGExportJobId string = newGuid()
param argPublicIPExportJobId string = newGuid()
param argVNetExportJobId string = newGuid()
param argSqlDbExportJobId string = newGuid()
param policyStateExportJobId string = newGuid()
param monitorVmssCpuMaxExportJobId string = newGuid()
param monitorVmssCpuAvgExportJobId string = newGuid()
param monitorVmssMemoryMinExportJobId string = newGuid()
param monitorSqlDbDtuMaxExportJobId string = newGuid()
param monitorSqlDbDtuAvgExportJobId string = newGuid()
param monitorAppServiceCpuMaxExportJobId string = newGuid()
param monitorAppServiceCpuAvgExportJobId string = newGuid()
param monitorAppServiceMemoryMaxExportJobId string = newGuid()
param monitorAppServiceMemoryAvgExportJobId string = newGuid()
param monitorDiskIOPSAvgExportJobId string = newGuid()
param monitorDiskMBPsAvgExportJobId string = newGuid()
param argAppServicePlanExportJobId string = newGuid()
param pricesheetExportJobId string = newGuid()
param reservationPricesExportJobId string = newGuid()
param reservationUsageExportJobId string = newGuid()
param savingsPlansUsageExportJobId string = newGuid()
param argDiskIngestJobId string = newGuid()
param argVhdIngestJobId string = newGuid()
param argVmIngestJobId string = newGuid()
param argVmssIngestJobId string = newGuid()
param argAvailSetIngestJobId string = newGuid()
param advisorIngestJobId string = newGuid()
param remediationLogsIngestJobId string = newGuid()
param consumptionIngestJobId string = newGuid()
param aadObjectsIngestJobId string = newGuid()
param argLoadBalancersIngestJobId string = newGuid()
param argAppGWsIngestJobId string = newGuid()
param argResContainersIngestJobId string = newGuid()
param rbacIngestJobId string = newGuid()
param argNICIngestJobId string = newGuid()
param argNSGIngestJobId string = newGuid()
param argPublicIPIngestJobId string = newGuid()
param argVNetIngestJobId string = newGuid()
param argSqlDbIngestJobId string = newGuid()
param policyStateIngestJobId string = newGuid()
param monitorIngestJobId string = newGuid()
param argAppServicePlanIngestJobId string = newGuid()
param pricesheetIngestJobId string = newGuid()
param reservationPricesIngestJobId string = newGuid()
param reservationUsageIngestJobId string = newGuid()
param savingsPlansUsageIngestJobId string = newGuid()
param unattachedDisksRecommendationJobId string = newGuid()
param advisorCostAugmentedRecommendationJobId string = newGuid()
param advisorAsIsRecommendationJobId string = newGuid()
param vmsHaRecommendationJobId string = newGuid()
param vmOptimizationsRecommendationJobId string = newGuid()
param aadExpiringCredsRecommendationJobId string = newGuid()
param unusedLoadBalancersRecommendationJobId string = newGuid()
param unusedAppGWsRecommendationJobId string = newGuid()
param armOptimizationsRecommendationJobId string = newGuid()
param vnetOptimizationsRecommendationJobId string = newGuid()
param vmssOptimizationsRecommendationJobId string = newGuid()
param sqldbOptimizationsRecommendationJobId string = newGuid()
param storageOptimizationsRecommendationJobId string = newGuid()
param appServiceOptimizationsRecommendationJobId string = newGuid()
param diskOptimizationsRecommendationJobId string = newGuid()
param recommendationsIngestJobId string = newGuid()
param recommendationsLogAnalyticsIngestJobId string = newGuid()
param suppressionsLogAnalyticsIngestJobId string = newGuid()
param recommendationsCleanUpJobId string = newGuid()

param roleContributor string = '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'

var advisorExportsRunbookName = 'Export-AdvisorRecommendationsToBlobStorage'
var argVmExportsRunbookName = 'Export-ARGVirtualMachinesPropertiesToBlobStorage'
var argVmssExportsRunbookName = 'Export-ARGVMSSPropertiesToBlobStorage'
var argDisksExportsRunbookName = 'Export-ARGManagedDisksPropertiesToBlobStorage'
var argVhdExportsRunbookName = 'Export-ARGUnmanagedDisksPropertiesToBlobStorage'
var argAvailSetExportsRunbookName = 'Export-ARGAvailabilitySetPropertiesToBlobStorage'
var consumptionExportsRunbookName = 'Export-ConsumptionToBlobStorage'
var aadObjectsExportsRunbookName = 'Export-AADObjectsToBlobStorage'
var argLoadBalancersExportsRunbookName = 'Export-ARGLoadBalancerPropertiesToBlobStorage'
var argAppGWsExportsRunbookName = 'Export-ARGAppGatewayPropertiesToBlobStorage'
var argResContainersExportsRunbookName = 'Export-ARGResourceContainersPropertiesToBlobStorage'
var rbacExportsRunbookName = 'Export-RBACAssignmentsToBlobStorage'
var argNICExportsRunbookName = 'Export-ARGNICPropertiesToBlobStorage'
var argNSGExportsRunbookName = 'Export-ARGNSGPropertiesToBlobStorage'
var argVNetExportsRunbookName = 'Export-ARGVNetPropertiesToBlobStorage'
var argPublicIpExportsRunbookName = 'Export-ARGPublicIpPropertiesToBlobStorage'
var argSqlDbExportsRunbookName = 'Export-ARGSqlDatabasePropertiesToBlobStorage'
var policyStateExportsRunbookName = 'Export-PolicyComplianceToBlobStorage'
var monitorExportsRunbookName = 'Export-AzMonitorMetricsToBlobStorage'
var argAppServicePlanExportsRunbookName = 'Export-ARGAppServicePlanPropertiesToBlobStorage'
var reservationsExportsRunbookName = 'Export-ReservationsUsageToBlobStorage'
var reservationsPriceExportsRunbookName = 'Export-ReservationsPriceToBlobStorage'
var priceSheetExportsRunbookName = 'Export-PriceSheetToBlobStorage'
var savingsPlansExportsRunbookName = 'Export-SavingsPlansUsageToBlobStorage'
var advisorExportsScheduleName = 'AzureOptimization_ExportAdvisorWeekly'
var argExportsScheduleName = 'AzureOptimization_ExportARGDaily'
var consumptionExportsScheduleName = 'AzureOptimization_ExportConsumptionDaily'
var aadObjectsExportsScheduleName = 'AzureOptimization_ExportAADObjectsDaily'
var rbacExportsScheduleName = 'AzureOptimization_ExportRBACDaily'
var policyStateExportsScheduleName = 'AzureOptimization_ExportPolicyStateDaily'
var monitorVmssCpuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorVmssCpuMaxHourly'
var monitorVmssCpuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorVmssCpuAvgHourly'
var monitorVmssMemoryMinExportsScheduleName = 'AzureOptimization_ExportMonitorVmssMemoryMinHourly'
var monitorSqlDbDtuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorSqlDbDtuMaxHourly'
var monitorSqlDbDtuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorSqlDbDtuAvgHourly'
var monitorAppServiceCpuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceCpuMaxHourly'
var monitorAppServiceCpuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceCpuAvgHourly'
var monitorAppServiceMemoryMaxExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceMemoryMaxHourly'
var monitorAppServiceMemoryAvgExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceMemoryAvgHourly'
var monitorDiskIOPSAvgExportsScheduleName = 'AzureOptimization_ExportMonitorDiskIOPSHourly'
var monitorDiskMBPsAvgExportsScheduleName = 'AzureOptimization_ExportMonitorDiskMBPsHourly'
var priceExportsScheduleName = 'AzureOptimization_ExportPricesWeekly'
var reservationsUsageExportsScheduleName = 'AzureOptimization_ExportReservationsDaily'
var savingsPlansUsageExportsScheduleName = 'AzureOptimization_ExportSavingsPlansDaily'
var csvExportsSchedules = [
  {
    exportSchedule: argExportsScheduleName
    exportDescription: 'Daily Azure Resource Graph exports'
    exportTimeOffset: 'PT1H05M'
    exportFrequency: 'Day'
  }
  {
    exportSchedule: advisorExportsScheduleName
    exportDescription: 'Weekly Azure Advisor exports'
    exportTimeOffset: 'PT1H15M'
    exportFrequency: 'Week'
  }
  {
    exportSchedule: consumptionExportsScheduleName
    exportDescription: 'Daily Azure Consumption exports'
    exportTimeOffset: 'PT1H'
    exportFrequency: 'Day'
  }
  {
    exportSchedule: aadObjectsExportsScheduleName
    exportDescription: 'Daily Microsoft Entra Objects exports'
    exportTimeOffset: 'PT1H'
    exportFrequency: 'Day'
  }
  {
    exportSchedule: rbacExportsScheduleName
    exportDescription: 'Daily Azure RBAC exports'
    exportTimeOffset: 'PT1H02M'
    exportFrequency: 'Day'
  }
  {
    exportSchedule: policyStateExportsScheduleName
    exportDescription: 'Daily Azure Policy State exports'
    exportTimeOffset: 'PT1H'
    exportFrequency: 'Day'
  }
  {
    exportSchedule: monitorVmssCpuAvgExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Percentage CPU (Avg.)'
    exportTimeOffset: 'PT1H15M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorVmssCpuMaxExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Percentage CPU (Max.)'
    exportTimeOffset: 'PT1H15M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorVmssMemoryMinExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Available Memory (Min.)'
    exportTimeOffset: 'PT1H15M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorSqlDbDtuMaxExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for SQL Database Percentage DTU (Max.)'
    exportTimeOffset: 'PT1H15M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorSqlDbDtuAvgExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for SQL Database Percentage DTU (Avg.)'
    exportTimeOffset: 'PT1H16M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorAppServiceCpuAvgExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage CPU (Avg.)'
    exportTimeOffset: 'PT1H16M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorAppServiceCpuMaxExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage CPU (Max.)'
    exportTimeOffset: 'PT1H16M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorAppServiceMemoryAvgExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage RAM (Avg.)'
    exportTimeOffset: 'PT1H16M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorAppServiceMemoryMaxExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage RAM (Max.)'
    exportTimeOffset: 'PT1H17M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorDiskIOPSAvgExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for Disk IOPS (Avg.)'
    exportTimeOffset: 'PT1H17M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: monitorDiskMBPsAvgExportsScheduleName
    exportDescription: 'Hourly Azure Monitor metrics exports for Disk MBPs (Avg.)'
    exportTimeOffset: 'PT1H17M'
    exportFrequency: 'Hour'
  }
  {
    exportSchedule: priceExportsScheduleName
    exportDescription: 'Weekly Pricesheet and Reservation Prices exports'
    exportTimeOffset: 'PT1H35M'
    exportFrequency: 'Week'
  }
  {
    exportSchedule: reservationsUsageExportsScheduleName
    exportDescription: 'Daily Reservation Usage exports'
    exportTimeOffset: 'PT2H'
    exportFrequency: 'Day'
  }
  {
    exportSchedule: savingsPlansUsageExportsScheduleName
    exportDescription: 'Daily Savings Plans Usage exports'
    exportTimeOffset: 'PT2H05M'
    exportFrequency: 'Day'
  }
]
var csvExports = [
  {
    runbookName: advisorExportsRunbookName
    isOneToMany: false
    containerName: 'advisorexports'
    variableName: 'AzureOptimization_AdvisorContainer'
    variableDescription: 'The Storage Account container where Azure Advisor exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestAdvisorWeekly'
    ingestDescription: 'Weekly Azure Advisor recommendations ingests'
    ingestTimeOffset: 'PT1H45M'
    ingestFrequency: 'Week'
    ingestJobId: advisorIngestJobId
    exportSchedule: advisorExportsScheduleName
    exportJobId: advisorExportJobId
  }
  {
    runbookName: argVmExportsRunbookName
    isOneToMany: false
    containerName: 'argvmexports'
    variableName: 'AzureOptimization_ARGVMContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph Virtual Machine exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGVMsDaily'
    ingestDescription: 'Daily Azure Resource Graph Virtual Machines ingests'
    ingestTimeOffset: 'PT1H30M'
    ingestFrequency: 'Day'
    ingestJobId: argVmIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argVmExportJobId
  }
  {
    runbookName: argVmssExportsRunbookName
    isOneToMany: false
    containerName: 'argvmssexports'
    variableName: 'AzureOptimization_ARGVMSSContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph VMSS exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGVMSSDaily'
    ingestDescription: 'Daily Azure Resource Graph VMSS ingests'
    ingestTimeOffset: 'PT1H30M'
    ingestFrequency: 'Day'
    ingestJobId: argVmssIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argVmssExportJobId
  }
  {
    runbookName: argDisksExportsRunbookName
    isOneToMany: false
    containerName: 'argdiskexports'
    variableName: 'AzureOptimization_ARGDiskContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph Managed Disks exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGDisksDaily'
    ingestDescription: 'Daily Azure Resource Graph Managed Disks ingests'
    ingestTimeOffset: 'PT1H30M'
    ingestFrequency: 'Day'
    ingestJobId: argDiskIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argDiskExportJobId
  }
  {
    runbookName: argVhdExportsRunbookName
    isOneToMany: false
    containerName: 'argvhdexports'
    variableName: 'AzureOptimization_ARGVhdContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph Unmanaged Disks exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGVHDsDaily'
    ingestDescription: 'Daily Azure Resource Graph Unmanaged Disks ingests'
    ingestTimeOffset: 'PT1H30M'
    ingestFrequency: 'Day'
    ingestJobId: argVhdIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argVhdExportJobId
  }
  {
    runbookName: argAvailSetExportsRunbookName
    isOneToMany: false
    containerName: 'argavailsetexports'
    variableName: 'AzureOptimization_ARGAvailabilitySetContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph Availability Set exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGAvailSetsDaily'
    ingestDescription: 'Daily Azure Resource Graph Availability Sets ingests'
    ingestTimeOffset: 'PT1H31M'
    ingestFrequency: 'Day'
    ingestJobId: argAvailSetIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argAvailSetExportJobId
  }
  {
    runbookName: consumptionExportsRunbookName
    isOneToMany: false
    containerName: 'consumptionexports'
    variableName: 'AzureOptimization_ConsumptionContainer'
    variableDescription: 'The Storage Account container where Azure Consumption exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestConsumptionDaily'
    ingestDescription: 'Daily Azure Consumption ingests'
    ingestTimeOffset: 'PT2H'
    ingestFrequency: 'Day'
    ingestJobId: consumptionIngestJobId
    exportSchedule: consumptionExportsScheduleName
    exportJobId: consumptionExportJobId
  }
  {
    runbookName: aadObjectsExportsRunbookName
    isOneToMany: false
    containerName: 'aadobjectsexports'
    variableName: 'AzureOptimization_AADObjectsContainer'
    variableDescription: 'The Storage Account container where Microsoft Entra Objects exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestAADObjectsDaily'
    ingestDescription: 'Daily Microsoft Entra Objects ingests'
    ingestTimeOffset: 'PT2H'
    ingestFrequency: 'Day'
    ingestJobId: aadObjectsIngestJobId
    exportSchedule: aadObjectsExportsScheduleName
    exportJobId: aadObjectsExportJobId
  }
  {
    runbookName: argLoadBalancersExportsRunbookName
    isOneToMany: false
    containerName: 'arglbexports'
    variableName: 'AzureOptimization_ARGLoadBalancerContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph Load Balancer exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGLoadBalancersDaily'
    ingestDescription: 'Daily Azure Resource Graph Load Balancers ingests'
    ingestTimeOffset: 'PT1H31M'
    ingestFrequency: 'Day'
    ingestJobId: argLoadBalancersIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argLoadBalancersExportJobId
  }
  {
    runbookName: argAppGWsExportsRunbookName
    isOneToMany: false
    containerName: 'argappgwexports'
    variableName: 'AzureOptimization_ARGAppGatewayContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph Application Gateway exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGAppGWsDaily'
    ingestDescription: 'Daily Azure Resource Graph Application Gateways ingests'
    ingestTimeOffset: 'PT1H31M'
    ingestFrequency: 'Day'
    ingestJobId: argAppGWsIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argAppGWsExportJobId
  }
  {
    runbookName: argResContainersExportsRunbookName
    isOneToMany: false
    containerName: 'argrescontainersexports'
    variableName: 'AzureOptimization_ARGResourceContainersContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph Resource Containers exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGResourceContainersDaily'
    ingestDescription: 'Daily Azure Resource Graph Resource Containers ingests'
    ingestTimeOffset: 'PT1H32M'
    ingestFrequency: 'Day'
    ingestJobId: argResContainersIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argResContainersExportJobId
  }
  {
    runbookName: rbacExportsRunbookName
    isOneToMany: false
    containerName: 'rbacexports'
    variableName: 'AzureOptimization_RBACAssignmentsContainer'
    variableDescription: 'The Storage Account container where RBAC Assignments exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestRBACDaily'
    ingestDescription: 'Daily Azure RBAC ingests'
    ingestTimeOffset: 'PT1H32M'
    ingestFrequency: 'Day'
    ingestJobId: rbacIngestJobId
    exportSchedule: rbacExportsScheduleName
    exportJobId: rbacExportJobId
  }
  {
    runbookName: argNICExportsRunbookName
    isOneToMany: false
    containerName: 'argnicexports'
    variableName: 'AzureOptimization_ARGNICContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph NIC exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGNICsDaily'
    ingestDescription: 'Daily Azure Resource Graph NIC ingests'
    ingestTimeOffset: 'PT1H32M'
    ingestFrequency: 'Day'
    ingestJobId: argNICIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argNICExportJobId
  }
  {
    runbookName: argNSGExportsRunbookName
    isOneToMany: false
    containerName: 'argnsgexports'
    variableName: 'AzureOptimization_ARGNSGContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph NSG exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGNSGsDaily'
    ingestDescription: 'Daily Azure Resource Graph NSG ingests'
    ingestTimeOffset: 'PT1H32M'
    ingestFrequency: 'Day'
    ingestJobId: argNSGIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argNSGExportJobId
  }
  {
    runbookName: argVNetExportsRunbookName
    isOneToMany: false
    containerName: 'argvnetexports'
    variableName: 'AzureOptimization_ARGVNetContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph VNet exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGVNetsDaily'
    ingestDescription: 'Daily Azure Resource Graph Virtual Network ingests'
    ingestTimeOffset: 'PT1H33M'
    ingestFrequency: 'Day'
    ingestJobId: argVNetIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argVNetExportJobId
  }
  {
    runbookName: argPublicIpExportsRunbookName
    isOneToMany: false
    containerName: 'argpublicipexports'
    variableName: 'AzureOptimization_ARGPublicIpContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph Public IP exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGPublicIPsDaily'
    ingestDescription: 'Daily Azure Resource Graph Public IP ingests'
    ingestTimeOffset: 'PT1H33M'
    ingestFrequency: 'Day'
    ingestJobId: argPublicIPIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argPublicIPExportJobId
  }
  {
    runbookName: argSqlDbExportsRunbookName
    isOneToMany: false
    containerName: 'argsqldbexports'
    variableName: 'AzureOptimization_ARGSqlDatabaseContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph SQL DB exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGSqlDbDaily'
    ingestDescription: 'Daily Azure Resource Graph SQL DB ingests'
    ingestTimeOffset: 'PT1H33M'
    ingestFrequency: 'Day'
    ingestJobId: argSqlDbIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argSqlDbExportJobId
  }
  {
    runbookName: policyStateExportsRunbookName
    isOneToMany: false
    containerName: 'policystateexports'
    variableName: 'AzureOptimization_PolicyStatesContainer'
    variableDescription: 'The Storage Account container where Azure Policy State exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestPolicyStateDaily'
    ingestDescription: 'Daily Azure Policy State ingests'
    ingestTimeOffset: 'PT1H33M'
    ingestFrequency: 'Day'
    ingestJobId: policyStateIngestJobId
    exportSchedule: policyStateExportsScheduleName
    exportJobId: policyStateExportJobId
  }
  {
    runbookName: monitorExportsRunbookName
    isOneToMany: true
    containerName: 'azmonitorexports'
    variableName: 'AzureOptimization_AzMonitorContainer'
    variableDescription: 'The Storage Account container where Azure Monitor metrics exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestAzMonitorMetricsHourly'
    ingestDescription: 'Hourly Azure Monitor metrics ingests'
    ingestTimeOffset: 'PT2H'
    ingestFrequency: 'Hour'
    ingestJobId: monitorIngestJobId
    exportSchedule: null
    exportJobId: 'dummy'
  }
  {
    runbookName: argAppServicePlanExportsRunbookName
    isOneToMany: false
    containerName: 'argappserviceplanexports'
    variableName: 'AzureOptimization_ARGAppServicePlanContainer'
    variableDescription: 'The Storage Account container where Azure Resource Graph App Service Plan exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestARGAppServicePlanDaily'
    ingestDescription: 'Daily Azure Resource Graph App Service Plan ingests'
    ingestTimeOffset: 'PT1H34M'
    ingestFrequency: 'Day'
    ingestJobId: argAppServicePlanIngestJobId
    exportSchedule: argExportsScheduleName
    exportJobId: argAppServicePlanExportJobId
  }
  {
    runbookName: priceSheetExportsRunbookName
    isOneToMany: false
    containerName: 'pricesheetexports'
    variableName: 'AzureOptimization_PriceSheetContainer'
    variableDescription: 'The Storage Account container where Pricesheet exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestPricesheetWeekly'
    ingestDescription: 'Weekly Pricesheet ingests'
    ingestTimeOffset: 'PT2H'
    ingestFrequency: 'Week'
    ingestJobId: pricesheetIngestJobId
    exportSchedule: priceExportsScheduleName
    exportJobId: pricesheetExportJobId
  }
  {
    runbookName: reservationsPriceExportsRunbookName
    isOneToMany: false
    containerName: 'reservationspriceexports'
    variableName: 'AzureOptimization_ReservationsPriceContainer'
    variableDescription: 'The Storage Account container where Reservations Prices exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestReservationsPriceWeekly'
    ingestDescription: 'Weekly Reservations Prices ingests'
    ingestTimeOffset: 'PT2H'
    ingestFrequency: 'Week'
    ingestJobId: reservationPricesIngestJobId
    exportSchedule: priceExportsScheduleName
    exportJobId: reservationPricesExportJobId
  }
  {
    runbookName: reservationsExportsRunbookName
    isOneToMany: false
    containerName: 'reservationsexports'
    variableName: 'AzureOptimization_ReservationsContainer'
    variableDescription: 'The Storage Account container where Reservations Usage exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestReservationsUsageDaily'
    ingestDescription: 'Daily Reservations Usage ingests'
    ingestTimeOffset: 'PT2H30M'
    ingestFrequency: 'Day'
    ingestJobId: reservationUsageIngestJobId
    exportSchedule: reservationsUsageExportsScheduleName
    exportJobId: reservationUsageExportJobId
  }
  {
    runbookName: savingsPlansExportsRunbookName
    isOneToMany: false
    containerName: 'savingsplansexports'
    variableName: 'AzureOptimization_SavingsPlansContainer'
    variableDescription: 'The Storage Account container where Savings Plans Usage exports are dumped to'
    ingestSchedule: 'AzureOptimization_IngestSavingsPlansUsageDaily'
    ingestDescription: 'Daily Savings Plans Usage ingests'
    ingestTimeOffset: 'PT2H35M'
    ingestFrequency: 'Day'
    ingestJobId: savingsPlansUsageIngestJobId
    exportSchedule: savingsPlansUsageExportsScheduleName
    exportJobId: savingsPlansUsageExportJobId
  }
]
var csvParameterizedExports = [
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorVmssCpuMaxExportsScheduleName
    exportJobId: monitorVmssCpuMaxExportJobId
    parameters: {
      ResourceType: 'microsoft.compute/virtualmachinescalesets'
      TimeSpan: '01:00:00'
      aggregationType: 'Maximum'
      MetricNames: 'Percentage CPU'
      TimeGrain: '01:00:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorVmssCpuAvgExportsScheduleName
    exportJobId: monitorVmssCpuAvgExportJobId
    parameters: {
      ResourceType: 'microsoft.compute/virtualmachinescalesets'
      TimeSpan: '01:00:00'
      aggregationType: 'Average'
      MetricNames: 'Percentage CPU'
      TimeGrain: '01:00:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorVmssMemoryMinExportsScheduleName
    exportJobId: monitorVmssMemoryMinExportJobId
    parameters: {
      ResourceType: 'microsoft.compute/virtualmachinescalesets'
      TimeSpan: '01:00:00'
      aggregationType: 'Minimum'
      MetricNames: 'Available Memory Bytes'
      TimeGrain: '01:00:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorSqlDbDtuMaxExportsScheduleName
    exportJobId: monitorSqlDbDtuMaxExportJobId
    parameters: {
      ResourceType: 'microsoft.sql/servers/databases'
      ARGFilter: 'sku.tier in (\'Standard\',\'Premium\')'
      TimeSpan: '01:00:00'
      aggregationType: 'Maximum'
      MetricNames: 'dtu_consumption_percent'
      TimeGrain: '01:00:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorSqlDbDtuAvgExportsScheduleName
    exportJobId: monitorSqlDbDtuAvgExportJobId
    parameters: {
      ResourceType: 'microsoft.sql/servers/databases'
      ARGFilter: 'sku.tier in (\'Standard\',\'Premium\')'
      TimeSpan: '01:00:00'
      aggregationType: 'Average'
      AggregationOfType: 'Maximum'
      MetricNames: 'dtu_consumption_percent'
      TimeGrain: '00:01:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorAppServiceCpuMaxExportsScheduleName
    exportJobId: monitorAppServiceCpuMaxExportJobId
    parameters: {
      ResourceType: 'microsoft.web/serverfarms'
      ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\''
      TimeSpan: '01:00:00'
      aggregationType: 'Maximum'
      MetricNames: 'CpuPercentage'
      TimeGrain: '01:00:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorAppServiceCpuAvgExportsScheduleName
    exportJobId: monitorAppServiceCpuAvgExportJobId
    parameters: {
      ResourceType: 'microsoft.web/serverfarms'
      ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\''
      TimeSpan: '01:00:00'
      aggregationType: 'Average'
      AggregationOfType: 'Maximum'
      MetricNames: 'CpuPercentage'
      TimeGrain: '00:01:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorAppServiceMemoryMaxExportsScheduleName
    exportJobId: monitorAppServiceMemoryMaxExportJobId
    parameters: {
      ResourceType: 'microsoft.web/serverfarms'
      ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\''
      TimeSpan: '01:00:00'
      aggregationType: 'Maximum'
      MetricNames: 'MemoryPercentage'
      TimeGrain: '01:00:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorAppServiceMemoryAvgExportsScheduleName
    exportJobId: monitorAppServiceMemoryAvgExportJobId
    parameters: {
      ResourceType: 'microsoft.web/serverfarms'
      ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\''
      TimeSpan: '01:00:00'
      aggregationType: 'Average'
      AggregationOfType: 'Maximum'
      MetricNames: 'MemoryPercentage'
      TimeGrain: '00:01:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorDiskIOPSAvgExportsScheduleName
    exportJobId: monitorDiskIOPSAvgExportJobId
    parameters: {
      ResourceType: 'microsoft.compute/disks'
      ARGFilter: 'sku.name =~ \'Premium_LRS\' and properties.diskState != \'Unattached\''
      TimeSpan: '01:00:00'
      aggregationType: 'Average'
      AggregationOfType: 'Maximum'
      MetricNames: 'Composite Disk Read Operations/sec,Composite Disk Write Operations/sec'
      TimeGrain: '00:01:00'
    }
  }
  {
    runbookName: monitorExportsRunbookName
    exportSchedule: monitorDiskMBPsAvgExportsScheduleName
    exportJobId: monitorDiskMBPsAvgExportJobId
    parameters: {
      ResourceType: 'microsoft.compute/disks'
      ARGFilter: 'sku.name =~ \'Premium_LRS\' and properties.diskState != \'Unattached\''
      TimeSpan: '01:00:00'
      aggregationType: 'Average'
      AggregationOfType: 'Maximum'
      MetricNames: 'Composite Disk Read Bytes/sec,Composite Disk Write Bytes/sec'
      TimeGrain: '00:01:00'
    }
  }
]
var unattachedDisksRecommendationsRunbookName = 'Recommend-UnattachedDisksToBlobStorage'
var advisorCostAugmentedRecommendationsRunbookName = 'Recommend-AdvisorCostAugmentedToBlobStorage'
var advisorAsIsRecommendationsRunbookName = 'Recommend-AdvisorAsIsToBlobStorage'
var vmsHARecommendationsRunbookName = 'Recommend-VMsHighAvailabilityToBlobStorage'
var vmOptimizationsRecommendationsRunbookName = 'Recommend-VMOptimizationsToBlobStorage'
var aadExpiringCredsRecommendationsRunbookName = 'Recommend-AADExpiringCredentialsToBlobStorage'
var unusedLBsRecommendationsRunbookName = 'Recommend-UnusedLoadBalancersToBlobStorage'
var unusedAppGWsRecommendationsRunbookName = 'Recommend-UnusedAppGWsToBlobStorage'
var armOptimizationsRecommendationsRunbookName = 'Recommend-ARMOptimizationsToBlobStorage'
var vnetOptimizationsRecommendationsRunbookName = 'Recommend-VNetOptimizationsToBlobStorage'
var vmssOptimizationsRecommendationsRunbookName = 'Recommend-VMSSOptimizationsToBlobStorage'
var sqldbOptimizationsRecommendationsRunbookName = 'Recommend-SqlDbOptimizationsToBlobStorage'
var storageOptimizationsRecommendationsRunbookName = 'Recommend-StorageAccountOptimizationsToBlobStorage'
var appServiceOptimizationsRecommendationsRunbookName = 'Recommend-AppServiceOptimizationsToBlobStorage'
var diskOptimizationsRecommendationsRunbookName = 'Recommend-DiskOptimizationsToBlobStorage'
var cleanUpOlderRecommendationsRunbookName = 'CleanUp-OlderRecommendationsFromSqlServer'
var recommendations = [
  {
    recommendationJobId: unattachedDisksRecommendationJobId
    runbookName: unattachedDisksRecommendationsRunbookName
  }
  {
    recommendationJobId: advisorCostAugmentedRecommendationJobId
    runbookName: advisorCostAugmentedRecommendationsRunbookName
  }
  {
    recommendationJobId: advisorAsIsRecommendationJobId
    runbookName: advisorAsIsRecommendationsRunbookName
  }
  {
    recommendationJobId: vmsHaRecommendationJobId
    runbookName: vmsHARecommendationsRunbookName
  }
  {
    recommendationJobId: vmOptimizationsRecommendationJobId
    runbookName: vmOptimizationsRecommendationsRunbookName
  }
  {
    recommendationJobId: aadExpiringCredsRecommendationJobId
    runbookName: aadExpiringCredsRecommendationsRunbookName
  }
  {
    recommendationJobId: unusedLoadBalancersRecommendationJobId
    runbookName: unusedLBsRecommendationsRunbookName
  }
  {
    recommendationJobId: unusedAppGWsRecommendationJobId
    runbookName: unusedAppGWsRecommendationsRunbookName
  }
  {
    recommendationJobId: armOptimizationsRecommendationJobId
    runbookName: armOptimizationsRecommendationsRunbookName
  }
  {
    recommendationJobId: vnetOptimizationsRecommendationJobId
    runbookName: vnetOptimizationsRecommendationsRunbookName
  }
  {
    recommendationJobId: vmssOptimizationsRecommendationJobId
    runbookName: vmssOptimizationsRecommendationsRunbookName
  }
  {
    recommendationJobId: sqldbOptimizationsRecommendationJobId
    runbookName: sqldbOptimizationsRecommendationsRunbookName
  }
  {
    recommendationJobId: storageOptimizationsRecommendationJobId
    runbookName: storageOptimizationsRecommendationsRunbookName
  }
  {
    recommendationJobId: appServiceOptimizationsRecommendationJobId
    runbookName: appServiceOptimizationsRecommendationsRunbookName
  }
  {
    recommendationJobId: diskOptimizationsRecommendationJobId
    runbookName: diskOptimizationsRecommendationsRunbookName
  }
]
var remediationLogsContainerName = 'remediationlogs'
var recommendationsContainerName = 'recommendationsexports'
var csvIngestRunbookName = 'Ingest-OptimizationCSVExportsToLogAnalytics'
var recommendationsIngestRunbookName = 'Ingest-RecommendationsToSQLServer'
var recommendationsLogAnalyticsIngestRunbookName = 'Ingest-RecommendationsToLogAnalytics'
var suppressionsLogAnalyticsIngestRunbookName = 'Ingest-SuppressionsToLogAnalytics'
var advisorRightSizeFilteredRemediationRunbookName = 'Remediate-AdvisorRightSizeFiltered'
var longDeallocatedVMsFilteredRemediationRunbookName = 'Remediate-LongDeallocatedVMsFiltered'
var unattachedDisksFilteredRemediationRunbookName = 'Remediate-UnattachedDisksFiltered'
var remediationLogsIngestScheduleName = 'AzureOptimization_IngestRemediationLogsDaily'
var recommendationsScheduleName = 'AzureOptimization_RecommendationsWeekly'
var recommendationsIngestScheduleName = 'AzureOptimization_IngestRecommendationsWeekly'
var suppressionsIngestScheduleName = 'AzureOptimization_IngestSuppressionsWeekly'
var recommendationsCleanUpScheduleName = 'AzureOptimization_CleanUpRecommendationsWeekly'
var Az_Accounts = {
  name: 'Az.Accounts'
  url: 'https://www.powershellgallery.com/api/v2/package/Az.Accounts/2.12.1'
}
var Microsoft_Graph_Authentication = {
  name: 'Microsoft.Graph.Authentication'
  url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Authentication/2.4.0'
}
var psModules = [
  {
    name: 'Az.Compute'
    url: 'https://www.powershellgallery.com/api/v2/package/Az.Compute/5.7.0'
  }
  {
    name: 'Az.OperationalInsights'
    url: 'https://www.powershellgallery.com/api/v2/package/Az.OperationalInsights/3.2.0'
  }
  {
    name: 'Az.ResourceGraph'
    url: 'https://www.powershellgallery.com/api/v2/package/Az.ResourceGraph/0.13.0'
  }
  {
    name: 'Az.Storage'
    url: 'https://www.powershellgallery.com/api/v2/package/Az.Storage/5.5.0'
  }
  {
    name: 'Az.Resources'
    url: 'https://www.powershellgallery.com/api/v2/package/Az.Resources/6.6.0'
  }
  {
    name: 'Az.Monitor'
    url: 'https://www.powershellgallery.com/api/v2/package/Az.Monitor/4.4.1'
  }
  {
    name: 'Az.PolicyInsights'
    url: 'https://www.powershellgallery.com/api/v2/package/Az.PolicyInsights/1.6.0'
  }
  {
    name: 'Microsoft.Graph.Users'
    url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Users/2.4.0'
  }
  {
    name: 'Microsoft.Graph.Groups'
    url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Groups/2.4.0'
  }
  {
    name: 'Microsoft.Graph.Applications'
    url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Applications/2.4.0'
  }
  {
    name: 'Microsoft.Graph.Identity.DirectoryManagement'
    url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Identity.DirectoryManagement/2.4.0'
  }
]
var runbooks = [
  {
    name: advisorExportsRunbookName
    version: '1.4.2.1'
    description: 'Exports Azure Advisor recommendations to Blob Storage using the Advisor API'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${advisorExportsRunbookName}.ps1')
  }
  {
    name: argDisksExportsRunbookName
    version: '1.3.4.1'
    description: 'Exports Managed Disks properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argDisksExportsRunbookName}.ps1')
  }
  {
    name: argVhdExportsRunbookName
    version: '1.1.4.1'
    description: 'Exports Unmanaged Disks (owned by a VM) properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVhdExportsRunbookName}.ps1')
  }
  {
    name: argVmExportsRunbookName
    version: '1.4.4.1'
    description: 'Exports Virtual Machine properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVmExportsRunbookName}.ps1')
  }
  {
    name: argVmssExportsRunbookName
    version: '1.0.2.1'
    description: 'Exports VMSS properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVmssExportsRunbookName}.ps1')
  }
  {
    name: argAvailSetExportsRunbookName
    version: '1.1.4.1'
    description: 'Exports Availability Set properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAvailSetExportsRunbookName}.ps1')
  }
  {
    name: consumptionExportsRunbookName
    version: '2.0.4.1'
    description: 'Exports Azure Consumption events to Blob Storage using Azure Consumption API'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${consumptionExportsRunbookName}.ps1')
  }
  {
    name: aadObjectsExportsRunbookName
    version: '1.2.2.1'
    description: 'Exports Azure AAD Objects to Blob Storage using Azure ARM API'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${aadObjectsExportsRunbookName}.ps1')
  }
  {
    name: argLoadBalancersExportsRunbookName
    version: '1.1.4.1'
    description: 'Exports Load Balancer properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argLoadBalancersExportsRunbookName}.ps1')
  }
  {
    name: argAppGWsExportsRunbookName
    version: '1.1.4.1'
    description: 'Exports Application Gateway properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAppGWsExportsRunbookName}.ps1')
  }
  {
    name: argResContainersExportsRunbookName
    version: '1.0.5.1'
    description: 'Exports Resource Containers properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argResContainersExportsRunbookName}.ps1')
  }
  {
    name: rbacExportsRunbookName
    version: '1.0.4.1'
    description: 'Exports RBAC assignments to Blob Storage using ARM and Microsoft Entra'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${rbacExportsRunbookName}.ps1')
  }
  {
    name: argNICExportsRunbookName
    version: '1.0.2.1'
    description: 'Exports NIC properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argNICExportsRunbookName}.ps1')
  }
  {
    name: argNSGExportsRunbookName
    version: '1.0.2.1'
    description: 'Exports NSG properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argNSGExportsRunbookName}.ps1')
  }
  {
    name: argPublicIpExportsRunbookName
    version: '1.0.2.1'
    description: 'Exports Public IP properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argPublicIpExportsRunbookName}.ps1')
  }
  {
    name: argVNetExportsRunbookName
    version: '1.0.2.1'
    description: 'Exports VNet properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVNetExportsRunbookName}.ps1')
  }
  {
    name: argSqlDbExportsRunbookName
    version: '1.0.2.1'
    description: 'Exports SQL DB properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argSqlDbExportsRunbookName}.ps1')
  }
  {
    name: policyStateExportsRunbookName
    version: '1.0.3.1'
    description: 'Exports Azure Policy State to Blob Storage'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${policyStateExportsRunbookName}.ps1')
  }
  {
    name: monitorExportsRunbookName
    version: '1.0.2.1'
    description: 'Exports Azure Monitor metrics to Blob Storage'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${monitorExportsRunbookName}.ps1')
  }
  {
    name: argAppServicePlanExportsRunbookName
    version: '1.0.1.1'
    description: 'Exports App Service Plan properties to Blob Storage using Azure Resource Graph'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAppServicePlanExportsRunbookName}.ps1')
  }
  {
    name: reservationsExportsRunbookName
    version: '1.1.2.1'
    description: 'Exports Reservations Usage to Blob Storage using the EA or MCA APIs'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${reservationsExportsRunbookName}.ps1')
  }
  {
    name: reservationsPriceExportsRunbookName
    version: '1.0.1.1'
    description: 'Exports Reservations Prices to Blob Storage using the Retail Prices API'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${reservationsPriceExportsRunbookName}.ps1')
  }
  {
    name: priceSheetExportsRunbookName
    version: '1.1.1.1'
    description: 'Exports Price Sheet to Blob Storage using the EA or MCA APIs'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${priceSheetExportsRunbookName}.ps1')
  }
  {
    name: savingsPlansExportsRunbookName
    version: '1.0.0.0'
    description: 'Exports Savings Plans Usage to Blob Storage using the EA or MCA APIs'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${savingsPlansExportsRunbookName}.ps1')
  }
  {
    name: csvIngestRunbookName
    version: '1.5.0.0'
    description: 'Ingests CSV blobs as custom logs to Log Analytics'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/data-collection/${csvIngestRunbookName}.ps1')
  }
  {
    name: unattachedDisksRecommendationsRunbookName
    version: '2.4.8.0'
    description: 'Generates unattached disks recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${unattachedDisksRecommendationsRunbookName}.ps1')
  }
  {
    name: advisorCostAugmentedRecommendationsRunbookName
    version: '2.9.1.0'
    description: 'Generates augmented Advisor Cost recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorCostAugmentedRecommendationsRunbookName}.ps1')
  }
  {
    name: advisorAsIsRecommendationsRunbookName
    version: '1.5.5.0'
    description: 'Generates all types of Advisor recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorAsIsRecommendationsRunbookName}.ps1')
  }
  {
    name: vmsHARecommendationsRunbookName
    version: '1.0.3.0'
    description: 'Generates VMs High Availability recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmsHARecommendationsRunbookName}.ps1')
  }
  {
    name: vmOptimizationsRecommendationsRunbookName
    version: '1.0.0.0'
    description: 'Generates VM optimizations recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmOptimizationsRecommendationsRunbookName}.ps1')
  }
  {
    name: aadExpiringCredsRecommendationsRunbookName
    version: '1.1.10.0'
    description: 'Generates AAD Objects with expiring credentials recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${aadExpiringCredsRecommendationsRunbookName}.ps1')
  }
  {
    name: unusedLBsRecommendationsRunbookName
    version: '1.2.9.0'
    description: 'Generates unused Load Balancers recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedLBsRecommendationsRunbookName}.ps1')
  }
  {
    name: unusedAppGWsRecommendationsRunbookName
    version: '1.2.9.0'
    description: 'Generates unused Application Gateways recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedAppGWsRecommendationsRunbookName}.ps1')
  }
  {
    name: armOptimizationsRecommendationsRunbookName
    version: '1.0.3.0'
    description: 'Generates ARM optimizations recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${armOptimizationsRecommendationsRunbookName}.ps1')
  }
  {
    name: vnetOptimizationsRecommendationsRunbookName
    version: '1.0.4.0'
    description: 'Generates Virtual Network optimizations recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${vnetOptimizationsRecommendationsRunbookName}.ps1')
  }
  {
    name: vmssOptimizationsRecommendationsRunbookName
    version: '1.1.1.0'
    description: 'Generates VM Scale Set optimizations recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmssOptimizationsRecommendationsRunbookName}.ps1')
  }
  {
    name: sqldbOptimizationsRecommendationsRunbookName
    version: '1.1.2.0'
    description: 'Generates SQL DB optimizations recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${sqldbOptimizationsRecommendationsRunbookName}.ps1')
  }
  {
    name: storageOptimizationsRecommendationsRunbookName
    version: '1.0.3.0'
    description: 'Generates Storage Account optimizations recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${storageOptimizationsRecommendationsRunbookName}.ps1')
  }
  {
    name: appServiceOptimizationsRecommendationsRunbookName
    version: '1.0.3.0'
    description: 'Generates App Service optimizations recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${appServiceOptimizationsRecommendationsRunbookName}.ps1')
  }
  {
    name: diskOptimizationsRecommendationsRunbookName
    version: '1.1.1.0'
    description: 'Generates Disk optimizations recommendations'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${diskOptimizationsRecommendationsRunbookName}.ps1')
  }
  {
    name: recommendationsIngestRunbookName
    version: '1.6.5.0'
    description: 'Ingests JSON-based recommendations into an Azure SQL Database'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${recommendationsIngestRunbookName}.ps1')
  }
  {
    name: recommendationsLogAnalyticsIngestRunbookName
    version: '1.0.2.0'
    description: 'Ingests JSON-based recommendations into Log Analytics'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${recommendationsLogAnalyticsIngestRunbookName}.ps1')
  }
  {
    name: suppressionsLogAnalyticsIngestRunbookName
    version: '1.0.0.0'
    description: 'Ingests suppressions into Log Analytics'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/recommendations/${suppressionsLogAnalyticsIngestRunbookName}.ps1')
  }
  {
    name: advisorRightSizeFilteredRemediationRunbookName
    version: '1.2.4.0'
    description: 'Remediates Azure Advisor right-size recommendations given fit and tag filters'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/remediations/${advisorRightSizeFilteredRemediationRunbookName}.ps1')
  }
  {
    name: longDeallocatedVMsFilteredRemediationRunbookName
    version: '1.0.3.0'
    description: 'Remediates long-deallocated VMs recommendations given fit and tag filters'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/remediations/${longDeallocatedVMsFilteredRemediationRunbookName}.ps1')
  }
  {
    name: unattachedDisksFilteredRemediationRunbookName
    version: '1.0.3.0'
    description: 'Remediates unattached disks recommendations given fit and tag filters'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/remediations/${unattachedDisksFilteredRemediationRunbookName}.ps1')
  }
  {
    name: cleanUpOlderRecommendationsRunbookName
    version: '1.0.0.0'
    description: 'Cleans up older recommendations from SQL Database'
    type: 'PowerShell'
    scriptUri: uri(templateLocation, 'runbooks/maintenance/${cleanUpOlderRecommendationsRunbookName}.ps1')
  }
]
var automationVariables = [
  {
    name: 'AzureOptimization_CloudEnvironment'
    description: 'Azure Cloud environment (e.g., AzureCloud, AzureChinaCloud, etc.)'
    value: '"${cloudEnvironment}"'
  }
  {
    name: 'AzureOptimization_AuthenticationOption'
    description: 'Runbook authentication type (RunAsAccount or ManagedIdentity)'
    value: '"${authenticationOption}"'
  }
  {
    name: 'AzureOptimization_StorageSink'
    description: 'The Azure Storage Account where data source exports are dumped to'
    value: '"${storageAccountName}"'
  }
  {
    name: 'AzureOptimization_StorageSinkRG'
    description: 'The resource group for the Azure Storage Account sink'
    value: '"${resourceGroup().name}"'
  }
  {
    name: 'AzureOptimization_StorageSinkSubId'
    description: 'The subscription Id for the Azure Storage Account sink'
    value: '"${subscription().subscriptionId}"'
  }
  {
    name: 'AzureOptimization_ConsumptionOffsetDays'
    description: 'The offset (in days) for querying for consumption data'
    value: 3
  }
  {
    name: 'AzureOptimization_AdvisorFilter'
    description: 'The category filter to use for Azure Advisor (non-Cost) recommendations exports'
    value: '"HighAvailability,Security,Performance,OperationalExcellence"'
  }
  {
    name: 'AzureOptimization_ReferenceRegion'
    description: 'The Azure region used as a reference for getting details about Azure VM sizes available'
    value: '"${projectLocation}"'
  }
  {
    name: 'AzureOptimization_SQLServerDatabase'
    description: 'The Azure SQL Database name for the ingestion control and recommendations tables'
    value: '"${sqlDatabaseName}"'
  }
  {
    name: 'AzureOptimization_LogAnalyticsChunkSize'
    description: 'The size (in rows) for each chunk of Log Analytics ingestion request'
    value: 6000
  }
  {
    name: 'AzureOptimization_StorageBlobsPageSize'
    description: 'The size (in blobs count) for each page of Storage Account container blob listing'
    value: 1000
  }
  {
    name: 'AzureOptimization_SQLServerInsertSize'
    description: 'The size (in inserted lines) for each page of recommendations ingestion into the SQL Database'
    value: 900
  }
  {
    name: 'AzureOptimization_LogAnalyticsLogPrefix'
    description: 'The prefix for all Azure Optimization custom log tables in Log Analytics'
    value: '"AzureOptimization"'
  }
  {
    name: 'AzureOptimization_LogAnalyticsWorkspaceName'
    description: 'The Log Analytics Workspace Name where optimization data will be ingested'
    value: '"${logAnalyticsWorkspaceName}"'
  }
  {
    name: 'AzureOptimization_LogAnalyticsWorkspaceRG'
    description: 'The resource group for the Log Analytics Workspace where optimization data will be ingested'
    value: '"${((!logAnalyticsReuse) ? resourceGroup().name : logAnalyticsWorkspaceRG)}"'
  }
  {
    name: 'AzureOptimization_LogAnalyticsWorkspaceSubId'
    description: 'The Azure subscription for the Log Analytics Workspace where optimization data will be ingested'
    value: '"${subscription().subscriptionId}"'
  }
  {
    name: 'AzureOptimization_LogAnalyticsWorkspaceTenantId'
    description: 'The Microsoft Entra tenant for the Log Analytics Workspace where optimization data will be ingested'
    value: '"${subscription().tenantId}"'
  }
  {
    name: 'AzureOptimization_PriceSheetMeterCategories'
    description: 'Comma-separated meter categories to be included in the Price Sheet (remove variable to include all categories)'
    value: '"Virtual Machines,Storage"'
  }
  {
    name: 'AzureOptimization_RetailPricesCurrencyCode'
    description: 'The currency code to be used for the retail prices exports (used for Reservations prices)'
    value: '"EUR"'
  }
  {
    name: 'AzureOptimization_RecommendAdvisorPeriodInDays'
    description: 'The period (in days) to look back for Advisor exported recommendations'
    value: 7
  }
  {
    name: 'AzureOptimization_RecommendationLongDeallocatedVmsIntervalDays'
    description: 'The period (in days) for considering a VM long deallocated'
    value: 30
  }
  {
    name: 'AzureOptimization_PerfPercentileCpu'
    description: 'The percentile to be used for processor metrics'
    value: 99
  }
  {
    name: 'AzureOptimization_PerfPercentileMemory'
    description: 'The percentile to be used for memory metrics'
    value: 99
  }
  {
    name: 'AzureOptimization_PerfPercentileNetwork'
    description: 'The percentile to be used for network metrics'
    value: 99
  }
  {
    name: 'AzureOptimization_PerfPercentileDisk'
    description: 'The percentile to be used for disk metrics'
    value: 99
  }
  {
    name: 'AzureOptimization_PerfPercentileSqlDtu'
    description: 'The percentile to be used for SQL DB DTU metrics'
    value: 99
  }
  {
    name: 'AzureOptimization_PerfThresholdCpuPercentage'
    description: 'The processor usage percentage threshold above which the fit score is decreased or below which the instance is considered underutilized'
    value: 30
  }
  {
    name: 'AzureOptimization_PerfThresholdMemoryPercentage'
    description: 'The memory usage percentage threshold above which the fit score is decreased or below which the instance is considered underutilized'
    value: 50
  }
  {
    name: 'AzureOptimization_PerfThresholdCpuDegradedMaxPercentage'
    description: 'The maximum processor usage percentage threshold above which the instance is considered degraded'
    value: 95
  }
  {
    name: 'AzureOptimization_PerfThresholdCpuDegradedAvgPercentage'
    description: 'The average processor usage percentage threshold above which the instance is considered degraded'
    value: 75
  }
  {
    name: 'AzureOptimization_PerfThresholdMemoryDegradedPercentage'
    description: 'The memory usage percentage threshold above which the instance is considered degraded'
    value: 90
  }
  {
    name: 'AzureOptimization_PerfThresholdNetworkMbps'
    description: 'The network usage threshold (in Mbps) above which the fit score is decreased'
    value: 750
  }
  {
    name: 'AzureOptimization_PerfThresholdCpuShutdownPercentage'
    description: 'The processor usage percentage threshold above which the fit score is decreased (shutdown scenarios)'
    value: 5
  }
  {
    name: 'AzureOptimization_PerfThresholdMemoryShutdownPercentage'
    description: 'The memory usage percentage threshold above which the fit score is decreased (shutdown scenarios)'
    value: 100
  }
  {
    name: 'AzureOptimization_PerfThresholdNetworkShutdownMbps'
    description: 'The network usage threshold (in Mbps) above which the fit score is decreased (shutdown scenarios)'
    value: 10
  }
  {
    name: 'AzureOptimization_PerfThresholdDtuPercentage'
    description: 'The DTU usage percentage threshold below which a SQL Database instance is considered underutilized'
    value: 40
  }
  {
    name: 'AzureOptimization_PerfThresholdDtuDegradedPercentage'
    description: 'The DTU usage percentage threshold above which a SQL Database instance is considered performance degraded'
    value: 75
  }
  {
    name: 'AzureOptimization_PerfThresholdDiskIOPSPercentage'
    description: 'The IOPS usage percentage threshold below which a Disk is considered underutilized'
    value: 5
  }
  {
    name: 'AzureOptimization_PerfThresholdDiskMBsPercentage'
    description: 'The throughput (MBps) usage percentage threshold below which a Disk is considered underutilized'
    value: 5
  }
  {
    name: 'AzureOptimization_RemediateRightSizeMinFitScore'
    description: 'The minimum fit score for right-size remediation'
    value: '"5.0"'
  }
  {
    name: 'AzureOptimization_RemediateRightSizeMinWeeksInARow'
    description: 'The minimum number of weeks in a row required for a right-size recommendation to be remediated'
    value: 4
  }
  {
    name: 'AzureOptimization_RecommendationAdvisorCostRightSizeId'
    description: 'The Azure Advisor VM right-size recommendation ID'
    value: '"e10b1381-5f0a-47ff-8c7b-37bd13d7c974"'
  }
  {
    name: 'AzureOptimization_RemediateLongDeallocatedVMsMinFitScore'
    description: 'The minimum fit score for long-deallocated VM remediation'
    value: '"5.0"'
  }
  {
    name: 'AzureOptimization_RemediateLongDeallocatedVMsMinWeeksInARow'
    description: 'The minimum number of weeks in a row required for a long-deallocated VM recommendation to be remediated'
    value: 4
  }
  {
    name: 'AzureOptimization_RecommendationLongDeallocatedVMsId'
    description: 'The long deallocated VM recommendation ID'
    value: '"c320b790-2e58-452a-aa63-7b62c383ad8a"'
  }
  {
    name: 'AzureOptimization_RemediateUnattachedDisksMinFitScore'
    description: 'The minimum fit score for unattached disk remediation'
    value: '"5.0"'
  }
  {
    name: 'AzureOptimization_RemediateUnattachedDisksMinWeeksInARow'
    description: 'The minimum number of weeks in a row required for a unattached disk recommendation to be remediated'
    value: 4
  }
  {
    name: 'AzureOptimization_RemediateUnattachedDisksAction'
    description: 'The action for the unattached disk recommendation to be remediated (Delete or Downsize)'
    value: '"Delete"'
  }
  {
    name: 'AzureOptimization_RecommendationUnattachedDisksId'
    description: 'The unattached disk recommendation ID'
    value: '"c84d5e86-e2d6-4d62-be7c-cecfbd73b0db"'
  }
  {
    name: 'AzureOptimization_RecommendationAADMinCredValidityDays'
    description: 'The minimum validity of an AAD Object credential in days'
    value: 30
  }
  {
    name: 'AzureOptimization_RecommendationAADMaxCredValidityYears'
    description: 'The maximum validity of an AAD Object credential in years'
    value: 2
  }
  {
    name: 'AzureOptimization_AADObjectsFilter'
    description: 'The Microsoft Entra object types to export'
    value: '"Application,ServicePrincipal,User,Group"'
  }
  {
    name: 'AzureOptimization_RecommendationRBACAssignmentsPercentageThreshold'
    description: 'The percentage threshold (used to trigger recommendations) for total RBAC assignments limits'
    value: 80
  }
  {
    name: 'AzureOptimization_RecommendationResourceGroupsPerSubPercentageThreshold'
    description: 'The percentage threshold (used to trigger recommendations) for resource group count limits'
    value: 80
  }
  {
    name: 'AzureOptimization_RecommendationVNetSubnetMaxUsedPercentageThreshold'
    description: 'The percentage threshold (used to trigger recommendations) for maximum subnet address space usage'
    value: 80
  }
  {
    name: 'AzureOptimization_RecommendationVNetSubnetMinUsedPercentageThreshold'
    description: 'The percentage threshold (used to trigger recommendations) for minimum subnet address space usage'
    value: 5
  }
  {
    name: 'AzureOptimization_RecommendationVNetSubnetEmptyMinAgeInDays'
    description: 'The minimum age (in days) for an empty subnet to trigger an NSG rule recommendation'
    value: 30
  }
  {
    name: 'AzureOptimization_RecommendationsMaxAgeInDays'
    description: 'The maximum age (in days) for a recommendation to be kept in the SQL database'
    value: 365
  }
  {
    name: 'AzureOptimization_RecommendationStorageAcountGrowthThresholdPercentage'
    description: 'The minimum Storage Account growth percentage required to flag Storage as not having a retention policy in place'
    value: 5
  }
  {
    name: 'AzureOptimization_RecommendationStorageAcountGrowthMonthlyCostThreshold'
    description: 'The minimum monthly cost (in your EA/MCA currency) required to flag Storage as not having a retention policy in place'
    value: 50
  }
  {
    name: 'AzureOptimization_RecommendationStorageAcountGrowthLookbackDays'
    description: 'The lookback period (in days) for analyzing Storage Account growth'
    value: 30
  }
]

resource logAnalyticsWorkspace 'microsoft.operationalinsights/workspaces@2020-08-01' = if (!logAnalyticsReuse) {
  name: logAnalyticsWorkspaceName
  location: projectLocation
  tags: resourceTags
  properties: {
    sku: {
      name: 'pergb2018'
    }
    retentionInDays: logAnalyticsRetentionDays
  }
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: storageAccountName
  location: projectLocation
  tags: resourceTags
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    allowBlobPublicAccess: false
    networkAcls: {
      bypass: 'AzureServices'
      virtualNetworkRules: []
      ipRules: []
      defaultAction: 'Allow'
    }
    supportsHttpsTrafficOnly: true
    encryption: {
      services: {
        file: {
          enabled: true
        }
        blob: {
          enabled: true
        }
      }
      keySource: 'Microsoft.Storage'
    }
    minimumTlsVersion: 'TLS1_2'
    accessTier: 'Cool'
  }
}

resource storageBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = {
  parent: storageAccount
  name: 'default'
  properties: {
    cors: {
      corsRules: []
    }
    deleteRetentionPolicy: {
      enabled: false
    }
  }
}

resource storageCsvExportsContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = [for item in csvExports: {
  name: '${storageAccountName}/default/${item.containerName}'
  properties: {
    publicAccess: 'None'
  }
  dependsOn: [
    storageBlobServices
    storageAccount
  ]
}]

resource storageRecommendationsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = {
  name: '${storageAccountName}/default/${recommendationsContainerName}'
  properties: {
    publicAccess: 'None'
  }
  dependsOn: [
    storageBlobServices
    storageAccount
  ]
}

resource storageRemediationLogsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = {
  name: '${storageAccountName}/default/${remediationLogsContainerName}'
  properties: {
    publicAccess: 'None'
  }
  dependsOn: [
    storageBlobServices
    storageAccount
  ]
}

resource storageLifecycleManagementPolicy 'Microsoft.Storage/storageAccounts/managementPolicies@2021-02-01' = {
  parent: storageAccount
  name: 'default'
  properties: {
    policy: {
      rules: [
        {
          enabled: true
          name: 'Clean6MonthsOldBlobs'
          type: 'Lifecycle'
          definition: {
            actions: {
              baseBlob: {
                delete: {
                  daysAfterModificationGreaterThan: 180
                }
              }
              snapshot: {
                delete: {
                  daysAfterCreationGreaterThan: 180
                }
              }
              version: {
                delete: {
                  daysAfterCreationGreaterThan: 180
                }
              }
            }
            filters: {
              blobTypes: [
                'blockBlob'
              ]
            }
          }
        }
      ]
    }
  }
  dependsOn: [
    storageBlobServices
  ]
}

resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
  name: sqlServerName
  location: projectLocation
  tags: resourceTags
  properties: {
    administratorLogin: sqlAdminLogin
    administratorLoginPassword: sqlAdminPassword
    version: '12.0'
    publicNetworkAccess: 'Enabled'
    minimalTlsVersion: '1.2'
  }
}

resource sqlServerFirewall 'Microsoft.Sql/servers/firewallRules@2022-05-01-preview' = {
  parent: sqlServer
  name: 'AllowAllWindowsAzureIps'
  properties: {
    endIpAddress: '0.0.0.0'
    startIpAddress: '0.0.0.0'
  }
}

resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-05-01-preview' = {
  parent: sqlServer
  name: sqlDatabaseName
  location: projectLocation
  tags: resourceTags
  sku: {
    name: 'Basic'
    tier: 'Basic'
    capacity: 5
  }
  properties: {
    collation: 'SQL_Latin1_General_CP1_CI_AS'
    maxSizeBytes: 2147483648
    catalogCollation: 'SQL_Latin1_General_CP1_CI_AS'
    zoneRedundant: false
    readScale: 'Disabled'
    autoPauseDelay: 60
    requestedBackupStorageRedundancy: 'Geo'
  }
}

resource sqlServerName_sqlDatabaseName_default 'Microsoft.Sql/servers/databases/backupShortTermRetentionPolicies@2022-05-01-preview' = {
  name: '${sqlServerName}/${sqlDatabaseName}/default'
  properties: {
    retentionDays: sqlBackupRetentionDays
  }
  dependsOn: [
    sqlDatabase
    sqlServer
  ]
}

resource automationAccount 'Microsoft.Automation/automationAccounts@2020-01-13-preview' = {
  name: automationAccountName
  location: projectLocation
  tags: resourceTags
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    sku: {
      name: 'Basic'
    }
  }
}

resource automationModule_Az_Accounts 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = {
  parent: automationAccount
  name: Az_Accounts.name
  tags: resourceTags
  properties: {
    contentLink: {
      uri: Az_Accounts.url
    }
  }
}

resource automationModule_Microsoft_Graph_Authentication 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = {
  parent: automationAccount
  name: Microsoft_Graph_Authentication.name
  tags: resourceTags
  properties: {
    contentLink: {
      uri: Microsoft_Graph_Authentication.url
    }
  }
}

resource automationModule_All 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = [for item in psModules: {
  parent: automationAccount  
  name: item.name
  tags: resourceTags
  properties: {
    contentLink: {
      uri: item.url
    }
  }
  dependsOn: [
    automationModule_Az_Accounts
    automationModule_Microsoft_Graph_Authentication
  ]
}]

resource automationRunbooks 'Microsoft.Automation/automationAccounts/runbooks@2020-01-13-preview' = [for item in runbooks: {
  parent: automationAccount  
  name: item.name
  tags: resourceTags
  location: projectLocation
  properties: {
    runbookType: item.type
    logProgress: false
    logVerbose: false
    description: item.description
    publishContentLink: {
      uri: item.scriptUri
      version: item.version
    }
  }
  dependsOn: [
    automationModule_All
  ]
}]

resource automationVariablesAll 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = [for item in automationVariables: {
  parent: automationAccount  
  name: item.name
  properties: {
    description: item.description
    value: item.value
  }
}]

resource automationVariables_csvExports 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = [for item in csvExports: {
  parent: automationAccount  
  name: item.variableName
  properties: {
    description: item.variableDescription
    value: '"${item.containerName}"'
  }
}]

resource auto
Download .txt
gitextract_09u_p9cv/

├── .github/
│   └── workflows/
│       ├── continuous-deployment-dev-new.yml
│       ├── continuous-deployment-dev.yml
│       └── continuous-deployment.yml
├── .gitignore
├── Deploy-AzureOptimizationEngine.ps1
├── LICENSE
├── README.md
├── Reset-AutomationSchedules.ps1
├── Setup-BenefitsUsageDependencies.ps1
├── Setup-DataCollectionRules.ps1
├── Setup-LogAnalyticsWorkspaces.ps1
├── Suppress-Recommendation.ps1
├── azuredeploy-nested.bicep
├── azuredeploy.bicep
├── custom-recommendations-types.json
├── docs/
│   ├── configuring-workspaces.md
│   ├── customizing-aoe.md
│   └── suppressing-recommendations.md
├── model/
│   ├── filters-table.sql
│   ├── loganalyticsingestcontrol-initialize.sql
│   ├── loganalyticsingestcontrol-table.sql
│   ├── loganalyticsingestcontrol-upgrade.sql
│   ├── recommendations-sp.sql
│   ├── recommendations-table.sql
│   ├── sqlserveringestcontrol-initialize.sql
│   └── sqlserveringestcontrol-table.sql
├── perfcounters.json
├── queries/
│   ├── rbac-spns-keys-expiring.kql
│   ├── rbac-spns-roles-aad-all.kql
│   ├── rbac-spns-roles-arm-all.kql
│   ├── rbac-spns-roles-arm-privileged.kql
│   ├── rbac-users-roles-aad-all.kql
│   ├── rbac-users-roles-arm-privileged.kql
│   ├── rbac-users-roles-guests-privileged.kql
│   └── rbac-users-roles-guests.kql
├── runbooks/
│   ├── data-collection/
│   │   ├── Export-AADObjectsToBlobStorage.ps1
│   │   ├── Export-ARGAppGatewayPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGAppServicePlanPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGLoadBalancerPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGManagedDisksPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGNICPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGNSGPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGPublicIpPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGResourceContainersPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGSqlDatabasePropertiesToBlobStorage.ps1
│   │   ├── Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGVMSSPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGVNetPropertiesToBlobStorage.ps1
│   │   ├── Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1
│   │   ├── Export-AdvisorRecommendationsToBlobStorage.ps1
│   │   ├── Export-AzMonitorMetricsToBlobStorage.ps1
│   │   ├── Export-ConsumptionToBlobStorage.ps1
│   │   ├── Export-PolicyComplianceToBlobStorage.ps1
│   │   ├── Export-PriceSheetToBlobStorage.ps1
│   │   ├── Export-RBACAssignmentsToBlobStorage.ps1
│   │   ├── Export-ReservationsPriceToBlobStorage.ps1
│   │   ├── Export-ReservationsUsageToBlobStorage.ps1
│   │   ├── Export-SavingsPlansUsageToBlobStorage.ps1
│   │   └── Ingest-OptimizationCSVExportsToLogAnalytics.ps1
│   ├── maintenance/
│   │   └── CleanUp-OlderRecommendationsFromSqlServer.ps1
│   ├── recommendations/
│   │   ├── Ingest-RecommendationsToLogAnalytics.ps1
│   │   ├── Ingest-RecommendationsToSQLServer.ps1
│   │   ├── Ingest-SuppressionsToLogAnalytics.ps1
│   │   ├── Recommend-AADExpiringCredentialsToBlobStorage.ps1
│   │   ├── Recommend-ARMOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-AdvisorAsIsToBlobStorage.ps1
│   │   ├── Recommend-AdvisorCostAugmentedToBlobStorage.ps1
│   │   ├── Recommend-AppServiceOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-DiskOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-SqlDbOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-StorageAccountOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-UnattachedDisksToBlobStorage.ps1
│   │   ├── Recommend-UnusedAppGWsToBlobStorage.ps1
│   │   ├── Recommend-UnusedLoadBalancersToBlobStorage.ps1
│   │   ├── Recommend-VMOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-VMSSOptimizationsToBlobStorage.ps1
│   │   ├── Recommend-VMsHighAvailabilityToBlobStorage.ps1
│   │   └── Recommend-VNetOptimizationsToBlobStorage.ps1
│   └── remediations/
│       ├── Remediate-AdvisorRightSizeFiltered.ps1
│       ├── Remediate-LongDeallocatedVMsFiltered.ps1
│       └── Remediate-UnattachedDisksFiltered.ps1
├── upgrade-manifest.json
└── views/
    ├── AzureOptimizationEngine.pbix
    ├── powerbi-query.m
    └── workbooks/
        ├── benefits-simulation.bicep
        ├── benefits-simulation.json
        ├── benefits-usage.bicep
        ├── benefits-usage.json
        ├── blockblobstorage-usage.bicep
        ├── blockblobstorage-usage.json
        ├── costs-growing.bicep
        ├── costs-growing.json
        ├── identities-roles.bicep
        ├── identities-roles.json
        ├── policy-compliance.bicep
        ├── policy-compliance.json
        ├── recommendations.bicep
        ├── recommendations.json
        ├── reservations-potential.bicep
        ├── reservations-potential.json
        ├── reservations-usage.bicep
        ├── reservations-usage.json
        ├── resources-inventory.bicep
        ├── resources-inventory.json
        ├── savingsplans-usage.bicep
        └── savingsplans-usage.json
Condensed preview — 107 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,066K chars).
[
  {
    "path": ".github/workflows/continuous-deployment-dev-new.yml",
    "chars": 2260,
    "preview": "name: AOE Continuous Deployment (DEV NEW)\non: \n  workflow_dispatch:\n  push:\n    branches:\n      - dev\npermissions:\n     "
  },
  {
    "path": ".github/workflows/continuous-deployment-dev.yml",
    "chars": 2485,
    "preview": "name: AOE Continuous Deployment (DEV)\non: \n  workflow_dispatch:\n  push:\n    branches:\n      - dev\npermissions:\n      id-"
  },
  {
    "path": ".github/workflows/continuous-deployment.yml",
    "chars": 2132,
    "preview": "name: AOE Continuous Deployment (PROD)\non: \n  workflow_dispatch:\n  push:\n    branches:\n      - master\npermissions:\n     "
  },
  {
    "path": ".gitignore",
    "chars": 228,
    "preview": "# Deployment state file\nlast-deployment-state.json\n# Database connection settings (for the Suppress-Recommendation.ps1 h"
  },
  {
    "path": "Deploy-AzureOptimizationEngine.ps1",
    "chars": 73567,
    "preview": "param (\n    [Parameter(Mandatory = $false)]\n    [string] $TemplateUri,\n\n    [Parameter(Mandatory = $false)]\n    [string]"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2020 Hélder Pinto\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 1704,
    "preview": "# Welcome to the Azure Optimization Engine - now a FinOps Toolkit tool! 🔍\n\n👋 Thank you for your interest in the Azure Op"
  },
  {
    "path": "Reset-AutomationSchedules.ps1",
    "chars": 9491,
    "preview": "param(\n    [Parameter(Mandatory = $false)] \n    [String] $AzureEnvironment = \"AzureCloud\",\n\n    [Parameter(Mandatory = $"
  },
  {
    "path": "Setup-BenefitsUsageDependencies.ps1",
    "chars": 10239,
    "preview": "param(\n    [Parameter(Mandatory = $false)] \n    [String] $AzureEnvironment = \"AzureCloud\",\n\n    [Parameter(Mandatory = $"
  },
  {
    "path": "Setup-DataCollectionRules.ps1",
    "chars": 9301,
    "preview": "param(\n    [Parameter(Mandatory = $false)] \n    [String] $AzureEnvironment = \"AzureCloud\",\n\n    [Parameter(Mandatory = $"
  },
  {
    "path": "Setup-LogAnalyticsWorkspaces.ps1",
    "chars": 6768,
    "preview": "param(\n    [Parameter(Mandatory = $false)] \n    [String] $AzureEnvironment = \"AzureCloud\",\n\n    [Parameter(Mandatory = $"
  },
  {
    "path": "Suppress-Recommendation.ps1",
    "chars": 8955,
    "preview": "param(\n    [Parameter(Mandatory = $true)] \n    [String] $RecommendationId\n)\n\n$ErrorActionPreference = \"Stop\"\n\nfunction T"
  },
  {
    "path": "azuredeploy-nested.bicep",
    "chars": 79576,
    "preview": "param projectLocation string\nparam templateLocation string\n\nparam storageAccountName string\nparam automationAccountName "
  },
  {
    "path": "azuredeploy.bicep",
    "chars": 2600,
    "preview": "targetScope = 'subscription'\nparam rgName string\nparam readerRoleAssignmentGuid string = guid(subscription().subscriptio"
  },
  {
    "path": "custom-recommendations-types.json",
    "chars": 10015,
    "preview": "{\n    \"recommendations\": [\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"c320b790-2e58-452a-aa63-7b62"
  },
  {
    "path": "docs/configuring-workspaces.md",
    "chars": 6777,
    "preview": "# Configuring Log Analytics workspaces\n\n## Validating/configuring performance counters collection\n\nIf you want to fully "
  },
  {
    "path": "docs/customizing-aoe.md",
    "chars": 10857,
    "preview": "# Customizing the Azure Optimization Engine\n\nThere are many customization options available in AOE, in the form of Azure"
  },
  {
    "path": "docs/suppressing-recommendations.md",
    "chars": 2604,
    "preview": "# Suppressing recommendations\n\nWhen working on the recommendations provided by AOE, you may find some cases where the re"
  },
  {
    "path": "model/filters-table.sql",
    "chars": 752,
    "preview": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[Filters"
  },
  {
    "path": "model/loganalyticsingestcontrol-initialize.sql",
    "chars": 7007,
    "preview": "IF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argvmexports')\nBEGIN\n    IN"
  },
  {
    "path": "model/loganalyticsingestcontrol-table.sql",
    "chars": 919,
    "preview": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[LogAnal"
  },
  {
    "path": "model/loganalyticsingestcontrol-upgrade.sql",
    "chars": 626,
    "preview": "UPDATE [dbo].[LogAnalyticsIngestControl] SET CollectedType = 'ARGVirtualMachine' WHERE StorageContainerName = 'argvmexpo"
  },
  {
    "path": "model/recommendations-sp.sql",
    "chars": 772,
    "preview": "IF OBJECT_ID ( N'[dbo].[GetRecommendations]', 'P' ) IS NOT NULL\nBEGIN\n    DROP PROCEDURE dbo.GetRecommendations\nEND\nEXEC"
  },
  {
    "path": "model/recommendations-table.sql",
    "chars": 3076,
    "preview": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[Recomme"
  },
  {
    "path": "model/sqlserveringestcontrol-initialize.sql",
    "chars": 266,
    "preview": "IF NOT EXISTS (SELECT * FROM [dbo].[SqlServerIngestControl] WHERE StorageContainerName = 'recommendationsexports')\nBEGIN"
  },
  {
    "path": "model/sqlserveringestcontrol-table.sql",
    "chars": 597,
    "preview": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[SqlServ"
  },
  {
    "path": "perfcounters.json",
    "chars": 2021,
    "preview": "[\n    {\n        \"objectName\": \"LogicalDisk\",\n        \"instance\": \"*\",\n        \"counterName\": \"Disk Read Bytes/sec\",\n    "
  },
  {
    "path": "queries/rbac-spns-keys-expiring.kql",
    "chars": 3154,
    "preview": "let expiryInterval = 30d;\nlet AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago"
  },
  {
    "path": "queries/rbac-spns-roles-aad-all.kql",
    "chars": 2522,
    "preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
  },
  {
    "path": "queries/rbac-spns-roles-arm-all.kql",
    "chars": 2522,
    "preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
  },
  {
    "path": "queries/rbac-spns-roles-arm-privileged.kql",
    "chars": 2848,
    "preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
  },
  {
    "path": "queries/rbac-users-roles-aad-all.kql",
    "chars": 1903,
    "preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
  },
  {
    "path": "queries/rbac-users-roles-arm-privileged.kql",
    "chars": 2119,
    "preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
  },
  {
    "path": "queries/rbac-users-roles-guests-privileged.kql",
    "chars": 2217,
    "preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
  },
  {
    "path": "queries/rbac-users-roles-guests.kql",
    "chars": 1891,
    "preview": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignments"
  },
  {
    "path": "runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1",
    "chars": 18818,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n"
  },
  {
    "path": "runbooks/data-collection/Export-ARGAppGatewayPropertiesToBlobStorage.ps1",
    "chars": 8863,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGAppServicePlanPropertiesToBlobStorage.ps1",
    "chars": 7490,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1",
    "chars": 6774,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGLoadBalancerPropertiesToBlobStorage.ps1",
    "chars": 8137,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGManagedDisksPropertiesToBlobStorage.ps1",
    "chars": 8031,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGNICPropertiesToBlobStorage.ps1",
    "chars": 8846,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGNSGPropertiesToBlobStorage.ps1",
    "chars": 8551,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGPublicIpPropertiesToBlobStorage.ps1",
    "chars": 9188,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGResourceContainersPropertiesToBlobStorage.ps1",
    "chars": 9314,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGSqlDatabasePropertiesToBlobStorage.ps1",
    "chars": 7177,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1",
    "chars": 8354,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGVMSSPropertiesToBlobStorage.ps1",
    "chars": 9687,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGVNetPropertiesToBlobStorage.ps1",
    "chars": 11762,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1",
    "chars": 12593,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-AdvisorRecommendationsToBlobStorage.ps1",
    "chars": 8944,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $targetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1",
    "chars": 11204,
    "preview": "Param (\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $true)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1",
    "chars": 35680,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-PolicyComplianceToBlobStorage.ps1",
    "chars": 24368,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [s"
  },
  {
    "path": "runbooks/data-collection/Export-PriceSheetToBlobStorage.ps1",
    "chars": 17191,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $BillingAccountID,\n\n    [Parameter(Mandatory = $false)]\n    [str"
  },
  {
    "path": "runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1",
    "chars": 10069,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n"
  },
  {
    "path": "runbooks/data-collection/Export-ReservationsPriceToBlobStorage.ps1",
    "chars": 5814,
    "preview": "param(\n    [Parameter(Mandatory = $false)] \n    [string] $Filter = \"serviceName eq 'Virtual Machines' and priceType eq '"
  },
  {
    "path": "runbooks/data-collection/Export-ReservationsUsageToBlobStorage.ps1",
    "chars": 12415,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetScope,\n\n    [Parameter(Mandatory = $false)]\n    [string] "
  },
  {
    "path": "runbooks/data-collection/Export-SavingsPlansUsageToBlobStorage.ps1",
    "chars": 10118,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetScope,\n\n    [Parameter(Mandatory = $false)]\n    [string] "
  },
  {
    "path": "runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1",
    "chars": 13414,
    "preview": "param(\n    [Parameter(Mandatory = $true)]\n    [string] $StorageSinkContainer\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudE"
  },
  {
    "path": "runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1",
    "chars": 1962,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlse"
  },
  {
    "path": "runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1",
    "chars": 13548,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -"
  },
  {
    "path": "runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1",
    "chars": 13080,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -"
  },
  {
    "path": "runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1",
    "chars": 8114,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -"
  },
  {
    "path": "runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1",
    "chars": 14072,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1",
    "chars": 19951,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1",
    "chars": 12451,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -"
  },
  {
    "path": "runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1",
    "chars": 43671,
    "preview": "$ErrorActionPreference = \"Stop\"\n\nfunction Find-SkuHourlyPrice {\n    param (\n        [object[]] $SKUPriceSheet,\n        ["
  },
  {
    "path": "runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1",
    "chars": 32547,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1",
    "chars": 25143,
    "preview": "$ErrorActionPreference = \"Stop\"\n\nfunction Find-DiskMonthlyPrice {\n    param (\n        [object[]] $SKUPriceSheet,\n       "
  },
  {
    "path": "runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1",
    "chars": 19669,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1",
    "chars": 14757,
    "preview": "function ConvertTo-Hashtable {\n    [CmdletBinding()]\n    [OutputType('hashtable')]\n    param (\n        [Parameter(ValueF"
  },
  {
    "path": "runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1",
    "chars": 11987,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1",
    "chars": 12155,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1",
    "chars": 17113,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1",
    "chars": 20132,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1",
    "chars": 38136,
    "preview": "$ErrorActionPreference = \"Stop\"\n\nfunction Find-SkuHourlyPrice {\n    param (\n        [object[]] $SKUPriceSheet,\n        ["
  },
  {
    "path": "runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1",
    "chars": 57129,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1",
    "chars": 55548,
    "preview": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-Automa"
  },
  {
    "path": "runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1",
    "chars": 8356,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [bool] $Simulate = $true\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnviro"
  },
  {
    "path": "runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1",
    "chars": 10943,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [bool] $Simulate = $true\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnviro"
  },
  {
    "path": "runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1",
    "chars": 9777,
    "preview": "param(\n    [Parameter(Mandatory = $false)]\n    [bool] $Simulate = $true\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnviro"
  },
  {
    "path": "upgrade-manifest.json",
    "chars": 39722,
    "preview": "{\n    \"modules\": [\n        {\n            \"name\": \"Az.Accounts\",\n            \"url\": \"https://www.powershellgallery.com/ap"
  },
  {
    "path": "views/powerbi-query.m",
    "chars": 8093,
    "preview": "let\n    Source = Sql.Database(\"aoedevgithub-sql.database.windows.net\", \"azureoptimization\", [Query=\"EXEC GetRecommendati"
  },
  {
    "path": "views/workbooks/benefits-simulation.bicep",
    "chars": 1158,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/benefits-simulation.json",
    "chars": 66232,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  },
  {
    "path": "views/workbooks/benefits-usage.bicep",
    "chars": 1148,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/benefits-usage.json",
    "chars": 40295,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  },
  {
    "path": "views/workbooks/blockblobstorage-usage.bicep",
    "chars": 1166,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/blockblobstorage-usage.json",
    "chars": 51655,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  },
  {
    "path": "views/workbooks/costs-growing.bicep",
    "chars": 1146,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/costs-growing.json",
    "chars": 27158,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"### Outliers/grow"
  },
  {
    "path": "views/workbooks/identities-roles.bicep",
    "chars": 1156,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/identities-roles.json",
    "chars": 52373,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  },
  {
    "path": "views/workbooks/policy-compliance.bicep",
    "chars": 1154,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/policy-compliance.json",
    "chars": 90787,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  },
  {
    "path": "views/workbooks/recommendations.bicep",
    "chars": 1150,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/recommendations.json",
    "chars": 177739,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  },
  {
    "path": "views/workbooks/reservations-potential.bicep",
    "chars": 1164,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/reservations-potential.json",
    "chars": 45096,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  },
  {
    "path": "views/workbooks/reservations-usage.bicep",
    "chars": 1156,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/reservations-usage.json",
    "chars": 92177,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  },
  {
    "path": "views/workbooks/resources-inventory.bicep",
    "chars": 1158,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/resources-inventory.json",
    "chars": 187909,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  },
  {
    "path": "views/workbooks/savingsplans-usage.bicep",
    "chars": 1157,
    "preview": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique wi"
  },
  {
    "path": "views/workbooks/savingsplans-usage.json",
    "chars": 43684,
    "preview": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterIt"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the helderpinto/AzureOptimizationEngine GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 107 files (1.9 MB), approximately 472.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!