diff --git a/.all-contributorsrc b/.all-contributorsrc index d3f24579c..176d0d3b6 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -159,6 +159,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/10661605?v=4", "profile": "https://aka.ms/helderpinto", "contributions": [ + "code", + "review", "doc", "bug" ] diff --git a/.github/workflows/aoe-cd-dev.yml b/.github/workflows/aoe-cd-dev.yml index fc2f22966..328217476 100644 --- a/.github/workflows/aoe-cd-dev.yml +++ b/.github/workflows/aoe-cd-dev.yml @@ -5,6 +5,7 @@ on: branches: - dev paths: + - 'docs/deploy/optimization-engine/**' - 'src/optimization-engine/**' permissions: id-token: write @@ -15,8 +16,6 @@ jobs: 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: @@ -48,8 +47,6 @@ jobs: "NamePrefix": "'"$AOE_NAMEPREFIX"'", "WorkspaceReuse": "n", "DeployWorkbooks": "y", - "SqlAdmin": "'"$AOE_SQL_ADMIN"'", - "SqlPass": "'"$AOE_SQL_PASSWD"'", "TargetLocation": "'"$AOE_LOCATION"'", "DeployBenefitsUsageDependencies": "n" }' > ./src/optimization-engine/deploymentSettings.json @@ -57,5 +54,5 @@ jobs: shell: pwsh run: | Set-Location ./src/optimization-engine - ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/src/optimization-engine/azuredeploy.bicep" + ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/src/optimization-engine/azuredeploy.bicep" -SqlAdminPrincipalType "Group" -SqlAdminPrincipalName ${{ secrets.AOE_SQL_ADMIN_PRINCIPAL_NAME }} -SqlAdminPrincipalObjectId ${{ secrets.AOE_SQL_ADMIN_PRINCIPAL_ID }} - run: echo "๐Ÿ This job's status is ${{ job.status }}." diff --git a/.github/workflows/aoe-cd-prod.yml b/.github/workflows/aoe-cd-prod.yml index 1abe8baf0..09181cd7c 100644 --- a/.github/workflows/aoe-cd-prod.yml +++ b/.github/workflows/aoe-cd-prod.yml @@ -5,6 +5,7 @@ on: branches: - main paths: + - 'docs/deploy/optimization-engine/**' - 'src/optimization-engine/**' permissions: id-token: write @@ -15,8 +16,6 @@ jobs: 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: @@ -48,8 +47,6 @@ jobs: "NamePrefix": "'"$AOE_NAMEPREFIX"'", "WorkspaceReuse": "n", "DeployWorkbooks": "y", - "SqlAdmin": "'"$AOE_SQL_ADMIN"'", - "SqlPass": "'"$AOE_SQL_PASSWD"'", "TargetLocation": "'"$AOE_LOCATION"'", "DeployBenefitsUsageDependencies": "n" }' > ./src/optimization-engine/deploymentSettings.json @@ -57,5 +54,5 @@ jobs: shell: pwsh run: | Set-Location ./src/optimization-engine - ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/src/optimization-engine/azuredeploy.bicep" + ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/src/optimization-engine/azuredeploy.bicep" -SqlAdminPrincipalType "Group" -SqlAdminPrincipalName ${{ secrets.AOE_SQL_ADMIN_PRINCIPAL_NAME }} -SqlAdminPrincipalObjectId ${{ secrets.AOE_SQL_ADMIN_PRINCIPAL_ID }} - run: echo "๐Ÿ This job's status is ${{ job.status }}." diff --git a/.github/workflows/aoe-cd-test.yml b/.github/workflows/aoe-cd-test.yml index b9404d4f4..5ed45bfa1 100644 --- a/.github/workflows/aoe-cd-test.yml +++ b/.github/workflows/aoe-cd-test.yml @@ -5,6 +5,7 @@ on: branches: - features/aoe paths: + - 'docs/deploy/optimization-engine/**' - 'src/optimization-engine/**' permissions: id-token: write @@ -15,8 +16,6 @@ jobs: 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: @@ -48,8 +47,6 @@ jobs: "NamePrefix": "'"$AOE_NAMEPREFIX"'", "WorkspaceReuse": "n", "DeployWorkbooks": "y", - "SqlAdmin": "'"$AOE_SQL_ADMIN"'", - "SqlPass": "'"$AOE_SQL_PASSWD"'", "TargetLocation": "'"$AOE_LOCATION"'", "DeployBenefitsUsageDependencies": "n" }' > ./src/optimization-engine/deploymentSettings.json @@ -57,5 +54,5 @@ jobs: shell: pwsh run: | Set-Location ./src/optimization-engine - ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/src/optimization-engine/azuredeploy.bicep" + ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/src/optimization-engine/azuredeploy.bicep" -SqlAdminPrincipalType "Group" -SqlAdminPrincipalName ${{ secrets.AOE_SQL_ADMIN_PRINCIPAL_NAME }} -SqlAdminPrincipalObjectId ${{ secrets.AOE_SQL_ADMIN_PRINCIPAL_ID }} - run: echo "๐Ÿ This job's status is ${{ job.status }}." diff --git a/README.md b/README.md index e4210ace5..d9f150a1a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ There are many ways to participate. From reporting bugs and requesting features Arjen Huitema
Arjen Huitema

๐Ÿ’ป Bill Anderson
Bill Anderson

๐Ÿ“– - Hรฉlder Pinto
Hรฉlder Pinto

๐Ÿ“– ๐Ÿ› + Hรฉlder Pinto
Hรฉlder Pinto

๐Ÿ’ป ๐Ÿ‘€ ๐Ÿ“– ๐Ÿ› Yuan Zhang
Yuan Zhang

๐Ÿ’ป ymehdimsft
ymehdimsft

๐Ÿ’ป srilatha inavolu
srilatha inavolu

๐Ÿ’ป ๐Ÿ‘€ diff --git a/docs/README.md b/docs/README.md index 5f0944425..59bb3f026 100644 --- a/docs/README.md +++ b/docs/README.md @@ -126,7 +126,7 @@ All the main changes are tracked in the changelog. For additional details, refer Arjen Huitema
Arjen Huitema

๐Ÿ’ป Bill Anderson
Bill Anderson

๐Ÿ“– - Hรฉlder Pinto
Hรฉlder Pinto

๐Ÿ“– ๐Ÿ› + Hรฉlder Pinto
Hรฉlder Pinto

๐Ÿ’ป ๐Ÿ‘€ ๐Ÿ“– ๐Ÿ› Yuan Zhang
Yuan Zhang

๐Ÿ’ป ymehdimsft
ymehdimsft

๐Ÿ’ป srilatha inavolu
srilatha inavolu

๐Ÿ’ป ๐Ÿ‘€ diff --git a/docs/_optimize/optimization-engine/README.md b/docs/_optimize/optimization-engine/README.md index 07d2961a3..b03a8834f 100644 --- a/docs/_optimize/optimization-engine/README.md +++ b/docs/_optimize/optimization-engine/README.md @@ -1,6 +1,6 @@ --- layout: default -title: Optimization Engine +title: Optimization engine has_children: true nav_order: 40 description: 'The Azure Optimization Engine (AOE) is an extensible solution designed to generate optimization recommendations for your Azure environment.' @@ -105,7 +105,7 @@ Once deployed and after all the initial ingestion and recommendations generation * A supported Azure subscription (see the [FAQ](./faq.md)) * A user account with Owner permissions over the chosen subscription, so that the Automation Managed Identity is granted the required privileges over the subscription (Reader) and deployment resource group (Contributor) -* Azure Powershell 6.6.0+ +* Azure Powershell 9.0.0+ * (Optional, for Identity and RBAC governance) Microsoft.Graph.Authentication and Microsoft.Graph.Identity.DirectoryManagement PowerShell modules (version 2.4.0+) * (Optional, for Identity and RBAC governance) A user account with at least Privileged Role Administrator permissions over the Microsoft Entra tenant, so that the Managed Identity is granted the required privileges over Microsoft Entra ID (Global Reader) * (Optional, for Azure commitments insights) A user account with administrative privileges over the Enterprise Agreement (Enterprise Enrollment Administrator) or the Microsoft Customer Agreement (Billing Profile Owner), so that the Managed Identity is granted the required privileges over your consumption agreement diff --git a/docs/_optimize/optimization-engine/configuring-workspaces.md b/docs/_optimize/optimization-engine/configuring-workspaces.md index f74df62ac..2ed6e6469 100644 --- a/docs/_optimize/optimization-engine/configuring-workspaces.md +++ b/docs/_optimize/optimization-engine/configuring-workspaces.md @@ -1,6 +1,6 @@ --- layout: default -parent: Optimization Engine +parent: Optimization engine title: Configuring workspaces nav_order: 30 description: 'Include the VM performance logs available in your Log Analytics workspaces to get deeper insights and more accurate results.' diff --git a/docs/_optimize/optimization-engine/customize.md b/docs/_optimize/optimization-engine/customize.md index 273885ac8..a6a0367dc 100644 --- a/docs/_optimize/optimization-engine/customize.md +++ b/docs/_optimize/optimization-engine/customize.md @@ -1,6 +1,6 @@ --- layout: default -parent: Optimization Engine +parent: Optimization engine title: Customizations nav_order: 20 description: 'Customize the Azure Optimization Engine settings according to your organization requirements.' @@ -29,6 +29,17 @@ By default, the Azure Automation Managed Identity is assigned the Reader role on In the context of augmented VM right-size recommendations, you may have your VMs reporting to multiple workspaces. If you need to include other workspaces - besides the main one AOE is using - in the recommendations scope, you just have to add their workspace IDs to the `AzureOptimization_RightSizeAdditionalPerfWorkspaces` variable (see more details in [Configuring workspaces](./configuring-workspaces.md)). +If you are a multi-tenant customer, you can extend the reach of AOE to a tenant other than the one where it was deployed. To achieve this, you must ensure the following pre-requisites: + +* Create a service principal (App registration) and a secret in the secondary tenant. +* Grant the required permissions to the service principal in the secondary tenant, namely **Reader** in Azure subscriptions/management groups and **Global Reader** in Entra ID. +* Create an [Automation credential](https://learn.microsoft.com/azure/automation/shared-resources/credentials?tabs=azure-powershell#create-a-new-credential-asset) in the AOE's Automation Account, with the service principal's client ID as username and the secret as password. +* Execute the `Register-MultitenantAutomationSchedules.ps1` script (available in the [AOE root folder](https://aka.ms/AzureOptimizationEngine/code)) in the context of the subscription where AOE was deployed. This script will create new job schedules for each of the export runbooks and configure them to query the secondary tenant. You just have to call the script following the syntax below: + +```powershell +./Register-MultitenantAutomationSchedules.ps1 -AutomationAccountName -ResourceGroupName -TargetSchedulesSuffix -TargetTenantId -TargetTenantCredentialName [-TargetSchedulesOffsetMinutes ] [-TargetAzureEnvironment ] [-ExcludedRunbooks ] [-IncludedRunbooks ] +``` +
## โฐ Adjust schedules @@ -113,4 +124,4 @@ Variable | Description `AzureOptimization_RecommendationsMaxAgeInDays` | The maximum age (in days) for a recommendation to be kept in the SQL database. Default: 365. `AzureOptimization_RetailPricesCurrencyCode` | The currency code (e.g., EUR, USD, etc.) used to collect the Reservations retail prices. `AzureOptimization_PriceSheetMeterCategories` | The comma-separated meter categories used for Pricesheet filtering, in order to avoid ingesting unnecessary data. Defaults to "Virtual Machines,Storage" -`AzureOptimization_ConsumptionScope` | The scope of the consumption exports: `Subscription` (default) or `BillingAccount`. See [more details](./setup-options.md#-enabling-azure-commitments-workbooks). +`AzureOptimization_ConsumptionScope` | The scope of the consumption exports: `Subscription` (default), `BillingProfile` (MCA only) or `BillingAccount` (for MCA, requires adding the Billing Account Reader role to the AOE managed identity). See [more details](./setup-options.md#-enabling-azure-commitments-workbooks). diff --git a/docs/_optimize/optimization-engine/faq.md b/docs/_optimize/optimization-engine/faq.md index 4e4961add..c92ef4287 100644 --- a/docs/_optimize/optimization-engine/faq.md +++ b/docs/_optimize/optimization-engine/faq.md @@ -1,6 +1,6 @@ --- layout: default -parent: Optimization Engine +parent: Optimization engine title: FAQ nav_order: 60 description: 'All the frequently asked questions about AOE in one place.' @@ -17,6 +17,15 @@ All the frequently asked questions about AOE in one place. * **What type of Azure subscriptions/clouds are supported?** AOE has been deployed and tested against EA, MCA and MSDN subscriptions in the Azure commercial cloud (AzureCloud). Although not tested yet, it should also work in MOSA subscriptions. It was designed to also operate in the US Government cloud, though it was never tested there. Sponsorship (MS-AZR-0036P and MS-AZR-0143P), CSP (MS-AZR-0145P, MS-AZR-0146P, and MS-AZR-159P) DreamSpark (MS-AZR-0144P) and Internal subscriptions should also work, but due to lack of availability or disparities in their consumption (billing) exports models, some of the Workbooks may not fully work. +* **Why are my Recommendations workbook and Power BI report still empty after deploying AOE?** AOE takes up to 3 hours after deployment to export and ingest the data required to generate recommendations into Log Analytics / SQL Database. If after this time you aren't still seeing any recommendations, check whether: + * Azure Advisor has been reporting recommendations for the subscriptions in the AOE scope; + * Azure Automation runbooks have been failing, especially critical ones such as `Ingest-` and `Recommend-`, and verify the Exception message that is logged, which will normally give you a hint for the failure cause; + * a daily cap has been set in the AOE Log Analytics Workspace that might be dropping the ingestion of AOE logs after the cap was reached. + +* **Why some workbooks present this message: `Failed to resolve table or column expression named 'AzureOptimizationPricesheetV1_CL'`?** This is typically a symptom of not having granted the required permissions to the AOE Automation Account managed identity. See instructions [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup). + +* **Why is the Identity and Roles workbook empty and presenting error messages?** This is typically a symptom of not having granted the required permissions, at the Entra ID tenant level, to the AOE Automation Account managed identity. After having granted the `Global Reader` role to the AOE managed identity, the workbook should populate on the next day. + * **Why is my Power BI report empty?** Most of the Power BI report pages are configured to filter out recommendations older than 7 days. If it shows empty, just try to refresh the report data. * **Why is my VM right-size recommendations overview page empty?** The AOE depends on Azure Advisor Cost recommendations for VM right-sizing. If no VMs are showing up, try increasing the CPU threshold in the Azure Advisor configuration... or maybe your infrastructure is not oversized after all! @@ -27,7 +36,7 @@ All the frequently asked questions about AOE in one place. * **What is the currency used for costs and savings?** The currency used is the one that is reported by default by the Azure Consumption APIs. It should match the one you usually see in Azure Cost Management. -* **What is the default time span for collecting Azure consumption data?** By default, the Azure consumption exports daily runbook collects 1-day data from 7 days ago. This offset works well for many types of subscriptions. If you're running AOE in PAYG or EA subscriptions, you can decrease the offset by adjusting the `AzureOptimization_ConsumptionOffsetDays` variable. However, using a value less than 2 days is not recommended. +* **What is the default time span for collecting Azure consumption data?** By default, the Azure consumption exports daily runbook collects 1-day data from 3 days ago. This offset works well for many types of subscriptions. If you're running AOE in PAYG or EA subscriptions, you can decrease the offset by adjusting the `AzureOptimization_ConsumptionOffsetDays` variable. However, using a value less than 2 days is not recommended. * **Why is AOE recommending to delete a long-deallocated VM that was deallocated just a few days before?** The _LongDeallocatedVms_ recommendation depends on accurate Azure consumption exports. If you just deployed AOE, it hasn't collected consumption long enough to provide accurate recommendations. Let AOE run at least for 30 days to get accurate recommendations. diff --git a/docs/_optimize/optimization-engine/reports.md b/docs/_optimize/optimization-engine/reports.md index 9d3fd6ed6..89b29500d 100644 --- a/docs/_optimize/optimization-engine/reports.md +++ b/docs/_optimize/optimization-engine/reports.md @@ -1,6 +1,6 @@ --- layout: default -parent: Optimization Engine +parent: Optimization engine title: Reports nav_order: 10 description: 'Visualize the Azure Optimization Engine rich recommendations and insights.' diff --git a/docs/_optimize/optimization-engine/setup-options.md b/docs/_optimize/optimization-engine/setup-options.md index f9697c331..4d78fffb6 100644 --- a/docs/_optimize/optimization-engine/setup-options.md +++ b/docs/_optimize/optimization-engine/setup-options.md @@ -1,6 +1,6 @@ --- layout: default -parent: Optimization Engine +parent: Optimization engine title: Setup options nav_order: 50 description: 'Advanced scenarios for setting up or upgrading AOE.' @@ -57,18 +57,26 @@ An example of the content of such silent deployment file is: "WorkspaceName": "<>", // mandatory if WorkspaceReuse is set to 'n' "WorkspaceResourceGroupName": "<>", // mandatory if workspaceReuse is set to 'n' "DeployWorkbooks": "y", // y = deploy the workbooks, n = don't deploy the workbooks - "SqlAdmin": "<>", - "SqlPass": "<>", "TargetLocation": "westeurope", "DeployBenefitsUsageDependencies": "y", // deploy the dependencies for the Azure commitments workbooks (EA/MCA customers only + agreement administrator role required) "CustomerType": "MCA", // mandatory if DeployBenefitsUsageDependencies is set to 'y', MCA/EA "BillingAccountId": ":_YYYY-MM-DD", // mandatory if DeployBenefitsUsageDependencies is set to 'y', MCA or EA Billing Account ID "BillingProfileId": "ABCD-DEF-GHI-JKL", // mandatory if CustomerType is set to 'MCA" "CurrencyCode": "EUR" // mandatory if DeployBenefitsUsageDependencies is set to 'y' - } - + } ``` +When silently deploying AOE, which typically happens in automated continuous deployment workflows, you might want to leverage SQL Entra ID authentication +parameters, for example to grant the SQL administrator role to an Entra ID group having the workflow automation service principal as member. For example: + +```powershell +.\Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath "" -SqlAdminPrincipalType Group -SqlAdminPrincipalName "" -SqlAdminPrincipalObjectId "" +``` + +
+ When deploying AOE with non-user identities (service principals), you must ensure you assign a system identity to the AOE SQL Server and grant it the `Directory Readers` role in Entra ID. Please follow the steps described [here](https://aka.ms/sqlaadsetup). +
+ ## ๐Ÿค Enabling Azure commitments workbooks In order to leverage the Workbooks that allow you to analyze your Azure commitments usage (`Benefits Usage`, `Reservations Usage` and `Savings Plans Usage`) or estimate the impact of doing additional consumption commitments (`Benefits Simulation` and `Reservations Potential`), you need to configure AOE and grant privileges to its Managed Identity at your consumption agreement level (EA or MCA). If you could not do it during setup/upgrade, you can still execute those extra configuration steps, provided you do it with a user that is **both Contributor in the AOE resource group and have administrative privileges over the consumption agreement** (Enterprise Enrollment Administrator for EA or Billing Profile Owner for MCA). You just have to use the `Setup-BenefitsUsageDependencies.ps1` script following the syntax below and answer the input requests: @@ -79,7 +87,7 @@ In order to leverage the Workbooks that allow you to analyze your Azure commitme If you run into issues with the Azure Pricesheet ingestion (due to the large size of the CVS export), you can create the following Azure Automation variable, to filter in the Price Sheet regions: `AzureOptimization_PriceSheetMeterRegions` set to the comma-separated billing regions of your virtual machines (e.g. *EU West,EU North*). -The Reservations Usage Workbook has a couple of "Unused Reservations" tiles that require AOE to export Consumption data at the EA/MCA scope (instead of the default Subscription scope). You can switch to EA/MCA scope consumption by creating/updating the `AzureOptimization_ConsumptionScope` Automation variable with `BillingAccount` as value. Be aware that this option may generate a very large single consumption export which may lead to errors due to lack of memory (this would in turn require [deploying AOE with a Hybrid Worker](./customize.md#-scale-aoe-runbooks-with-hybrid-worker)). +The Reservations Usage Workbook has a couple of "Unused Reservations" tiles that require AOE to export Consumption data at the EA/MCA scope (instead of the default Subscription scope). You can switch to EA/MCA scope consumption by creating/updating the `AzureOptimization_ConsumptionScope` Automation variable with `BillingAccount` (EA/MCA, requiring additional Billing Account Reader role manually granted to the AOE managed identity) or `BillingProfile` (MCA only) as value. Be aware that this option may generate a very large single consumption export which may lead to errors due to lack of memory (this would in turn require [deploying AOE with a Hybrid Worker](./customize.md#-scale-aoe-runbooks-with-hybrid-worker)). ## ๐Ÿ”ผ Upgrading AOE diff --git a/docs/_optimize/optimization-engine/suppressing-recommendations.md b/docs/_optimize/optimization-engine/suppressing-recommendations.md index c96d84af0..8d4f182ec 100644 --- a/docs/_optimize/optimization-engine/suppressing-recommendations.md +++ b/docs/_optimize/optimization-engine/suppressing-recommendations.md @@ -1,6 +1,6 @@ --- layout: default -parent: Optimization Engine +parent: Optimization engine title: Suppressing recommendations nav_order: 40 description: 'Adjust the recommendations results to your environment characteristics.' diff --git a/src/optimization-engine/Deploy-AzureOptimizationEngine.ps1 b/src/optimization-engine/Deploy-AzureOptimizationEngine.ps1 index e9ca6d015..daf1436c6 100644 --- a/src/optimization-engine/Deploy-AzureOptimizationEngine.ps1 +++ b/src/optimization-engine/Deploy-AzureOptimizationEngine.ps1 @@ -25,6 +25,15 @@ The path to the silent deployment settings file. If provided, the script will us .PARAMETER ResourceTags A hashtable of resource tags to apply to the deployed resources. Default is an empty hashtable. +.PARAMETER SqlAdminPrincipalType +The type of the SQL Admin principal. Default is User. Can also be Group or ServicePrincipal. + +.PARAMETER SqlAdminPrincipalName +The name of the SQL Admin principal. Required only if the principal type is Group or ServicePrincipal. + +.PARAMETER SqlAdminPrincipalObjectId +The object ID of the SQL Admin principal. Required only if the principal type is Group or ServicePrincipal. + .PARAMETER EnableDefaultTelemetry A boolean to indicate if the default telemetry should be enabled. Default is true. @@ -65,6 +74,15 @@ param ( [Parameter(Mandatory = $false)] [hashtable] $ResourceTags = @{}, + [Parameter(Mandatory = $false)] + [string] $SqlAdminPrincipalType = "User", + + [Parameter(Mandatory = $false)] + [string] $SqlAdminPrincipalName, + + [Parameter(Mandatory = $false)] + [string] $SqlAdminPrincipalObjectId, + [Parameter(Mandatory = $false)] [bool] $EnableDefaultTelemetry = $true ) @@ -101,29 +119,6 @@ function ConvertTo-Hashtable { } } -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 @@ -198,14 +193,6 @@ if(-not([string]::IsNullOrEmpty($SilentDeploymentSettingsPath)) -and (Test-Path { 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." @@ -276,8 +263,6 @@ if (-not((Test-Path -Path "./azuredeploy.bicep") -and (Test-Path -Path "./azured 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 @@ -291,6 +276,31 @@ else { } } +if (-not([string]::IsNullOrEmpty($SqlAdminPrincipalName)) -and -not([string]::IsNullOrEmpty($SqlAdminPrincipalObjectId))) +{ + $userPrincipalName = $SqlAdminPrincipalName + $userObjectId = $SqlAdminPrincipalObjectId +} +elseif ($SqlAdminPrincipalType -eq "User") +{ + $user = Get-AzADUser -SignedIn -Select UserType, UserPrincipalName, Id + if (-not([string]::IsNullOrEmpty($user.UserPrincipalName)) -and -not([string]::IsNullOrEmpty($user.Id))) + { + $userPrincipalName = $user.UserPrincipalName + $userObjectId = $user.Id + } + else + { + throw "Could not get the signed-in user details." + } +} +else +{ + throw "You must provide the SQL Admin principal name and object Id for non-User principal types." +} + +$cloudDetails = Get-AzEnvironment -Name $AzureEnvironment + #endregion #region Azure subscription choice @@ -515,6 +525,7 @@ else { } Write-Host "...for the Azure SQL Server..." -ForegroundColor Green +$sqlServerAlreadyExists = $false $sql = Get-AzSqlServer -ResourceGroupName $resourceGroupName -Name $sqlServerName -ErrorAction SilentlyContinue if ($null -eq $sql -and -not($sqlServerName -like "*.database.*") -and -not($IgnoreNamingAvailabilityErrors)) { @@ -528,7 +539,9 @@ if ($null -eq $sql -and -not($sqlServerName -like "*.database.*") -and -not($Ign Write-Host "$($sqlNameResult.message) ($sqlServerName)" -ForegroundColor Red } } -else { +else +{ + $sqlServerAlreadyExists = $true Write-Host "(The SQL Server was already deployed)" -ForegroundColor Green } @@ -591,33 +604,6 @@ 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 @@ -700,11 +686,6 @@ else } 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") @@ -748,9 +729,10 @@ if ("Y", "y" -contains $continueInput) { $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 ` + -storageAccountName $storageAccountName -automationAccountName $automationAccountName -sqlServerAlreadyExists $sqlServerAlreadyExists ` -sqlServerName $sqlServerName -sqlDatabaseName $sqlDatabaseName -cloudEnvironment $AzureEnvironment ` - -sqlAdminLogin $sqlAdmin -sqlAdminPassword $sqlPass -resourceTags $ResourceTags -enableDefaultTelemetry $EnableDefaultTelemetry -WarningAction SilentlyContinue + -userPrincipalName $userPrincipalName -userObjectId $userObjectId -sqlAdminPrincipalType $SqlAdminPrincipalType ` + -resourceTags $ResourceTags -enableDefaultTelemetry $EnableDefaultTelemetry -WarningAction SilentlyContinue $deploymentSucceeded = $true } catch { @@ -1122,6 +1104,16 @@ if ("Y", "y" -contains $continueInput) { } #endregion + # Ensuring SQL Server allows only Entra ID authentication + if (-not((Get-AzSqlServerActiveDirectoryOnlyAuthentication -ServerName $sqlServerName -ResourceGroupName $resourceGroupName).AzureADOnlyAuthentication)) + { + Write-Host "Setting $userPrincipalName as Microsoft Entra admin in SQL Database..." -ForegroundColor Green + Set-AzSqlServerActiveDirectoryAdministrator -ObjectId $userObjectId ` + -ServerName $sqlServerName -ResourceGroupName $resourceGroupName -DisplayName $userPrincipalName | Out-Null + Write-Host "Enabling Entra ID-only authentication in SQL Database..." -ForegroundColor Green + Enable-AzSqlServerActiveDirectoryOnlyAuthentication -ServerName $sqlServerName -ResourceGroupName $resourceGroupName | Out-Null + } + #region Open SQL Server firewall rule if (-not($sqlServerName -like "*.database.*")) { @@ -1144,7 +1136,6 @@ if ("Y", "y" -contains $continueInput) { #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)" @@ -1160,9 +1151,12 @@ if ("Y", "y" -contains $continueInput) { 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;") + + $azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $createTableQuery = Get-Content -Path "./model/loganalyticsingestcontrol-table.sql" @@ -1173,7 +1167,8 @@ if ("Y", "y" -contains $continueInput) { $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 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $initTableQuery = Get-Content -Path "./model/loganalyticsingestcontrol-initialize.sql" @@ -1184,7 +1179,8 @@ if ("Y", "y" -contains $continueInput) { $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 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $upgradeTableQuery = Get-Content -Path "./model/loganalyticsingestcontrol-upgrade.sql" @@ -1195,7 +1191,8 @@ if ("Y", "y" -contains $continueInput) { $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 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $createTableQuery = Get-Content -Path "./model/sqlserveringestcontrol-table.sql" @@ -1206,7 +1203,8 @@ if ("Y", "y" -contains $continueInput) { $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 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $initTableQuery = Get-Content -Path "./model/sqlserveringestcontrol-initialize.sql" @@ -1217,7 +1215,8 @@ if ("Y", "y" -contains $continueInput) { $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 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $createTableQuery = Get-Content -Path "./model/recommendations-table.sql" @@ -1228,7 +1227,8 @@ if ("Y", "y" -contains $continueInput) { $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 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $createTableQuery = Get-Content -Path "./model/recommendations-sp.sql" @@ -1239,7 +1239,8 @@ if ("Y", "y" -contains $continueInput) { $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 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $createTableQuery = Get-Content -Path "./model/filters-table.sql" @@ -1250,6 +1251,18 @@ if ("Y", "y" -contains $continueInput) { $Cmd.ExecuteReader() $Conn.Close() + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token + $Conn.Open() + + $createUserQuery = (Get-Content -Path "./model/automation-user.sql").Replace("", $automationAccountName) + $Cmd = new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = $createUserQuery + $Cmd.ExecuteReader() + $Conn.Close() + $connectionSuccess = $true } catch { diff --git a/src/optimization-engine/Register-MultitenantAutomationSchedules.ps1 b/src/optimization-engine/Register-MultitenantAutomationSchedules.ps1 new file mode 100644 index 000000000..59ca6f713 --- /dev/null +++ b/src/optimization-engine/Register-MultitenantAutomationSchedules.ps1 @@ -0,0 +1,169 @@ +<# +.SYNOPSIS +This script adds job schedules for all Azure Optimization Engine data collection runbooks for a given tenant/cloud. + +.DESCRIPTION +This script adds job schedules for all Azure Optimization Engine data collection runbooks for a given tenant/cloud. +It is useful when you want to collect data from multiple tenants/clouds in a single AOE deployment. +The script will create new schedules with a suffix and offset for each existing schedule, and associate them to the corresponding runbooks. +It requires you to create first a service principal in the target tenant and grant it the necessary permissions to the target subscriptions/tenant. +You also need to create an Automation Credential in the AOE Automation Account with the service principal credentials (client ID and secret). + +.PARAMETER AzureEnvironment +The Azure environment to use. Possible values are AzureCloud, AzureChinaCloud, AzureUSGovernment. + +.PARAMETER AutomationAccountName +The name of the Automation Account where the Azure Optimization Engine is deployed. + +.PARAMETER ResourceGroupName +The name of the Resource Group where the Automation Account is located. + +.PARAMETER TargetSchedulesSuffix +The suffix to append to the new schedules names. + +.PARAMETER TargetSchedulesOffsetMinutes +The offset in minutes to apply to the new schedules start times (defaults to 0). + +.PARAMETER TargetAzureEnvironment +The target Azure environment to collect data from. Possible values are AzureCloud, AzureChinaCloud, AzureUSGovernment. Defaults to AzureCloud. + +.PARAMETER TargetTenantId +The target tenant ID to collect data from. + +.PARAMETER TargetTenantCredentialName +The name of the Automation Credential in the AOE Automation Account containing the service principal credentials (client ID and secret). + +.PARAMETER ExcludedRunbooks +An array of runbook names to exclude from the process. Defaults to runbooks that export data at the EA/MCA level, which should only run on the source tenant. + +.PARAMETER IncludedRunbooks +An array of runbook names to include in the process. If none are provided, all data collection runbooks will be processed. + +.EXAMPLE +Register-MultitenantAutomationSchedules.ps1 -AutomationAccountName "AOE" -ResourceGroupName "AOE-RG" -TargetSchedulesSuffix "-Tenant1" -TargetTenantId "00000000-0000-0000-0000-000000000000" -TargetTenantCredentialName "Tenant1" -ExcludedRunbooks @("Export-ReservationsPriceToBlobStorage") + +.LINK +https://aka.ms/AzureOptimizationEngine/customize +#> +param( + [Parameter(Mandatory = $false)] + [String] $AzureEnvironment = "AzureCloud", + + [Parameter(Mandatory = $true)] + [String] $AutomationAccountName, + + [Parameter(Mandatory = $true)] + [String] $ResourceGroupName, + + [Parameter(Mandatory = $true)] + [String] $TargetSchedulesSuffix, + + [Parameter(Mandatory = $false)] + [int] $TargetSchedulesOffsetMinutes = 0, + + [Parameter(Mandatory = $false)] + [String] $TargetAzureEnvironment = "AzureCloud", + + [Parameter(Mandatory = $true)] + [String] $TargetTenantId, + + [Parameter(Mandatory = $true)] + [String] $TargetTenantCredentialName, + + [Parameter(Mandatory = $false)] + [String[]] $ExcludedRunbooks = @("Export-ReservationsPriceToBlobStorage","Export-PriceSheetToBlobStorage","Export-ReservationsUsageToBlobStorage","Export-SavingsPlansUsageToBlobStorage"), + + [Parameter(Mandatory = $false)] + [String[]] $IncludedRunbooks = @() +) + +$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." +} + +$dataCollectionRunbooks = $scheduledRunbooks | Where-Object { $_.RunbookName -like "Export-*" -and $_.RunbookName -notin $ExcludedRunbooks -and $_.RunbookName -ne "Export-ReservationsPriceToBlobStorage" } +if ($IncludedRunbooks.Count -gt 0) +{ + $dataCollectionRunbooks = $dataCollectionRunbooks | Where-Object { $_.RunbookName -in $IncludedRunbooks } +} + +if (-not($dataCollectionRunbooks)) +{ + throw "The $AutomationAccountName Automation Account does not contain any scheduled data collection runbook. It might not be associated to the Azure Optimization Engine." +} + +foreach ($jobSchedule in $dataCollectionRunbooks) +{ + Write-Host "Processing $($jobSchedule.RunbookName) runbook for $($jobSchedule.ScheduleName) schedule..." -ForegroundColor Green + $schedule = Get-AzAutomationSchedule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -ScheduleName $jobSchedule.ScheduleName + $newScheduleName = "$($schedule.Name)$TargetSchedulesSuffix" + $newSchedule = Get-AzAutomationSchedule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName ` + -ScheduleName $newScheduleName -ErrorAction SilentlyContinue + if (-not($newSchedule)) + { + Write-Host "Creating new schedule $newScheduleName..." -ForegroundColor Green + $newScheduleParameters = @{ + ResourceGroupName = $ResourceGroupName; + AutomationAccountName = $AutomationAccountName; + Name = $newScheduleName; + StartTime = $schedule.NextRun.AddMinutes($TargetSchedulesOffsetMinutes); + ExpiryTime = $schedule.ExpiryTime; + Timezone = $schedule.TimeZone + } + switch ($schedule.Frequency) + { + "Hour" { + $newScheduleParameters['HourInterval'] = $schedule.Interval + } + "Day" { + $newScheduleParameters['DayInterval'] = $schedule.Interval + } + "Week" { + $newScheduleParameters['WeekInterval'] = $schedule.Interval + $newScheduleParameters['DaysOfWeek'] = $schedule.WeeklyScheduleOptions.DaysOfWeek + } + default { + throw "Unsupported frequency: $($schedule.Frequency)" + } + } + New-AzAutomationSchedule @newScheduleParameters | Out-Null + } + + Write-Host "Associating schedule $newScheduleName to $($jobSchedule.RunbookName) runbook..." -ForegroundColor Green + $jobScheduleDetails = Get-AzAutomationScheduledRunbook -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName ` + -JobScheduleId $jobSchedule.JobScheduleId + $jobScheduleDetails.Parameters.Add("externalCloudEnvironment", $TargetAzureEnvironment) + $jobScheduleDetails.Parameters.Add("externalTenantId", $TargetTenantId) + $jobScheduleDetails.Parameters.Add("externalCredentialName", $TargetTenantCredentialName) + $newJobScheduleParameters = @{ + ResourceGroupName = $ResourceGroupName; + AutomationAccountName = $AutomationAccountName; + RunbookName = $jobScheduleDetails.RunbookName; + ScheduleName = $newScheduleName; + RunOn = $jobScheduleDetails.HybridWorker; + Parameters = $jobScheduleDetails.Parameters + } + Register-AzAutomationScheduledRunbook @newJobScheduleParameters | Out-Null +} + +Write-Host "DONE" -ForegroundColor Green \ No newline at end of file diff --git a/src/optimization-engine/Suppress-Recommendation.ps1 b/src/optimization-engine/Suppress-Recommendation.ps1 index 889bef62f..e4f8aaefd 100644 --- a/src/optimization-engine/Suppress-Recommendation.ps1 +++ b/src/optimization-engine/Suppress-Recommendation.ps1 @@ -9,6 +9,9 @@ to suppress the recommendation. .PARAMETER RecommendationId The recommendation Id to suppress. +.PARAMETER AzureEnvironment +The Azure environment to connect to. Default is AzureCloud. + .EXAMPLE .\Suppress-Recommendation.ps1 -RecommendationId "00000000-0000-0000-0000-000000000001" @@ -17,7 +20,10 @@ https://aka.ms/AzureOptimizationEngine/suppressrecs #> param( [Parameter(Mandatory = $true)] - [String] $RecommendationId + [String] $RecommendationId, + + [Parameter(Mandatory = $false)] + [string] $AzureEnvironment = "AzureCloud" ) $ErrorActionPreference = "Stop" @@ -44,6 +50,22 @@ if (-not(Test-IsGuid -ObjectGuid $RecommendationId)) Exit } +$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 + } +} + +$cloudDetails = Get-AzEnvironment -Name $AzureEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + $databaseConnectionSettingsPath = ".\database-connection-settings.json" $dbConnectionSettings = @{} @@ -81,20 +103,6 @@ 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" @@ -106,7 +114,9 @@ $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;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$databaseServer,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn @@ -251,13 +261,15 @@ if ("Y", "y" -contains $continueInput) $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;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn2 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$databaseServer,1433;Database=$databaseName;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn2.AccessToken = $dbToken.Token $Conn2.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn2 $Cmd.CommandText = $sqlStatement - $Cmd.CommandTimeout=120 + $Cmd.CommandTimeout = $SqlTimeout try { $Cmd.ExecuteReader() diff --git a/src/optimization-engine/azuredeploy-nested.bicep b/src/optimization-engine/azuredeploy-nested.bicep index cfed41bd4..0cd2a7d96 100644 --- a/src/optimization-engine/azuredeploy-nested.bicep +++ b/src/optimization-engine/azuredeploy-nested.bicep @@ -4,16 +4,17 @@ param templateLocation string param storageAccountName string param automationAccountName string param sqlServerName string +param sqlServerAlreadyExists bool param sqlDatabaseName string param logAnalyticsReuse bool param logAnalyticsWorkspaceName string param logAnalyticsWorkspaceRG string param logAnalyticsRetentionDays int param sqlBackupRetentionDays int -param sqlAdminLogin string +param userObjectId string +param userPrincipalName string +param sqlAdminPrincipalType string -@secure() -param sqlAdminPassword string param cloudEnvironment string param authenticationOption string param baseTime string @@ -734,7 +735,7 @@ var csvParameterizedExports = [ exportJobId: monitorDiskIOPSAvgExportJobId parameters: { ResourceType: 'microsoft.compute/disks' - ARGFilter: 'sku.name =~ \'Premium_LRS\' and properties.diskState != \'Unattached\'' + ARGFilter: 'sku.name startswith \'Premium_\' and properties.diskState =~ \'Attached\'' TimeSpan: '01:00:00' aggregationType: 'Average' AggregationOfType: 'Maximum' @@ -748,7 +749,7 @@ var csvParameterizedExports = [ exportJobId: monitorDiskMBPsAvgExportJobId parameters: { ResourceType: 'microsoft.compute/disks' - ARGFilter: 'sku.name =~ \'Premium_LRS\' and properties.diskState != \'Unattached\'' + ARGFilter: 'sku.name startswith \'Premium_\' and properties.diskState =~ \'Attached\'' TimeSpan: '01:00:00' aggregationType: 'Average' AggregationOfType: 'Maximum' @@ -948,14 +949,14 @@ var runbooks = [ } { name: consumptionExportsRunbookName - version: '2.0.4.1' + version: '2.1.0.0' 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' + version: '1.3.0.0' description: 'Exports Azure AAD Objects to Blob Storage using Azure ARM API' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/data-collection/${aadObjectsExportsRunbookName}.ps1') @@ -983,7 +984,7 @@ var runbooks = [ } { name: rbacExportsRunbookName - version: '1.0.4.1' + version: '1.1.0.0' description: 'Exports RBAC assignments to Blob Storage using ARM and Microsoft Entra' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/data-collection/${rbacExportsRunbookName}.ps1') @@ -1074,161 +1075,161 @@ var runbooks = [ } { name: csvIngestRunbookName - version: '1.5.0.0' + version: '1.6.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' + version: '2.5.0.0' description: 'Generates unattached disks recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${unattachedDisksRecommendationsRunbookName}.ps1') } { name: advisorCostAugmentedRecommendationsRunbookName - version: '2.9.1.0' + version: '2.10.0.0' description: 'Generates augmented Advisor Cost recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorCostAugmentedRecommendationsRunbookName}.ps1') } { name: advisorAsIsRecommendationsRunbookName - version: '1.5.5.0' + version: '1.6.0.0' description: 'Generates all types of Advisor recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorAsIsRecommendationsRunbookName}.ps1') } { name: vmsHARecommendationsRunbookName - version: '1.0.3.0' + version: '1.1.0.0' description: 'Generates VMs High Availability recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmsHARecommendationsRunbookName}.ps1') } { name: vmOptimizationsRecommendationsRunbookName - version: '1.0.0.0' + version: '1.1.0.0' description: 'Generates VM optimizations recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmOptimizationsRecommendationsRunbookName}.ps1') } { name: aadExpiringCredsRecommendationsRunbookName - version: '1.1.10.0' + version: '1.2.0.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' + version: '1.3.0.0' description: 'Generates unused Load Balancers recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedLBsRecommendationsRunbookName}.ps1') } { name: unusedAppGWsRecommendationsRunbookName - version: '1.2.9.0' + version: '1.3.0.0' description: 'Generates unused Application Gateways recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedAppGWsRecommendationsRunbookName}.ps1') } { name: armOptimizationsRecommendationsRunbookName - version: '1.0.3.0' + version: '1.1.0.0' description: 'Generates ARM optimizations recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${armOptimizationsRecommendationsRunbookName}.ps1') } { name: vnetOptimizationsRecommendationsRunbookName - version: '1.0.4.0' + version: '1.1.0.0' description: 'Generates Virtual Network optimizations recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${vnetOptimizationsRecommendationsRunbookName}.ps1') } { name: vmssOptimizationsRecommendationsRunbookName - version: '1.1.1.0' + version: '1.2.0.0' description: 'Generates VM Scale Set optimizations recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmssOptimizationsRecommendationsRunbookName}.ps1') } { name: sqldbOptimizationsRecommendationsRunbookName - version: '1.1.2.0' + version: '1.2.0.0' description: 'Generates SQL DB optimizations recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${sqldbOptimizationsRecommendationsRunbookName}.ps1') } { name: storageOptimizationsRecommendationsRunbookName - version: '1.0.3.0' + version: '1.1.0.0' description: 'Generates Storage Account optimizations recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${storageOptimizationsRecommendationsRunbookName}.ps1') } { name: appServiceOptimizationsRecommendationsRunbookName - version: '1.0.3.0' + version: '1.1.0.0' description: 'Generates App Service optimizations recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${appServiceOptimizationsRecommendationsRunbookName}.ps1') } { name: diskOptimizationsRecommendationsRunbookName - version: '1.1.1.0' + version: '1.2.0.0' description: 'Generates Disk optimizations recommendations' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${diskOptimizationsRecommendationsRunbookName}.ps1') } { name: recommendationsIngestRunbookName - version: '1.6.5.0' + version: '1.7.0.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' + version: '1.1.0.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' + version: '1.1.0.0' description: 'Ingests suppressions into Log Analytics' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/recommendations/${suppressionsLogAnalyticsIngestRunbookName}.ps1') } { name: advisorRightSizeFilteredRemediationRunbookName - version: '1.2.4.0' + version: '1.3.0.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' + version: '1.1.0.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' + version: '1.1.0.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' + version: '1.1.0.0' description: 'Cleans up older recommendations from SQL Database' type: 'PowerShell' scriptUri: uri(templateLocation, 'runbooks/maintenance/${cleanUpOlderRecommendationsRunbookName}.ps1') @@ -1701,17 +1702,47 @@ resource storageLifecycleManagementPolicy 'Microsoft.Storage/storageAccounts/man ] } -resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { +resource existingSqlServer 'Microsoft.Sql/servers@2022-05-01-preview' existing = if (sqlServerAlreadyExists) { + name: sqlServerName +} + +resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = if (!sqlServerAlreadyExists) { name: sqlServerName location: projectLocation tags: resourceTags properties: { - administratorLogin: sqlAdminLogin - administratorLoginPassword: sqlAdminPassword version: '12.0' publicNetworkAccess: 'Enabled' minimalTlsVersion: '1.2' + administrators: { + administratorType: 'ActiveDirectory' + azureADOnlyAuthentication: true + login: userPrincipalName + principalType: sqlAdminPrincipalType + sid: userObjectId + tenantId: tenant().tenantId + } + } +} + +resource sqlAdminsResource 'Microsoft.Sql/servers/administrators@2022-05-01-preview' = if (sqlServerAlreadyExists) { + parent: existingSqlServer + name: 'ActiveDirectory' + properties: { + administratorType: 'ActiveDirectory' + login: userPrincipalName + sid: userObjectId + tenantId: tenant().tenantId + } +} + +resource sqlAzureAdOnly 'Microsoft.Sql/servers/azureADOnlyAuthentications@2022-05-01-preview' = if (sqlServerAlreadyExists) { + name: 'Default' + parent: existingSqlServer + properties: { + azureADOnlyAuthentication: true } + dependsOn:[sqlAdminsResource] } resource sqlServerFirewall 'Microsoft.Sql/servers/firewallRules@2022-05-01-preview' = { @@ -1872,16 +1903,6 @@ resource automationVariables_LogAnalyticsWorkspaceKey 'Microsoft.Automation/auto } } -resource automatinCredentials_SQLServer 'Microsoft.Automation/automationAccounts/credentials@2020-01-13-preview' = { - parent: automationAccount - name: 'AzureOptimization_SQLServerCredential' - properties: { - description: 'Azure Optimization SQL Database Credentials' - password: sqlAdminPassword - userName: sqlAdminLogin - } -} - resource automationSchedules_csvExports 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = [for item in csvExportsSchedules: { parent: automationAccount name: item.exportSchedule diff --git a/src/optimization-engine/azuredeploy.bicep b/src/optimization-engine/azuredeploy.bicep index ec8982de5..0a09fce3b 100644 --- a/src/optimization-engine/azuredeploy.bicep +++ b/src/optimization-engine/azuredeploy.bicep @@ -10,16 +10,16 @@ param templateLocation string param storageAccountName string param automationAccountName string param sqlServerName string +param sqlServerAlreadyExists bool = false param sqlDatabaseName string = 'azureoptimization' param logAnalyticsReuse bool param logAnalyticsWorkspaceName string param logAnalyticsWorkspaceRG string param logAnalyticsRetentionDays int = 120 param sqlBackupRetentionDays int = 7 -param sqlAdminLogin string - -@secure() -param sqlAdminPassword string +param userPrincipalName string +param userObjectId string +param sqlAdminPrincipalType string = 'User' param cloudEnvironment string = 'AzureCloud' param authenticationOption string = 'ManagedIdentity' @@ -48,19 +48,21 @@ module resourcesDeployment './azuredeploy-nested.bicep' = { storageAccountName: storageAccountName automationAccountName: automationAccountName sqlServerName: sqlServerName + sqlServerAlreadyExists: sqlServerAlreadyExists sqlDatabaseName: sqlDatabaseName logAnalyticsReuse: logAnalyticsReuse logAnalyticsWorkspaceName: logAnalyticsWorkspaceName logAnalyticsWorkspaceRG: logAnalyticsWorkspaceRG logAnalyticsRetentionDays: logAnalyticsRetentionDays sqlBackupRetentionDays: sqlBackupRetentionDays - sqlAdminLogin: sqlAdminLogin - sqlAdminPassword: sqlAdminPassword cloudEnvironment: cloudEnvironment authenticationOption: authenticationOption baseTime: baseTime contributorRoleAssignmentGuid: contributorRoleAssignmentGuid resourceTags: resourceTags + userPrincipalName: userPrincipalName + userObjectId: userObjectId + sqlAdminPrincipalType: sqlAdminPrincipalType enableDefaultTelemetry: enableDefaultTelemetry } dependsOn: [ diff --git a/src/optimization-engine/model/automation-user.sql b/src/optimization-engine/model/automation-user.sql new file mode 100644 index 000000000..775b1ba66 --- /dev/null +++ b/src/optimization-engine/model/automation-user.sql @@ -0,0 +1,14 @@ +IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = '') +BEGIN + CREATE USER [] FROM EXTERNAL PROVIDER +END + +IF IS_ROLEMEMBER ('db_datareader','') = 0 +BEGIN + ALTER ROLE [db_datareader] ADD MEMBER [] +END + +IF IS_ROLEMEMBER ('db_datawriter','') = 0 +BEGIN + ALTER ROLE [db_datawriter] ADD MEMBER [] +END \ No newline at end of file diff --git a/src/optimization-engine/runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1 b/src/optimization-engine/runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1 index 8bde11357..bbe9bc594 100644 --- a/src/optimization-engine/runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1 @@ -213,8 +213,18 @@ if (-not([string]::IsNullOrEmpty($externalCredentialName))) } else { - "Logging in to Microsoft Graph..." - Connect-MgGraph -Identity -Environment $graphEnvironment -NoWelcome + "Logging in to Microsoft Graph with $authenticationOption..." + + switch ($authenticationOption) { + "UserAssignedManagedIdentity" { + Connect-MgGraph -Identity -ClientId $uamiClientID -Environment $graphEnvironment -NoWelcome + break + } + Default { #ManagedIdentity + Connect-MgGraph -Identity -Environment $graphEnvironment -NoWelcome + break + } + } } $datetime = (get-date).ToUniversalTime() diff --git a/src/optimization-engine/runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1 b/src/optimization-engine/runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1 index e31dd034f..c673eaf9b 100644 --- a/src/optimization-engine/runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1 @@ -124,7 +124,7 @@ if (-not([string]::IsNullOrEmpty($TargetSubscription))) { } else { $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} - $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId + $subscriptionSuffix = "all-" + $tenantId } [TimeSpan]::Parse($TimeGrain) | Out-Null diff --git a/src/optimization-engine/runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1 b/src/optimization-engine/runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1 index eac393878..1d359ac44 100644 --- a/src/optimization-engine/runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1 @@ -335,9 +335,17 @@ else } else { - if ($consumptionScope -ne "Subscription") + if ($consumptionScope -eq "BillingProfile") { - throw "Invalid value for AzureOptimization_ConsumptionScope. Valid values are 'Subscription' or 'BillingAccount'." + $BillingAccountID = Get-AutomationVariable -Name "AzureOptimization_BillingAccountID" + $BillingProfileID = Get-AutomationVariable -Name "AzureOptimization_BillingProfileID" + } + else + { + if ($consumptionScope -ne "Subscription") + { + throw "Invalid value for AzureOptimization_ConsumptionScope. Valid values are 'Subscription' or 'BillingAccount'." + } } } } @@ -400,7 +408,7 @@ if ($consumptionScope -eq "Subscription") } else { - "Exporting consumption data from $targetStartDate to $targetEndDate for Billing Account ID $BillingAccountID..." + "Exporting consumption data from $targetStartDate to $targetEndDate for $consumptionScope..." } @@ -667,59 +675,128 @@ else { $tags = $null } + + if ([string]::IsNullOrEmpty($consumptionLine.properties.billingProfileId)) + { + # legacy consumption schema - $billingEntry = New-Object PSObject -Property @{ - Timestamp = $timestamp - AccountName = $consumptionLine.properties.accountName - AccountOwnerId = $consumptionLine.properties.accountOwnerId - AdditionalInfo = $consumptionLine.properties.additionalInfo - benefitId = $consumptionLine.properties.benefitId - benefitName = $consumptionLine.properties.benefitName - BillingAccountId = $consumptionLine.properties.billingAccountId - BillingAccountName = $consumptionLine.properties.billingAccountName - BillingCurrencyCode = $consumptionLine.properties.billingCurrency - BillingPeriodEndDate= $consumptionLine.properties.billingPeriodEndDate - BillingPeriodStartDate= $consumptionLine.properties.billingPeriodStartDate - BillingProfileId = $consumptionLine.properties.billingProfileId - BillingProfileName= $consumptionLine.properties.billingProfileName - ChargeType = $consumptionLine.properties.chargeType - ConsumedService = $consumptionLine.properties.consumedService - CostAllocationRuleName = $consumptionLine.properties.costAllocationRuleName - CostCenter = $consumptionLine.properties.costCenter - CostInBillingCurrency = $consumptionLine.properties.cost - Date = (Get-Date $consumptionLine.properties.date).ToString("MM/dd/yyyy") - EffectivePrice = $consumptionLine.properties.effectivePrice - Frequency = $consumptionLine.properties.frequency - InvoiceSectionName = $consumptionLine.properties.invoiceSection - IsAzureCreditEligible = $consumptionLine.properties.isAzureCreditEligible - MeterCategory = $consumptionLine.properties.meterDetails.meterCategory - MeterId = $consumptionLine.properties.meterId - MeterName = $consumptionLine.properties.meterDetails.meterName - MeterRegion = $consumptionLine.properties.meterDetails.meterRegion - MeterSubCategory = $consumptionLine.properties.meterDetails.meterSubCategory - OfferId = $consumptionLine.properties.offerId - PartNumber = $consumptionLine.properties.partNumber - PayGPrice = $consumptionLine.properties.PayGPrice - PlanName = $consumptionLine.properties.planName - PricingModel = $consumptionLine.properties.pricingModel - ProductName = $consumptionLine.properties.product - PublisherName = $consumptionLine.properties.publisherName - PublisherType = $consumptionLine.properties.publisherType - Quantity = $consumptionLine.properties.quantity - ReservationId = $consumptionLine.properties.reservationId - ReservationName = $consumptionLine.properties.reservationName - ResourceGroup = $consumptionLine.properties.resourceGroup - ResourceId = $consumptionLine.properties.resourceId - ResourceLocation = $consumptionLine.properties.resourceLocation - ResourceName = $consumptionLine.properties.resourceName - ServiceFamily = $consumptionLine.properties.meterDetails.serviceFamily - SubscriptionId = $consumptionLine.properties.subscriptionId - SubscriptionName = $consumptionLine.properties.subscriptionName - Tags = $tags - Term = $consumptionLine.properties.term - UnitOfMeasure = $consumptionLine.properties.meterDetails.unitOfMeasure - UnitPrice = $consumptionLine.properties.unitPrice - } + $billingEntry = New-Object PSObject -Property @{ + Timestamp = $timestamp + AccountName = $consumptionLine.properties.accountName + AccountOwnerId = $consumptionLine.properties.accountOwnerId + AdditionalInfo = $consumptionLine.properties.additionalInfo + benefitId = $consumptionLine.properties.benefitId + benefitName = $consumptionLine.properties.benefitName + BillingAccountId = $consumptionLine.properties.billingAccountId + BillingAccountName = $consumptionLine.properties.billingAccountName + BillingCurrencyCode = $consumptionLine.properties.billingCurrency + BillingPeriodEndDate= $consumptionLine.properties.billingPeriodEndDate + BillingPeriodStartDate= $consumptionLine.properties.billingPeriodStartDate + BillingProfileId = $consumptionLine.properties.billingProfileId + BillingProfileName= $consumptionLine.properties.billingProfileName + ChargeType = $consumptionLine.properties.chargeType + ConsumedService = $consumptionLine.properties.consumedService + CostAllocationRuleName = $consumptionLine.properties.costAllocationRuleName + CostCenter = $consumptionLine.properties.costCenter + CostInBillingCurrency = $consumptionLine.properties.cost + Date = (Get-Date $consumptionLine.properties.date).ToString("MM/dd/yyyy") + EffectivePrice = $consumptionLine.properties.effectivePrice + Frequency = $consumptionLine.properties.frequency + InvoiceSectionName = $consumptionLine.properties.invoiceSection + IsAzureCreditEligible = $consumptionLine.properties.isAzureCreditEligible + MeterCategory = $consumptionLine.properties.meterDetails.meterCategory + MeterId = $consumptionLine.properties.meterId + MeterName = $consumptionLine.properties.meterDetails.meterName + MeterRegion = $consumptionLine.properties.meterDetails.meterRegion + MeterSubCategory = $consumptionLine.properties.meterDetails.meterSubCategory + OfferId = $consumptionLine.properties.offerId + PartNumber = $consumptionLine.properties.partNumber + PayGPrice = $consumptionLine.properties.PayGPrice + PlanName = $consumptionLine.properties.planName + PricingModel = $consumptionLine.properties.pricingModel + ProductName = $consumptionLine.properties.product + PublisherName = $consumptionLine.properties.publisherName + PublisherType = $consumptionLine.properties.publisherType + Quantity = $consumptionLine.properties.quantity + ReservationId = $consumptionLine.properties.reservationId + ReservationName = $consumptionLine.properties.reservationName + ResourceGroup = $consumptionLine.properties.resourceGroup + ResourceId = $consumptionLine.properties.resourceId + ResourceLocation = $consumptionLine.properties.resourceLocation + ResourceName = $consumptionLine.properties.resourceName + ServiceFamily = $consumptionLine.properties.meterDetails.serviceFamily + SubscriptionId = $consumptionLine.properties.subscriptionId + SubscriptionName = $consumptionLine.properties.subscriptionName + Tags = $tags + Term = $consumptionLine.properties.term + UnitOfMeasure = $consumptionLine.properties.meterDetails.unitOfMeasure + UnitPrice = $consumptionLine.properties.unitPrice + } + } + else + { + # MCA consumption schema + $billingEntry = New-Object PSObject -Property @{ + Timestamp = $timestamp + AdditionalInfo = $consumptionLine.properties.additionalInfo + benefitId = $consumptionLine.properties.benefitId + benefitName = $consumptionLine.properties.benefitName + BillingAccountId = $consumptionLine.properties.billingAccountId + BillingAccountName = $consumptionLine.properties.billingAccountName + BillingCurrencyCode = $consumptionLine.properties.billingCurrencyCode + BillingPeriodEndDate= $consumptionLine.properties.billingPeriodEndDate + BillingPeriodStartDate= $consumptionLine.properties.billingPeriodStartDate + BillingProfileId = $consumptionLine.properties.billingProfileId + BillingProfileName= $consumptionLine.properties.billingProfileName + ChargeType = $consumptionLine.properties.chargeType + ConsumedService = $consumptionLine.properties.consumedService + CostAllocationRuleName = $consumptionLine.properties.costAllocationRuleName + CostCenter = $consumptionLine.properties.costCenter + CostInBillingCurrency = $consumptionLine.properties.costInBillingCurrency + costInPricingCurrency = $consumptionLine.properties.costInPricingCurrency + costInUSD = $consumptionLine.properties.costInUSD + customerName = $consumptionLine.properties.customerName + Date = (Get-Date $consumptionLine.properties.date).ToString("MM/dd/yyyy") + EffectivePrice = $consumptionLine.properties.effectivePrice + exchangeRate = $consumptionLine.properties.exchangeRate + exchangeRateDate = $consumptionLine.properties.exchangeRateDate + exchangeRatePricingToBilling = $consumptionLine.properties.exchangeRatePricingToBilling + Frequency = $consumptionLine.properties.frequency + invoiceSectionId = $consumptionLine.properties.invoiceSectionId + InvoiceSectionName = $consumptionLine.properties.invoiceSectionName + IsAzureCreditEligible = $consumptionLine.properties.isAzureCreditEligible + MeterCategory = $consumptionLine.properties.meterCategory + MeterId = $consumptionLine.properties.meterId + MeterName = $consumptionLine.properties.meterName + MeterRegion = $consumptionLine.properties.meterRegion + MeterSubCategory = $consumptionLine.properties.meterSubCategory + PartNumber = $consumptionLine.properties.partNumber + paygCostInBillingCurrency = $consumptionLine.properties.paygCostInBillingCurrency + paygCostInUSD = $consumptionLine.properties.paygCostInUSD + PayGPrice = $consumptionLine.properties.payGPrice + PlanName = $consumptionLine.properties.planName + pricingCurrencyCode = $consumptionLine.properties.pricingCurrencyCode + PricingModel = $consumptionLine.properties.pricingModel + ProductName = $consumptionLine.properties.product + productIdentifier = $consumptionLine.properties.productIdentifier + PublisherName = $consumptionLine.properties.publisherName + PublisherType = $consumptionLine.properties.publisherType + Quantity = $consumptionLine.properties.quantity + ReservationId = $consumptionLine.properties.reservationId + ReservationName = $consumptionLine.properties.reservationName + ResourceGroup = $consumptionLine.properties.resourceGroup + ResourceId = $consumptionLine.properties.instanceName + ResourceLocation = $consumptionLine.properties.resourceLocation + resourceLocationNormalized = $consumptionLine.properties.resourceLocationNormalized + ServiceFamily = $consumptionLine.properties.serviceFamily + SubscriptionId = $consumptionLine.properties.subscriptionGuid + SubscriptionName = $consumptionLine.properties.subscriptionName + Tags = $tags + Term = $consumptionLine.properties.term + UnitOfMeasure = $consumptionLine.properties.unitOfMeasure + UnitPrice = $consumptionLine.properties.unitPrice + } + } $billingEntries += $billingEntry } } @@ -778,8 +855,16 @@ else } else { - "Starting cost details export process from $targetStartDate to $targetEndDate for Billing Account ID $BillingAccountID..." - Generate-CostDetails -ScopeId "/providers/Microsoft.Billing/billingAccounts/$BillingAccountID" -ScopeName $BillingAccountID + if ($consumptionScope -eq "BillingAccount") + { + "Starting cost details export process from $targetStartDate to $targetEndDate for Billing Account ID $BillingAccountID..." + Generate-CostDetails -ScopeId "/providers/Microsoft.Billing/billingAccounts/$BillingAccountID" -ScopeName $BillingAccountID.Replace(":","_") + } + if ($consumptionScope -eq "BillingProfile") + { + "Starting cost details export process from $targetStartDate to $targetEndDate for Billing Account ID $BillingAccountID / Billing Profile ID $BillingProfileID ..." + Generate-CostDetails -ScopeId "/providers/Microsoft.Billing/billingAccounts/$BillingAccountID/billingProfiles/$BillingProfileID" -ScopeName $BillingProfileID + } } } diff --git a/src/optimization-engine/runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1 b/src/optimization-engine/runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1 index f5000dec2..a9280d5a5 100644 --- a/src/optimization-engine/runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1 @@ -203,8 +203,18 @@ if (-not([string]::IsNullOrEmpty($externalCredentialName))) } else { - "Logging in to Microsoft Graph..." - Connect-MgGraph -Identity -Environment $graphEnvironment -NoWelcome + "Logging in to Microsoft Graph with $authenticationOption..." + + switch ($authenticationOption) { + "UserAssignedManagedIdentity" { + Connect-MgGraph -Identity -ClientId $uamiClientID -Environment $graphEnvironment -NoWelcome + break + } + Default { #ManagedIdentity + Connect-MgGraph -Identity -Environment $graphEnvironment -NoWelcome + break + } + } } $domainName = (Get-MgDomain | Where-Object { $_.IsVerified -and $_.IsDefault } | Select-Object -First 1).Id diff --git a/src/optimization-engine/runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1 b/src/optimization-engine/runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1 index e45dac978..bab858192 100644 --- a/src/optimization-engine/runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1 +++ b/src/optimization-engine/runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1 @@ -21,9 +21,6 @@ if ($authenticationOption -eq "UserAssignedManagedIdentity") } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -140,6 +137,9 @@ Function Post-OMSData($workspaceId, $sharedKey, $body, $logType, $TimeStampField } #endregion Functions +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + # get reference to storage sink Write-Output "Getting blobs list from $storageAccountSink storage account ($storageAccountSinkContainer container)..." Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId @@ -162,7 +162,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn @@ -272,12 +274,14 @@ foreach ($blob in $unprocessedBlobs) { $lastProcessedDateTime = $updatedLastProcessedDateTime Write-Output "Updating last processed time / line to $($updatedLastProcessedDateTime) / $updatedLastProcessedLine" $sqlStatement = "UPDATE [$LogAnalyticsIngestControlTable] SET LastProcessedLine = $updatedLastProcessedLine, LastProcessedDateTime = '$updatedLastProcessedDateTime' WHERE StorageContainerName = '$storageAccountSinkContainer'" - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn $Cmd.CommandText = $sqlStatement - $Cmd.CommandTimeout=120 + $Cmd.CommandTimeout = $SqlTimeout $Cmd.ExecuteReader() $Conn.Close() $Conn.Dispose() @@ -308,12 +312,14 @@ foreach ($blob in $unprocessedBlobs) { $updatedLastProcessedDateTime = $newProcessedTime Write-Output "Updating last processed time / line to $($updatedLastProcessedDateTime) / $updatedLastProcessedLine" $sqlStatement = "UPDATE [$LogAnalyticsIngestControlTable] SET LastProcessedLine = $updatedLastProcessedLine, LastProcessedDateTime = '$updatedLastProcessedDateTime' WHERE StorageContainerName = '$storageAccountSinkContainer'" - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn $Cmd.CommandText = $sqlStatement - $Cmd.CommandTimeout=120 + $Cmd.CommandTimeout = $SqlTimeout $Cmd.ExecuteReader() $Conn.Close() $Conn.Dispose() diff --git a/src/optimization-engine/runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1 b/src/optimization-engine/runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1 index 9220678b0..89b399917 100644 --- a/src/optimization-engine/runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1 +++ b/src/optimization-engine/runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1 @@ -1,9 +1,21 @@ $ErrorActionPreference = "Stop" +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$authenticationOption = Get-AutomationVariable -Name "AzureOptimization_AuthenticationOption" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity +if ([string]::IsNullOrEmpty($authenticationOption)) +{ + $authenticationOption = "ManagedIdentity" +} +if ($authenticationOption -eq "UserAssignedManagedIdentity") +{ + $uamiClientID = Get-AutomationVariable -Name "AzureOptimization_UAMIClientID" +} + $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -16,6 +28,23 @@ if (-not($RecommendationsMaxAge -gt 0)) } $recommendationsTable = "Recommendations" +$SqlTimeout = 120 + +"Logging in to Azure with $authenticationOption..." + +switch ($authenticationOption) { + "UserAssignedManagedIdentity" { + Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID + break + } + Default { #ManagedIdentity + Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment + break + } +} + +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) $tries = 0 $connectionSuccess = $false @@ -25,7 +54,9 @@ Write-Output "Cleaning up recommendations older than $RecommendationsMaxAge days do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1 b/src/optimization-engine/runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1 index 391ed24c9..075c57b85 100644 --- a/src/optimization-engine/runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1 @@ -16,9 +16,6 @@ if ($authenticationOption -eq "UserAssignedManagedIdentity") } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -140,6 +137,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + # get reference to storage sink Write-Output "Getting reference to $storageAccountSink storage account (recommendations exports sink)" Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId @@ -163,7 +163,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn @@ -292,12 +294,14 @@ foreach ($blob in $unprocessedBlobs) { $lastProcessedDateTime = $updatedLastProcessedDateTime Write-Output "Updating last processed time / line to $($updatedLastProcessedDateTime) / $updatedLastProcessedLine" $sqlStatement = "UPDATE [$LogAnalyticsIngestControlTable] SET LastProcessedLine = $updatedLastProcessedLine, LastProcessedDateTime = '$updatedLastProcessedDateTime' WHERE StorageContainerName = '$storageAccountSinkContainer'" - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn $Cmd.CommandText = $sqlStatement - $Cmd.CommandTimeout=120 + $Cmd.CommandTimeout = $SqlTimeout $Cmd.ExecuteReader() $Conn.Close() $Conn.Dispose() diff --git a/src/optimization-engine/runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1 b/src/optimization-engine/runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1 index f7041940f..9341b91a8 100644 --- a/src/optimization-engine/runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1 @@ -16,9 +16,6 @@ if ($authenticationOption -eq "UserAssignedManagedIdentity") } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -57,6 +54,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + # get reference to storage sink Write-Output "Getting reference to $storageAccountSink storage account (recommendations exports sink)" Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId @@ -84,7 +84,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn @@ -227,13 +229,15 @@ foreach ($blob in $unprocessedBlobs) { } } - $Conn2 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn2 = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn2.AccessToken = $dbToken.Token $Conn2.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn2 $Cmd.CommandText = $sqlStatement - $Cmd.CommandTimeout=120 + $Cmd.CommandTimeout = $SqlTimeout try { $Cmd.ExecuteReader() @@ -263,12 +267,14 @@ foreach ($blob in $unprocessedBlobs) { $lastProcessedDateTime = $updatedLastProcessedDateTime Write-Output "Updating last processed time / line to $($updatedLastProcessedDateTime) / $updatedLastProcessedLine" $sqlStatement = "UPDATE [$SqlServerIngestControlTable] SET LastProcessedLine = $updatedLastProcessedLine, LastProcessedDateTime = '$updatedLastProcessedDateTime' WHERE StorageContainerName = '$storageAccountSinkContainer'" - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn $Cmd.CommandText = $sqlStatement - $Cmd.CommandTimeout=$SqlTimeout + $Cmd.CommandTimeout = $SqlTimeout $Cmd.ExecuteReader() $Conn.Close() } diff --git a/src/optimization-engine/runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1 b/src/optimization-engine/runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1 index 2ebbe91fb..84725c4fc 100644 --- a/src/optimization-engine/runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1 @@ -20,15 +20,22 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { $sqldatabase = "azureoptimization" } +$authenticationOption = Get-AutomationVariable -Name "AzureOptimization_AuthenticationOption" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity +if ([string]::IsNullOrEmpty($authenticationOption)) +{ + $authenticationOption = "ManagedIdentity" +} +if ($authenticationOption -eq "UserAssignedManagedIdentity") +{ + $uamiClientID = Get-AutomationVariable -Name "AzureOptimization_UAMIClientID" +} + $SqlTimeout = 300 $FiltersTable = "Filters" @@ -105,6 +112,22 @@ Function Post-OMSData($workspaceId, $sharedKey, $body, $logType, $TimeStampField } #endregion Functions +"Logging in to Azure with $authenticationOption..." + +switch ($authenticationOption) { + "UserAssignedManagedIdentity" { + Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID + break + } + Default { #ManagedIdentity + Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment + break + } +} + +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Getting excluded recommendation sub-type IDs..." $tries = 0 @@ -112,7 +135,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1 index 00e81c592..1a7f04c4f 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1 @@ -35,9 +35,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -65,6 +62,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -72,7 +72,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1 index d959112c2..145ffed27 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1 @@ -38,9 +38,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -115,6 +112,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -122,7 +122,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1 index 2b1de2d5d..0c2387f09 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1 @@ -39,9 +39,6 @@ if (-not($daysBackwards -gt 0)) { } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -73,6 +70,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -80,7 +80,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn @@ -120,7 +122,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1 index fd3d91b12..5720fc66e 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1 @@ -184,9 +184,6 @@ if (-not($rightSizeRecommendationId)) { $additionalPerfWorkspaces = Get-AutomationVariable -Name "AzureOptimization_RightSizeAdditionalPerfWorkspaces" -ErrorAction SilentlyContinue $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -215,6 +212,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -222,7 +222,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn @@ -265,7 +267,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1 index 11f8dbf56..a095e390d 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1 @@ -38,9 +38,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -113,6 +110,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -120,7 +120,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1 index 1999e2e1d..8ed919e9f 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1 @@ -6,7 +6,7 @@ function Find-DiskMonthlyPrice { [string] $DiskSizeTier ) - $diskSkus = $SKUPriceSheet | Where-Object { $_.MeterName_s.Replace(" Disks","") -eq $DiskSizeTier } + $diskSkus = $SKUPriceSheet | Where-Object { $_.MeterName_s.Replace(" Disks","").Replace(" Disk","") -eq $DiskSizeTier } $targetMonthlyPrice = [double]::MaxValue if ($diskSkus) { @@ -56,9 +56,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -108,6 +105,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -115,7 +115,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn @@ -182,7 +184,7 @@ try $baseQuery = @" $pricesheetTableName | where TimeGenerated > ago(14d) - | where MeterCategory_s == 'Storage' and MeterSubCategory_s endswith "Managed Disks" and MeterName_s endswith "Disks" and MeterRegion_s == '$pricesheetRegion' and PriceType_s == 'Consumption' + | where MeterCategory_s == 'Storage' and MeterSubCategory_s contains "Managed Disk" and (MeterName_s endswith "Disk" or MeterName_s endswith "Disks") and MeterName_s !has 'Special' and MeterRegion_s == '$pricesheetRegion' and PriceType_s == 'Consumption' | distinct MeterName_s, MeterSubCategory_s, MeterCategory_s, MeterRegion_s, UnitPrice_s, UnitOfMeasure_s "@ @@ -222,7 +224,8 @@ $baseQuery = @" | summarize MaxIOPSMetric = max(todouble(MetricValue_s)) by ResourceId | join kind=inner ( $disksTableName - | where TimeGenerated > ago(1d) and DiskState_s != 'Unattached' and SKU_s startswith 'Premium' + | where TimeGenerated > ago(1d) and DiskState_s =~ 'Attached' and SKU_s startswith 'Premium' + | extend DiskTier_s = strcat(DiskTier_s, ' ', tostring(split(SKU_s, '_')[1])) | project ResourceId=InstanceId_s, DiskName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, Tags_s, MaxIOPSDisk=toint(DiskIOPS_s), DiskSizeGB_s, SKU_s, DiskTier_s, DiskType_s ) on ResourceId | project-away ResourceId1 @@ -234,7 +237,8 @@ $baseQuery = @" | summarize MaxMBsMetric = max(todouble(MetricValue_s)/1024/1024) by ResourceId | join kind=inner ( $disksTableName - | where TimeGenerated > ago(1d) and DiskState_s != 'Unattached' and SKU_s startswith 'Premium' + | where TimeGenerated > ago(1d) and DiskState_s =~ 'Attached' and SKU_s startswith 'Premium' + | extend DiskTier_s = strcat(DiskTier_s, ' ', tostring(split(SKU_s, '_')[1])) | project ResourceId=InstanceId_s, DiskName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, Tags_s, MaxMBsDisk=toint(DiskThroughput_s), DiskSizeGB_s, SKU_s, DiskTier_s, DiskType_s ) on ResourceId | project-away ResourceId1 @@ -280,7 +284,7 @@ foreach ($result in $results) $targetSku = $null $currentDiskTier = $null - if ([string]::IsNullOrEmpty($result.DiskTier_s)) # older disks do not have Tier info in their properties + if ([string]::IsNullOrEmpty($result.DiskTier_s) -or $result.DiskTier_s.Trim().Length -le 3) # older disks do not have Tier info in their properties { $currentSkuCandidates = @() foreach ($sku in $skus) @@ -294,13 +298,14 @@ foreach ($result in $results) if ($sku.Name -eq $result.SKU_s -and $skuMinSizeGB -lt [int]$result.DiskSizeGB_s -and $skuMaxSizeGB -ge [int]$result.DiskSizeGB_s ` -and [int]$skuMaxIOps -eq [int]$result.MaxIOPSDisk -and [int]$skuMaxBandwidthMBps -eq [int]$result.MaxMBsDisk) { - if ($null -eq $skuPricesFound[$sku.Size]) + $skuSize = $sku.Size + " " + $result.SKU_s.Split("_")[1] + if ($null -eq $skuPricesFound[$skuSize]) { - $skuPricesFound[$sku.Size] = Find-DiskMonthlyPrice -DiskSizeTier $sku.Size -SKUPriceSheet $pricesheetEntries + $skuPricesFound[$sku.Size] = Find-DiskMonthlyPrice -DiskSizeTier $skuSize -SKUPriceSheet $pricesheetEntries } $currentSkuCandidate = New-Object PSObject -Property @{ - Name = $sku.Size + Name = $skuSize MaxSizeGB = $skuMaxSizeGB } @@ -334,16 +339,17 @@ foreach ($result in $results) if ($sku.Name -eq $targetSkuPerfTier -and $skuMinSizeGB -lt [int]$result.DiskSizeGB_s -and $skuMaxSizeGB -ge [int]$result.DiskSizeGB_s ` -and [double]$skuMaxIOps -ge [double]$result.MaxIOPSMetric -and [double]$skuMaxBandwidthMBps -ge [double]$result.MaxMBsMetric) { + $skuSize = $sku.Size + " " + $targetSkuPerfTier.Split("_")[1] if ($null -eq $skuPricesFound[$sku.Size]) { - $skuPricesFound[$sku.Size] = Find-DiskMonthlyPrice -DiskSizeTier $sku.Size -SKUPriceSheet $pricesheetEntries + $skuPricesFound[$skuSize] = Find-DiskMonthlyPrice -DiskSizeTier $skuSize -SKUPriceSheet $pricesheetEntries } - if ($skuPricesFound[$sku.Size] -lt [double]::MaxValue -and $skuPricesFound[$sku.Size] -lt $skuPricesFound[$currentDiskTier]) + if ($skuPricesFound[$skuSize] -lt [double]::MaxValue -and $skuPricesFound[$skuSize] -lt $skuPricesFound[$currentDiskTier]) { $targetSkuCandidate = New-Object PSObject -Property @{ - Name = $sku.Size - MonthlyPrice = $skuPricesFound[$sku.Size] + Name = $skuSize + MonthlyPrice = $skuPricesFound[$skuSize] MaxSizeGB = $skuMaxSizeGB MaxIOPS = $skuMaxIOps MaxMBps = $skuMaxBandwidthMBps diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 index 484b289e2..a2c509645 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 @@ -38,9 +38,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -86,6 +83,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -93,7 +93,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1 index e3832d8c6..313239419 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1 @@ -70,9 +70,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -114,6 +111,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + $tenantId = (Get-AzContext).Tenant.Id Write-Output "Finding tables where recommendations will be generated from..." @@ -123,7 +123,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1 index 9dd38466b..e1e3a2140 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1 @@ -41,9 +41,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -71,6 +68,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -78,7 +78,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1 index 737cb9935..e1edc9dd9 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1 @@ -41,9 +41,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -71,6 +68,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -78,7 +78,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1 index 193104391..b4907ca6b 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1 @@ -41,9 +41,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -71,6 +68,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -78,7 +78,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1 index 3165bc592..ecf9f55b3 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1 @@ -41,9 +41,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -72,6 +69,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -79,7 +79,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1 index ddc65a5fe..ce6574af4 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1 @@ -116,9 +116,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -190,6 +187,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -197,7 +197,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1 index 8e7b36368..15f0e5349 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1 @@ -35,9 +35,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -62,6 +59,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -69,7 +69,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1 b/src/optimization-engine/runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1 index 5ccd80633..e506b372e 100644 --- a/src/optimization-engine/runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1 +++ b/src/optimization-engine/runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1 @@ -41,9 +41,6 @@ if ([string]::IsNullOrEmpty($lognamePrefix)) } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -108,6 +105,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + Write-Output "Finding tables where recommendations will be generated from..." $tries = 0 @@ -115,7 +115,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1 b/src/optimization-engine/runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1 index 5d58fcf54..9642fe141 100644 --- a/src/optimization-engine/runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1 +++ b/src/optimization-engine/runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1 @@ -21,9 +21,6 @@ if ($authenticationOption -eq "UserAssignedManagedIdentity") } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -75,6 +72,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + # get reference to storage sink Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context @@ -86,7 +86,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1 b/src/optimization-engine/runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1 index 3fc61eb5d..a81d892be 100644 --- a/src/optimization-engine/runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1 +++ b/src/optimization-engine/runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1 @@ -21,9 +21,6 @@ if ($authenticationOption -eq "UserAssignedManagedIdentity") } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -75,6 +72,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + # get reference to storage sink Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context @@ -86,7 +86,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1 b/src/optimization-engine/runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1 index 68ed719d6..dfd151741 100644 --- a/src/optimization-engine/runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1 +++ b/src/optimization-engine/runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1 @@ -21,9 +21,6 @@ if ($authenticationOption -eq "UserAssignedManagedIdentity") } $sqlserver = Get-AutomationVariable -Name "AzureOptimization_SQLServerHostname" -$sqlserverCredential = Get-AutomationPSCredential -Name "AzureOptimization_SQLServerCredential" -$SqlUsername = $sqlserverCredential.UserName -$SqlPass = $sqlserverCredential.GetNetworkCredential().Password $sqldatabase = Get-AutomationVariable -Name "AzureOptimization_SQLServerDatabase" -ErrorAction SilentlyContinue if ([string]::IsNullOrEmpty($sqldatabase)) { @@ -80,6 +77,9 @@ switch ($authenticationOption) { } } +$cloudDetails = Get-AzEnvironment -Name $CloudEnvironment +$azureSqlDomain = $cloudDetails.SqlDatabaseDnsSuffix.Substring(1) + # get reference to storage sink Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context @@ -91,7 +91,9 @@ $connectionSuccess = $false do { $tries++ try { - $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;") + $dbToken = Get-AzAccessToken -ResourceUrl "https://$azureSqlDomain/" + $Conn = New-Object System.Data.SqlClient.SqlConnection("Server=tcp:$sqlserver,1433;Database=$sqldatabase;Encrypt=True;Connection Timeout=$SqlTimeout;") + $Conn.AccessToken = $dbToken.Token $Conn.Open() $Cmd=new-object system.Data.SqlClient.SqlCommand $Cmd.Connection = $Conn diff --git a/src/optimization-engine/upgrade-manifest.json b/src/optimization-engine/upgrade-manifest.json index 492f94859..7311e0263 100644 --- a/src/optimization-engine/upgrade-manifest.json +++ b/src/optimization-engine/upgrade-manifest.json @@ -304,14 +304,14 @@ { "runbook": { "name": "runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1", - "version": "1.5.0.0" + "version": "1.6.0.0" }, "source": "dataCollection" }, { "runbook": { "name": "runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1", - "version": "1.6.5.0" + "version": "1.7.0.0" }, "source": "recommendations", "schedule": "AzureOptimization_IngestRecommendationsWeekly" @@ -319,7 +319,7 @@ { "runbook": { "name": "runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1", - "version": "1.0.2.0" + "version": "1.1.0.0" }, "source": "recommendations", "schedule": "AzureOptimization_IngestRecommendationsWeekly" @@ -327,7 +327,7 @@ { "runbook": { "name": "runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1", - "version": "1.0.0.0" + "version": "1.1.0.0" }, "source": "recommendations", "schedule": "AzureOptimization_IngestSuppressionsWeekly" @@ -335,7 +335,7 @@ { "runbook": { "name": "runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1", - "version": "1.0.0.0" + "version": "1.1.0.0" }, "source": "maintenance", "schedule": "AzureOptimization_CleanUpRecommendationsWeekly" @@ -345,7 +345,7 @@ { "runbook": { "name": "runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1", - "version": "1.2.2.1" + "version": "1.3.0.0" }, "container": "aadobjectsexports", "requiredVariables": [ @@ -521,7 +521,7 @@ { "runbook": { "name": "runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1", - "version": "2.0.4.1" + "version": "2.1.0.0" }, "container": "consumptionexports", "requiredVariables": [ @@ -532,7 +532,7 @@ { "runbook": { "name": "runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1", - "version": "1.0.4.1" + "version": "1.1.0.0" }, "container": "rbacexports", "requiredVariables": [ @@ -663,7 +663,7 @@ "schedule": "AzureOptimization_ExportMonitorDiskIOPSHourly", "parameters": { "ResourceType": "microsoft.compute/disks", - "ARGFilter": "sku.name =~ 'Premium_LRS' and properties.diskState != 'Unattached'", + "ARGFilter": "sku.name startswith 'Premium_' and properties.diskState =~ 'Attached'", "TimeSpan": "01:00:00", "aggregationType": "Average", "AggregationOfType": "Maximum", @@ -675,7 +675,7 @@ "schedule": "AzureOptimization_ExportMonitorDiskMBPsHourly", "parameters": { "ResourceType": "microsoft.compute/disks", - "ARGFilter": "sku.name =~ 'Premium_LRS' and properties.diskState != 'Unattached'", + "ARGFilter": "sku.name startswith 'Premium_' and properties.diskState =~ 'Attached'", "TimeSpan": "01:00:00", "aggregationType": "Average", "AggregationOfType": "Maximum", @@ -743,7 +743,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1", - "version": "1.1.10.0" + "version": "1.2.0.0" }, "requiredVariables": [ { @@ -760,7 +760,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1", - "version": "1.5.5.0" + "version": "1.6.0.0" }, "requiredVariables": [ ], @@ -769,7 +769,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1", - "version": "2.9.1.0" + "version": "2.10.0.0" }, "requiredVariables": [ { @@ -782,7 +782,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1", - "version": "1.0.0.0" + "version": "1.1.0.0" }, "requiredVariables": [ { @@ -799,7 +799,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1", - "version": "2.4.8.0" + "version": "2.5.0.0" }, "requiredVariables": [ { @@ -812,7 +812,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1", - "version": "1.2.9.0" + "version": "1.3.0.0" }, "requiredVariables": [ { @@ -825,7 +825,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1", - "version": "1.2.9.0" + "version": "1.3.0.0" }, "requiredVariables": [ { @@ -838,7 +838,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1", - "version": "1.0.3.0" + "version": "1.1.0.0" }, "requiredVariables": [ ], @@ -847,7 +847,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1", - "version": "1.0.3.0" + "version": "1.1.0.0" }, "requiredVariables": [ { @@ -864,7 +864,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1", - "version": "1.0.4.0" + "version": "1.1.0.0" }, "requiredVariables": [ { @@ -885,7 +885,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1", - "version": "1.1.1.0" + "version": "1.2.0.0" }, "requiredVariables": [ { @@ -906,7 +906,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1", - "version": "1.1.2.0" + "version": "1.2.0.0" }, "requiredVariables": [ { @@ -927,7 +927,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1", - "version": "1.0.3.0" + "version": "1.1.0.0" }, "requiredVariables": [ { @@ -948,7 +948,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1", - "version": "1.0.3.0" + "version": "1.1.0.0" }, "requiredVariables": [ { @@ -969,7 +969,7 @@ { "runbook": { "name": "runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1", - "version": "1.1.1.0" + "version": "1.2.0.0" }, "requiredVariables": [ { @@ -988,7 +988,7 @@ { "runbook": { "name": "runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1", - "version": "1.2.4.0" + "version": "1.3.0.0" }, "requiredVariables": [ { @@ -1004,7 +1004,7 @@ { "runbook": { "name": "runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1", - "version": "1.0.3.0" + "version": "1.1.0.0" }, "requiredVariables": [ { @@ -1020,7 +1020,7 @@ { "runbook": { "name": "runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1", - "version": "1.0.3.0" + "version": "1.1.0.0" }, "requiredVariables": [ {