diff --git a/docs/deploy/finops-hub-0.4.json b/docs/deploy/finops-hub-0.4.json new file mode 100644 index 000000000..3a2e1c2f2 --- /dev/null +++ b/docs/deploy/finops-hub-0.4.json @@ -0,0 +1,3480 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.24.24.22086", + "templateHash": "4509128994982024537" + } + }, + "parameters": { + "hubName": { + "type": "string", + "metadata": { + "description": "Optional. Name of the hub. Used to ensure unique resource names. Default: \"finops-hub\"." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Azure location where all resources should be created. See https://aka.ms/azureregions. Default: Same as deployment." + } + }, + "fallbackEventGridLocation": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Azure location to use for a temporary Event Grid namespace to register the Microsoft.EventGrid resource provider if the primary location is not supported. The namespace will be deleted and is not used for hub operation. Default: \"\" (same as location)." + } + }, + "storageSku": { + "type": "string", + "defaultValue": "Premium_LRS", + "allowedValues": [ + "Premium_LRS", + "Premium_ZRS" + ], + "metadata": { + "description": "Optional. Storage SKU to use. LRS = Lowest cost, ZRS = High availability. Note Standard SKUs are not available for Data Lake gen2 storage. Allowed: Premium_LRS, Premium_ZRS. Default: Premium_LRS." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to all resources. We will also add the cm-resource-parent tag for improved cost roll-ups in Cost Management." + } + }, + "tagsByResource": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to resources based on their resource type. Resource type specific tags will be merged with tags for all resources." + } + }, + "scopesToMonitor": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. List of scope IDs to monitor and ingest cost for." + } + }, + "exportRetentionInDays": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Number of days of cost data to retain in the ms-cm-exports container. Default: 0." + } + }, + "ingestionRetentionInMonths": { + "type": "int", + "defaultValue": 13, + "metadata": { + "description": "Optional. Number of months of cost data to retain in the ingestion container. Default: 13." + } + }, + "remoteHubStorageUri": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Storage account to push data to for ingestion into a remote hub." + } + }, + "remoteHubStorageKey": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Storage account key to use when pushing data to a remote hub." + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "hub", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "hubName": { + "value": "[parameters('hubName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "fallbackEventGridLocation": { + "value": "[parameters('fallbackEventGridLocation')]" + }, + "storageSku": { + "value": "[parameters('storageSku')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "tagsByResource": { + "value": "[parameters('tagsByResource')]" + }, + "scopesToMonitor": { + "value": "[parameters('scopesToMonitor')]" + }, + "exportRetentionInDays": { + "value": "[parameters('exportRetentionInDays')]" + }, + "ingestionRetentionInMonths": { + "value": "[parameters('ingestionRetentionInMonths')]" + }, + "remoteHubStorageUri": { + "value": "[parameters('remoteHubStorageUri')]" + }, + "remoteHubStorageKey": { + "value": "[parameters('remoteHubStorageKey')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.24.24.22086", + "templateHash": "10941542023228141808" + } + }, + "parameters": { + "hubName": { + "type": "string", + "metadata": { + "description": "Optional. Name of the hub. Used to ensure unique resource names. Default: \"finops-hub\"." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Azure location where all resources should be created. See https://aka.ms/azureregions. Default: (resource group location)." + } + }, + "fallbackEventGridLocation": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Azure location to use for a temporary Event Grid namespace to register the Microsoft.EventGrid resource provider if the primary location is not supported. The namespace will be deleted and is not used for hub operation. Default: \"\" (same as location)." + } + }, + "storageSku": { + "type": "string", + "defaultValue": "Premium_LRS", + "allowedValues": [ + "Premium_LRS", + "Premium_ZRS" + ], + "metadata": { + "description": "Optional. Storage SKU to use. LRS = Lowest cost, ZRS = High availability. Note Standard SKUs are not available for Data Lake gen2 storage. Allowed: Premium_LRS, Premium_ZRS. Default: Premium_LRS." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to all resources. We will also add the cm-resource-parent tag for improved cost roll-ups in Cost Management." + } + }, + "tagsByResource": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to resources based on their resource type. Resource type specific tags will be merged with tags for all resources." + } + }, + "scopesToMonitor": { + "type": "array", + "metadata": { + "description": "Optional. List of scope IDs to monitor and ingest cost for." + } + }, + "exportRetentionInDays": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Number of days of cost data to retain in the ms-cm-exports container. Default: 0." + } + }, + "ingestionRetentionInMonths": { + "type": "int", + "defaultValue": 13, + "metadata": { + "description": "Optional. Number of months of cost data to retain in the ingestion container. Default: 13." + } + }, + "remoteHubStorageUri": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Remote storage account for ingestion dataset." + } + }, + "remoteHubStorageKey": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Storage account key for remote storage account." + } + }, + "enableDefaultTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable telemetry to track anonymous module usage trends, monitor for bugs, and improve future releases." + } + } + }, + "variables": { + "finOpsToolkitVersion": "0.4", + "resourceTags": "[union(parameters('tags'), createObject('cm-resource-parent', format('{0}/providers/Microsoft.Cloud/hubs/{1}', resourceGroup().id, parameters('hubName')), 'ftk-version', variables('finOpsToolkitVersion'), 'ftk-tool', 'FinOps hubs'))]", + "uniqueSuffix": "[uniqueString(parameters('hubName'), resourceGroup().id)]", + "dataFactoryPrefix": "[format('{0}-engine', replace(parameters('hubName'), '_', '-'))]", + "dataFactorySuffix": "[format('-{0}', variables('uniqueSuffix'))]", + "dataFactoryName": "[replace(format('{0}{1}', take(variables('dataFactoryPrefix'), sub(63, length(variables('dataFactorySuffix')))), variables('dataFactorySuffix')), '--', '-')]", + "eventGridPrefix": "[format('{0}-ns', replace(parameters('hubName'), '_', '-'))]", + "eventGridSuffix": "[format('-{0}', variables('uniqueSuffix'))]", + "eventGridName": "[replace(format('{0}{1}', take(variables('eventGridPrefix'), sub(50, length(variables('eventGridSuffix')))), variables('eventGridSuffix')), '--', '-')]", + "eventGridContributorRoleId": "1e241071-0855-49ea-94dc-649edcd759de", + "eventGridAllowedLocations": [ + "eastus2", + "westus3", + "northeurope", + "westeurope", + "southeastasia", + "eastasia", + "southcentralus", + "uaenorth", + "eastus", + "centralus", + "westus2", + "uksouth", + "italynorth", + "australiasoutheast", + "brazilsouth", + "ukwest", + "northcentralus", + "centralindia", + "japaneast", + "francecentral", + "canadacentral", + "australiaeast", + "japanwest", + "canadaeast", + "southindia", + "koreacentral", + "koreasouth", + "switzerlandnorth", + "germanywestcentral", + "norwayeast", + "swedencentral", + "polandcentral", + "israelcentral" + ], + "eventGridLocation": "[if(contains(variables('eventGridAllowedLocations'), parameters('location')), parameters('location'), if(contains(variables('eventGridAllowedLocations'), parameters('fallbackEventGridLocation')), parameters('fallbackEventGridLocation'), variables('eventGridAllowedLocations')[0]))]", + "telemetryId": "00f120b5-2007-6120-0000-40b000000000" + }, + "resources": [ + { + "condition": "[parameters('enableDefaultTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('pid-{0}-{1}', variables('telemetryId'), uniqueString(deployment().name, parameters('location')))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "FinOps toolkit", + "version": "[variables('finOpsToolkitVersion')]" + } + }, + "resources": [] + } + } + }, + { + "type": "Microsoft.EventGrid/namespaces", + "apiVersion": "2023-12-15-preview", + "name": "[variables('eventGridName')]", + "location": "[variables('eventGridLocation')]", + "sku": { + "capacity": 1, + "name": "Standard" + }, + "properties": { + "publicNetworkAccess": "Disabled" + } + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('{0}_cleanup', variables('uniqueSuffix'))]", + "location": "[variables('eventGridLocation')]" + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.EventGrid/namespaces/{0}', variables('eventGridName'))]", + "name": "[guid(variables('eventGridContributorRoleId'), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix'))))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('eventGridContributorRoleId'))]", + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix'))), '2023-01-31').principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName'))]" + ] + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('{0}_deleteEventGrid', variables('uniqueSuffix'))]", + "location": "[parameters('location')]", + "kind": "AzurePowerShell", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix'))))]": {} + } + }, + "properties": { + "azPowerShellVersion": "8.0", + "scriptContent": "Remove-AzResource -Id $env:resourceId -Force", + "timeout": "PT30M", + "cleanupPreference": "OnSuccess", + "retentionInterval": "PT1H", + "environmentVariables": [ + { + "name": "resourceId", + "value": "[resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName'))]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix')))]", + "[extensionResourceId(resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName')), 'Microsoft.Authorization/roleAssignments', guid(variables('eventGridContributorRoleId'), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix')))))]", + "[resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories", + "apiVersion": "2018-06-01", + "name": "[variables('dataFactoryName')]", + "location": "[parameters('location')]", + "tags": "[union(variables('resourceTags'), if(contains(parameters('tagsByResource'), 'Microsoft.DataFactory/factories'), parameters('tagsByResource')['Microsoft.DataFactory/factories'], createObject()))]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "globalConfigurations": { + "PipelineBillingEnabled": "true" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName'))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "storage", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "hubName": { + "value": "[parameters('hubName')]" + }, + "uniqueSuffix": { + "value": "[variables('uniqueSuffix')]" + }, + "sku": { + "value": "[parameters('storageSku')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('resourceTags')]" + }, + "tagsByResource": { + "value": "[parameters('tagsByResource')]" + }, + "scopesToMonitor": { + "value": "[parameters('scopesToMonitor')]" + }, + "msexportRetentionInDays": { + "value": "[parameters('exportRetentionInDays')]" + }, + "ingestionRetentionInMonths": { + "value": "[parameters('ingestionRetentionInMonths')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.24.24.22086", + "templateHash": "10866138453633794931" + } + }, + "parameters": { + "hubName": { + "type": "string", + "metadata": { + "description": "Required. Name of the hub. Used to ensure unique resource names." + } + }, + "uniqueSuffix": { + "type": "string", + "metadata": { + "description": "Required. Suffix to add to the storage account name to ensure uniqueness." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Azure location where all resources should be created. See https://aka.ms/azureregions. Default: (resource group location)." + } + }, + "sku": { + "type": "string", + "defaultValue": "Premium_LRS", + "allowedValues": [ + "Premium_LRS", + "Premium_ZRS" + ], + "metadata": { + "description": "Optional. Storage SKU to use. LRS = Lowest cost, ZRS = High availability. Note Standard SKUs are not available for Data Lake gen2 storage. Allowed: Premium_LRS, Premium_ZRS. Default: Premium_LRS." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to all resources. We will also add the cm-resource-parent tag for improved cost roll-ups in Cost Management." + } + }, + "tagsByResource": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to resources based on their resource type. Resource type specific tags will be merged with tags for all resources." + } + }, + "scopesToMonitor": { + "type": "array", + "metadata": { + "description": "Optional. List of scope IDs to monitor and ingest cost for." + } + }, + "msexportRetentionInDays": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Number of days of cost data to retain in the ms-cm-exports container. Default: 0." + } + }, + "ingestionRetentionInMonths": { + "type": "int", + "defaultValue": 13, + "metadata": { + "description": "Optional. Number of months of cost data to retain in the ingestion container. Default: 13." + } + } + }, + "variables": { + "$fxv#0": "{\r\n \"additionalColumns\": [],\r\n \"translator\": {\r\n \"type\": \"TabularTranslator\",\r\n \"mappings\": [\r\n {\r\n \"source\": { \"name\": \"BilledCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"BilledCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingCurrency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingCurrency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingPeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"BillingPeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingPeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"BillingPeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeClass\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeClass\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeFrequency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeFrequency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargePeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"ChargePeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargePeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"ChargePeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountStatus\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountStatus\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ConsumedQuantity\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ConsumedQuantity\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ConsumedUnit\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ConsumedUnit\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ContractedCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ContractedCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ContractedUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ContractedUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"EffectiveCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"EffectiveCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"InvoiceIssuerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"InvoiceIssuerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ListCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ListCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ListUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ListUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PricingCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingQuantity\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"PricingQuantity\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingUnit\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PricingUnit\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ProviderName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ProviderName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PublisherName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PublisherName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"RegionId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"RegionId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"RegionName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"RegionName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ServiceCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ServiceCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ServiceName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ServiceName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SkuId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SkuId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SkuPriceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SkuPriceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"Tags\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"Tags\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountOwnerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountOwnerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BilledCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BilledCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BilledUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BilledUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingExchangeRate\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BillingExchangeRate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingExchangeRateDate\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_BillingExchangeRateDate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingProfileId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingProfileId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingProfileName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingProfileName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ContractedCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_ContractedCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CostAllocationRuleName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CostAllocationRuleName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CostCenter\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CostCenter\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CustomerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CustomerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CustomerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CustomerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_EffectiveCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_EffectiveCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_EffectiveUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_EffectiveUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceIssuerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceIssuerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceSectionId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceSectionId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceSectionName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceSectionName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ListCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_ListCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PartnerCreditApplied\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PartnerCreditApplied\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PartnerCreditRate\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PartnerCreditRate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingBlockSize\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_PricingBlockSize\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingCurrency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingCurrency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingUnitDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingUnitDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PublisherCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PublisherCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PublisherId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PublisherId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResellerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResellerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResellerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResellerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResourceGroupName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResourceGroupName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResourceType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResourceType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ServicePeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_ServicePeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ServicePeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_ServicePeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuDetails\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuDetails\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuIsCreditEligible\", \"type\": \"Boolean\" },\r\n \"sink\": { \"name\": \"x_SkuIsCreditEligible\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOfferId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOfferId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOrderId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOrderId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOrderName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOrderName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuPartNumber\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuPartNumber\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuRegion\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuRegion\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuServiceFamily\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuServiceFamily\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuTerm\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuTerm\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuTier\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuTier\" }\r\n }\r\n ]\r\n }\r\n}\r\n", + "$fxv#1": "{\r\n \"additionalColumns\": [],\r\n \"translator\": {\r\n \"type\": \"TabularTranslator\",\r\n \"mappings\": [\r\n {\r\n \"source\": { \"name\": \"AvailabilityZone\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"AvailabilityZone\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BilledCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"BilledCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingCurrency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingCurrency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingPeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"BillingPeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingPeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"BillingPeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeFrequency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeFrequency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargePeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"ChargePeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargePeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"ChargePeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"EffectiveCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"EffectiveCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"InvoiceIssuerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"InvoiceIssuerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ListCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ListCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ListUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ListUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PricingCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingQuantity\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"PricingQuantity\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingUnit\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PricingUnit\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ProviderName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ProviderName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PublisherName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PublisherName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"Region\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"Region\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ServiceCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ServiceCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ServiceName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ServiceName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SkuId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SkuId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SkuPriceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SkuPriceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"Tags\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"Tags\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"UsageQuantity\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"UsageQuantity\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"UsageUnit\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"UsageUnit\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountOwnerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountOwnerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BilledCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BilledCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BilledUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BilledUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingExchangeRate\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BillingExchangeRate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingExchangeRateDate\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_BillingExchangeRateDate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingProfileId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingProfileId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingProfileName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingProfileName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ChargeId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ChargeId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CostAllocationRuleName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CostAllocationRuleName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CostCenter\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CostCenter\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CustomerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CustomerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CustomerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CustomerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_EffectiveCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_EffectiveCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_EffectiveUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_EffectiveUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceIssuerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceIssuerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceSectionId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceSectionId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceSectionName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceSectionName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_OnDemandCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_OnDemandCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_OnDemandCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_OnDemandCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_OnDemandUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_OnDemandUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PartnerCreditApplied\", \"type\": \"Boolean\" },\r\n \"sink\": { \"name\": \"x_PartnerCreditApplied\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PartnerCreditRate\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_PartnerCreditRate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingBlockSize\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_PricingBlockSize\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingCurrency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingCurrency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingUnitDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingUnitDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PublisherCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PublisherCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PublisherId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PublisherId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResellerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResellerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResellerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResellerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResourceGroupName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResourceGroupName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResourceType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResourceType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ServicePeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_ServicePeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ServicePeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_ServicePeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuDetails\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuDetails\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuIsCreditEligible\", \"type\": \"Boolean\" },\r\n \"sink\": { \"name\": \"x_SkuIsCreditEligible\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOfferId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOfferId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOrderId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOrderId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOrderName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOrderName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuPartNumber\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuPartNumber\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuRegion\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuRegion\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuServiceFamily\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuServiceFamily\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuTerm\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuTerm\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuTier\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuTier\" }\r\n }\r\n ]\r\n }\r\n}\r\n", + "$fxv#2": "0.4", + "$fxv#3": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\nWrite-Output \"Updating settings.json file...\"\r\nWrite-Output \" Storage account: $env:storageAccountName\"\r\nWrite-Output \" Container: $env:containerName\"\r\n\r\n$validateScopes = { $_.Length -gt 45 }\r\n\r\n# Initialize variables\r\n$fileName = 'settings.json'\r\n$filePath = Join-Path -Path . -ChildPath $fileName\r\n$newScopes = $env:scopes.Split('|') | Where-Object $validateScopes | ForEach-Object { @{ scope = $_ } }\r\n\r\n# Get storage context\r\n$storageContext = @{\r\n Context = New-AzStorageContext -StorageAccountName $env:storageAccountName -UseConnectedAccount\r\n Container = $env:containerName\r\n}\r\n\r\n# Download existing settings, if they exist\r\n$blob = Get-AzStorageBlobContent @storageContext -Blob $fileName -Destination $filePath -Force\r\nif ($blob)\r\n{\r\n \r\n $text = Get-Content $filePath -Raw\r\n Write-Output \"---------\"\r\n Write-Output $text\r\n Write-Output \"---------\"\r\n $json = $text | ConvertFrom-Json\r\n Write-Output \"Existing settings.json file found. Updating...\"\r\n # Rename exportScopes to scopes + convert to object array\r\n if ($json.exportScopes)\r\n {\r\n Write-Output \" Updating exportScopes...\"\r\n if ($json.exportScopes[0] -is [string])\r\n {\r\n Write-Output \" Converting string array to object array...\"\r\n $json.exportScopes = $json.exportScopes | Where-Object $validateScopes | ForEach-Object { @{ scope = $_ } }\r\n if (-not ($json.exportScopes -is [array]))\r\n {\r\n Write-Output \" Converting single object to object array...\"\r\n $json.exportScopes = @($json.exportScopes)\r\n }\r\n }\r\n\r\n Write-Output \" Renaming to 'scopes'...\"\r\n $json | Add-Member -MemberType NoteProperty -Name scopes -Value $json.exportScopes\r\n $json.PSObject.Properties.Remove('exportScopes')\r\n }\r\n}\r\n\r\n# Set default if not found\r\nif (!$json)\r\n{\r\n Write-Output \"No existing settings.json file found. Creating new file...\"\r\n $json = [ordered]@{\r\n '$schema' = 'https://aka.ms/finops/hubs/settings-schema'\r\n type = 'HubInstance'\r\n version = ''\r\n learnMore = 'https://aka.ms/finops/hubs'\r\n scopes = @()\r\n retention = @{\r\n 'msexports' = @{\r\n days = 0\r\n }\r\n 'ingestion' = @{\r\n months = 13\r\n }\r\n }\r\n }\r\n\r\n $text = $json | ConvertTo-Json\r\n Write-Output \"---------\"\r\n Write-Output $text\r\n Write-Output \"---------\"\r\n}\r\n\r\n# Set values from inputs\r\n$json.scopes = $env:scopes.Split('|') | ForEach-Object { @{ 'scope' = $_ } }\r\n$json.retention.msexports.days = [Int32]::Parse($env:msexportRetentionInDays)\r\n$json.retention.ingestion.months = [Int32]::Parse($env:ingestionRetentionInMonths)\r\n\r\n# Updating settings\r\nWrite-Output \"Updating version to $env:ftkVersion...\"\r\n$json.version = $env:ftkVersion\r\nif ($newScopes)\r\n{\r\n Write-Output \"Merging $($newScopes.Count) scopes...\"\r\n $json.scopes = Compare-Object -ReferenceObject $json.scopes -DifferenceObject $newScopes -Property scope -PassThru -IncludeEqual\r\n\r\n # Remove the SideIndicator property from the Compare-Object output\r\n $json.scopes | ForEach-Object { $_.PSObject.Properties.Remove('SideIndicator') } | ConvertTo-Json\r\n\r\n if (-not ($json.scopes -is [array]))\r\n {\r\n $json.scopes = @($json.scopes)\r\n }\r\n Write-Output \"$($json.scopes.Count) scopes found.\"\r\n}\r\n$text = $json | ConvertTo-Json\r\nWrite-Output \"---------\"\r\nWrite-Output $text\r\nWrite-Output \"---------\"\r\n$text | Out-File $filePath\r\n\r\n# Upload new/updated settings\r\nWrite-Output \"Uploading settings.json file...\"\r\nSet-AzStorageBlobContent @storageContext -File $filePath -Force | Out-Null\r\n\r\n# Save focusSchemaFile file to storage\r\n$schemaFiles = $env:schemaFiles | ConvertFrom-Json -Depth 10\r\nWrite-Output \"Uploading ${$schemaFiles.PSObject.Properties.Count} schema files...\"\r\n$schemaFiles.PSObject.Properties | ForEach-Object {\r\n $fileName = \"$($_.Name).json\"\r\n $tempPath = \"./$fileName\"\r\n Write-Output \" Uploading $($_.Name).json...\"\r\n $_.Value | Out-File $tempPath\r\n Set-AzStorageBlobContent @storageContext -File $tempPath -Blob \"schemas/$fileName\" -Force | Out-Null\r\n}\r\n", + "safeHubName": "[replace(replace(toLower(parameters('hubName')), '-', ''), '_', '')]", + "storageAccountSuffix": "[parameters('uniqueSuffix')]", + "storageAccountName": "[format('{0}{1}', take(variables('safeHubName'), sub(24, length(variables('storageAccountSuffix')))), variables('storageAccountSuffix'))]", + "schemaFiles": { + "focuscost_1.0": "[variables('$fxv#0')]", + "focuscost_1.0-preview(v1)": "[variables('$fxv#1')]" + }, + "blobUploadRbacRoles": [ + "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "e40ec5ca-96e0-45a2-b4ff-59039f2c2b59" + ] + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-09-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('sku')]" + }, + "kind": "BlockBlobStorage", + "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.Storage/storageAccounts'), parameters('tagsByResource')['Microsoft.Storage/storageAccounts'], createObject()))]", + "properties": { + "supportsHttpsTrafficOnly": true, + "isHnsEnabled": true, + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false + } + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}', variables('storageAccountName'), 'default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', 'config')]", + "properties": { + "publicAccess": "None", + "metadata": {} + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', 'msexports')]", + "properties": { + "publicAccess": "None", + "metadata": {} + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]" + ] + }, + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2022-09-01", + "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', 'ingestion')]", + "properties": { + "publicAccess": "None", + "metadata": {} + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]" + ] + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('{0}_blobManager', variables('storageAccountName'))]", + "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.ManagedIdentity/userAssignedIdentities'), parameters('tagsByResource')['Microsoft.ManagedIdentity/userAssignedIdentities'], createObject()))]", + "location": "[parameters('location')]" + }, + { + "copy": { + "name": "identityRoleAssignments", + "count": "[length(variables('blobUploadRbacRoles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), variables('blobUploadRbacRoles')[copyIndex()], resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_blobManager', variables('storageAccountName'))))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('blobUploadRbacRoles')[copyIndex()])]", + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_blobManager', variables('storageAccountName'))), '2023-01-31').principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_blobManager', variables('storageAccountName')))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('{0}_uploadSettings', variables('storageAccountName'))]", + "kind": "AzurePowerShell", + "location": "[if(startsWith(parameters('location'), 'china'), 'chinaeast2', parameters('location'))]", + "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.Resources/deploymentScripts'), parameters('tagsByResource')['Microsoft.Resources/deploymentScripts'], createObject()))]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_blobManager', variables('storageAccountName'))))]": {} + } + }, + "properties": { + "azPowerShellVersion": "8.0", + "retentionInterval": "PT1H", + "environmentVariables": [ + { + "name": "ftkVersion", + "value": "[variables('$fxv#2')]" + }, + { + "name": "scopes", + "value": "[join(parameters('scopesToMonitor'), '|')]" + }, + { + "name": "msexportRetentionInDays", + "value": "[string(parameters('msexportRetentionInDays'))]" + }, + { + "name": "ingestionRetentionInMonths", + "value": "[string(parameters('ingestionRetentionInMonths'))]" + }, + { + "name": "storageAccountName", + "value": "[variables('storageAccountName')]" + }, + { + "name": "containerName", + "value": "config" + }, + { + "name": "schemaFiles", + "value": "[string(variables('schemaFiles'))]" + } + ], + "scriptContent": "[variables('$fxv#3')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', 'config')]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_blobManager', variables('storageAccountName')))]", + "identityRoleAssignments" + ] + } + ], + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the storage account." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the storage account." + }, + "value": "[variables('storageAccountName')]" + }, + "configContainer": { + "type": "string", + "metadata": { + "description": "The name of the container used for configuration settings." + }, + "value": "config" + }, + "exportContainer": { + "type": "string", + "metadata": { + "description": "The name of the container used for Cost Management exports." + }, + "value": "msexports" + }, + "ingestionContainer": { + "type": "string", + "metadata": { + "description": "The name of the container used for normalized data ingestion." + }, + "value": "ingestion" + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "dataFactoryResources", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "dataFactoryName": { + "value": "[variables('dataFactoryName')]" + }, + "storageAccountName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.name.value]" + }, + "exportContainerName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.exportContainer.value]" + }, + "configContainerName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.configContainer.value]" + }, + "ingestionContainerName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.ingestionContainer.value]" + }, + "keyVaultName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'keyVault'), '2022-09-01').outputs.name.value]" + }, + "location": { + "value": "[parameters('location')]" + }, + "hubName": { + "value": "[parameters('hubName')]" + }, + "remoteHubStorageUri": { + "value": "[parameters('remoteHubStorageUri')]" + }, + "tags": { + "value": "[variables('resourceTags')]" + }, + "tagsByResource": { + "value": "[parameters('tagsByResource')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.24.24.22086", + "templateHash": "2392183082773057596" + } + }, + "functions": [ + { + "namespace": "__bicep", + "members": { + "getExportBody": { + "parameters": [ + { + "type": "string", + "name": "exportContainerName" + }, + { + "type": "string", + "name": "focusSchemaVersion" + }, + { + "type": "bool", + "name": "isMonthly" + } + ], + "output": { + "type": "string", + "value": "[format('{{ \"properties\": {{ \"definition\": {{ \"dataSet\": {{ \"configuration\": {{ \"dataVersion\": \"{0}\", \"filters\": [] }}, \"granularity\": \"Daily\" }}, \"timeframe\": \"{1}\", \"type\": \"FocusCost\" }}, \"deliveryInfo\": {{ \"destination\": {{ \"container\": \"{2}\", \"rootFolderPath\": \"@{{item().scope}}\", \"type\": \"AzureBlob\", \"resourceId\": \"@{{variables(''storageAccountId'')}}\" }} }}, \"schedule\": {{ \"recurrence\": \"{3}\", \"recurrencePeriod\": {{ \"from\": \"2024-01-01T00:00:00.000Z\", \"to\": \"2050-02-01T00:00:00.000Z\" }}, \"status\": \"Inactive\" }}, \"format\": \"Csv\", \"partitionData\": true, \"dataOverwriteBehavior\": \"CreateNewReport\", \"compressionMode\": \"None\" }}, \"id\": \"@{{variables(''resourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports/@{{variables(''exportName'')}}\", \"name\": \"@{{variables(''exportName'')}}\", \"type\": \"Microsoft.CostManagement/reports\", \"identity\": {{ \"type\": \"systemAssigned\" }}, \"location\": \"global\" }}', parameters('focusSchemaVersion'), if(parameters('isMonthly'), 'MonthToDate', 'TheLastMonth'), parameters('exportContainerName'), if(parameters('isMonthly'), 'Monthly', 'Daily'))]" + } + } + } + } + ], + "parameters": { + "hubName": { + "type": "string", + "metadata": { + "description": "Required. Name of the FinOps hub instance." + } + }, + "dataFactoryName": { + "type": "string", + "metadata": { + "description": "Required. Name of the Data Factory instance." + } + }, + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Azure Key Vault instance." + } + }, + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Azure storage account instance." + } + }, + "exportContainerName": { + "type": "string", + "metadata": { + "description": "Required. The name of the container where Cost Management data is exported." + } + }, + "ingestionContainerName": { + "type": "string", + "metadata": { + "description": "Required. The name of the container where normalized data is ingested." + } + }, + "configContainerName": { + "type": "string", + "metadata": { + "description": "Required. The name of the container where normalized data is ingested." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location to use for the managed identity and deployment script to auto-start triggers. Default = (resource group location)." + } + }, + "remoteHubStorageUri": { + "type": "string", + "metadata": { + "description": "Optional. Remote storage account for ingestion dataset." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to all resources. We will also add the cm-resource-parent tag for improved cost roll-ups in Cost Management." + } + }, + "tagsByResource": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to resources based on their resource type. Resource type specific tags will be merged with tags for all resources." + } + } + }, + "variables": { + "$fxv#0": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\n# Init outputs\r\n$DeploymentScriptOutputs = @{}\r\n\r\n# \r\n$adfParams = @{\r\n ResourceGroupName = $env:DataFactoryResourceGroup\r\n DataFactoryName = $env:DataFactoryName\r\n}\r\n\r\n# Delete old triggers\r\n$triggers = Get-AzDataFactoryV2Trigger @adfParams -ErrorAction SilentlyContinue `\r\n| Where-Object { $_.Name -match '^msexports(_(setup|daily|monthly|extract))?$' }\r\n$DeploymentScriptOutputs[\"stopTriggers\"] = $triggers | Stop-AzDataFactoryV2Trigger -Force -ErrorAction SilentlyContinue\r\n$DeploymentScriptOutputs[\"deleteTriggers\"] = $triggers | Remove-AzDataFactoryV2Trigger -Force -ErrorAction SilentlyContinue\r\n\r\n# Delete old pipelines\r\n$DeploymentScriptOutputs[\"pipelines\"] = Get-AzDataFactoryV2Pipeline @adfParams -ErrorAction SilentlyContinue `\r\n| Where-Object { $_.Name -match '^msexports_(backfill|extract|fill|get|run|setup|transform)$' } `\r\n| Remove-AzDataFactoryV2Pipeline -Force -ErrorAction SilentlyContinue\r\n", + "$fxv#1": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\nParam(\r\n [switch] $Stop\r\n)\r\n\r\n# Init outputs\r\n$DeploymentScriptOutputs = @{}\r\n\r\nif (-not $Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n\r\n# Loop thru triggers\r\n$env:Triggers.Split('|') `\r\n| ForEach-Object {\r\n $trigger = $_\r\n if ($Stop) {\r\n Write-Output \"Stopping trigger $trigger...\"\r\n $triggerOutput = Stop-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force `\r\n -ErrorAction SilentlyContinue # Ignore errors, since the trigger may not exist\r\n } else {\r\n Write-Output \"Starting trigger $trigger...\"\r\n $triggerOutput = Start-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force\r\n }\r\n if ($triggerOutput) {\r\n Write-Output \"done...\"\r\n } else {\r\n Write-Output \"failed...\"\r\n }\r\n $DeploymentScriptOutputs[$trigger] = $triggerOutput\r\n}\r\n\r\nif ($Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n", + "$fxv#2": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\nParam(\r\n [switch] $Stop\r\n)\r\n\r\n# Init outputs\r\n$DeploymentScriptOutputs = @{}\r\n\r\nif (-not $Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n\r\n# Loop thru triggers\r\n$env:Triggers.Split('|') `\r\n| ForEach-Object {\r\n $trigger = $_\r\n if ($Stop) {\r\n Write-Output \"Stopping trigger $trigger...\"\r\n $triggerOutput = Stop-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force `\r\n -ErrorAction SilentlyContinue # Ignore errors, since the trigger may not exist\r\n } else {\r\n Write-Output \"Starting trigger $trigger...\"\r\n $triggerOutput = Start-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force\r\n }\r\n if ($triggerOutput) {\r\n Write-Output \"done...\"\r\n } else {\r\n Write-Output \"failed...\"\r\n }\r\n $DeploymentScriptOutputs[$trigger] = $triggerOutput\r\n}\r\n\r\nif ($Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n", + "focusSchemaVersion": "1.0", + "ftkVersion": "0.4", + "exportApiVersion": "2023-07-01-preview", + "datasetPropsDefault": { + "location": { + "type": "AzureBlobFSLocation", + "fileName": { + "value": "@{dataset().fileName}", + "type": "Expression" + }, + "folderPath": { + "value": "@{dataset().folderPath}", + "type": "Expression" + } + } + }, + "safeExportContainerName": "[replace(format('{0}', parameters('exportContainerName')), '-', '_')]", + "safeIngestionContainerName": "[replace(format('{0}', parameters('ingestionContainerName')), '-', '_')]", + "safeConfigContainerName": "[replace(format('{0}', parameters('configContainerName')), '-', '_')]", + "fileAddedExportTriggerName": "[format('{0}_FileAdded', variables('safeExportContainerName'))]", + "updateConfigTriggerName": "[format('{0}_SettingsUpdated', variables('safeConfigContainerName'))]", + "dailyTriggerName": "[format('{0}_DailySchedule', variables('safeConfigContainerName'))]", + "monthlyTriggerName": "[format('{0}_MonthlySchedule', variables('safeConfigContainerName'))]", + "allHubTriggers": [ + "[variables('fileAddedExportTriggerName')]", + "[variables('updateConfigTriggerName')]", + "[variables('dailyTriggerName')]", + "[variables('monthlyTriggerName')]" + ], + "autoStartRbacRoles": [ + "673868aa-7521-48a0-acc6-0f60742d39f5", + "e40ec5ca-96e0-45a2-b4ff-59039f2c2b59" + ], + "storageRbacRoles": [ + "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "acdd72a7-3385-48ef-bd42-f606fba81ae7", + "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9" + ] + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('{0}_triggerManager', parameters('dataFactoryName'))]", + "location": "[parameters('location')]", + "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.ManagedIdentity/userAssignedIdentities'), parameters('tagsByResource')['Microsoft.ManagedIdentity/userAssignedIdentities'], createObject()))]" + }, + { + "copy": { + "name": "identityRoleAssignments", + "count": "[length(variables('autoStartRbacRoles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.DataFactory/factories/{0}', parameters('dataFactoryName'))]", + "name": "[guid(resourceId('Microsoft.DataFactory/factories', parameters('dataFactoryName')), variables('autoStartRbacRoles')[copyIndex()], resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('autoStartRbacRoles')[copyIndex()])]", + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))), '2023-01-31').principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]" + ] + }, + { + "copy": { + "name": "pipelineIdentityRoleAssignments", + "count": "[length(variables('storageRbacRoles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), variables('storageRbacRoles')[copyIndex()], resourceId('Microsoft.DataFactory/factories', parameters('dataFactoryName')))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('storageRbacRoles')[copyIndex()])]", + "principalId": "[reference(resourceId('Microsoft.DataFactory/factories', parameters('dataFactoryName')), '2018-06-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('{0}_deleteOldResources', parameters('dataFactoryName'))]", + "location": "[if(startsWith(parameters('location'), 'china'), 'chinaeast2', parameters('location'))]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]": {} + } + }, + "kind": "AzurePowerShell", + "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.Resources/deploymentScripts'), parameters('tagsByResource')['Microsoft.Resources/deploymentScripts'], createObject()))]", + "properties": { + "azPowerShellVersion": "8.0", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess", + "scriptContent": "[variables('$fxv#0')]", + "environmentVariables": [ + { + "name": "DataFactorySubscriptionId", + "value": "[subscription().id]" + }, + { + "name": "DataFactoryResourceGroup", + "value": "[resourceGroup().name]" + }, + { + "name": "DataFactoryName", + "value": "[parameters('dataFactoryName')]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]", + "identityRoleAssignments" + ] + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('{0}_stopTriggers', parameters('dataFactoryName'))]", + "location": "[parameters('location')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]": {} + } + }, + "kind": "AzurePowerShell", + "tags": "[parameters('tags')]", + "properties": { + "azPowerShellVersion": "8.0", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess", + "scriptContent": "[variables('$fxv#1')]", + "arguments": "-Stop", + "environmentVariables": [ + { + "name": "DataFactorySubscriptionId", + "value": "[subscription().id]" + }, + { + "name": "DataFactoryResourceGroup", + "value": "[resourceGroup().name]" + }, + { + "name": "DataFactoryName", + "value": "[parameters('dataFactoryName')]" + }, + { + "name": "Triggers", + "value": "[join(variables('allHubTriggers'), '|')]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]", + "identityRoleAssignments" + ] + }, + { + "type": "Microsoft.DataFactory/factories/linkedservices", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), parameters('keyVaultName'))]", + "properties": { + "annotations": [], + "parameters": {}, + "type": "AzureKeyVault", + "typeProperties": { + "baseUrl": "[reference(format('Microsoft.KeyVault/vaults/{0}', parameters('keyVaultName')), '2023-02-01').vaultUri]" + } + } + }, + { + "type": "Microsoft.DataFactory/factories/linkedservices", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), parameters('storageAccountName'))]", + "properties": { + "annotations": [], + "parameters": {}, + "type": "AzureBlobFS", + "typeProperties": { + "url": "[reference(format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName')), '2021-08-01').primaryEndpoints.dfs]" + } + } + }, + { + "condition": "[not(empty(parameters('remoteHubStorageUri')))]", + "type": "Microsoft.DataFactory/factories/linkedservices", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), 'remoteHubStorage')]", + "properties": { + "annotations": [], + "parameters": {}, + "type": "AzureBlobFS", + "typeProperties": { + "url": "[parameters('remoteHubStorageUri')]", + "accountKey": { + "type": "AzureKeyVaultSecret", + "store": { + "referenceName": "[parameters('keyVaultName')]", + "type": "LinkedServiceReference" + }, + "secretName": "[format('{0}-storage-key', toLower(parameters('hubName')))]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('keyVaultName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/datasets", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", + "properties": { + "annotations": [], + "parameters": { + "fileName": { + "type": "String", + "defaultValue": "settings.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" + } + }, + "type": "Json", + "typeProperties": "[variables('datasetPropsDefault')]", + "linkedServiceName": { + "parameters": {}, + "referenceName": "[parameters('storageAccountName')]", + "type": "LinkedServiceReference" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/datasets", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), 'manifest')]", + "properties": { + "annotations": [], + "parameters": { + "fileName": { + "type": "String", + "defaultValue": "manifest.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('exportContainerName')]" + } + }, + "type": "Json", + "typeProperties": "[variables('datasetPropsDefault')]", + "linkedServiceName": { + "parameters": {}, + "referenceName": "[parameters('storageAccountName')]", + "type": "LinkedServiceReference" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/datasets", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('safeExportContainerName'))]", + "properties": { + "annotations": [], + "parameters": { + "blobPath": { + "type": "String" + } + }, + "type": "DelimitedText", + "typeProperties": { + "location": { + "type": "AzureBlobFSLocation", + "fileName": { + "value": "@{dataset().blobPath}", + "type": "Expression" + }, + "fileSystem": "[variables('safeExportContainerName')]" + }, + "columnDelimiter": ",", + "escapeChar": "\"", + "quoteChar": "\"", + "firstRowAsHeader": true + }, + "linkedServiceName": { + "parameters": {}, + "referenceName": "[parameters('storageAccountName')]", + "type": "LinkedServiceReference" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/datasets", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('safeIngestionContainerName'))]", + "properties": { + "annotations": [], + "parameters": { + "blobPath": { + "type": "String" + } + }, + "type": "Parquet", + "typeProperties": { + "location": { + "type": "AzureBlobFSLocation", + "fileName": { + "value": "@{dataset().blobPath}", + "type": "Expression" + }, + "fileSystem": "[variables('safeIngestionContainerName')]" + } + }, + "linkedServiceName": { + "parameters": {}, + "referenceName": "[if(empty(parameters('remoteHubStorageUri')), parameters('storageAccountName'), 'remoteHubStorage')]", + "type": "LinkedServiceReference" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), 'remoteHubStorage')]", + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/triggers", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('fileAddedExportTriggerName'))]", + "properties": { + "annotations": [], + "pipelines": [ + { + "pipelineReference": { + "referenceName": "[format('{0}_ExecuteETL', variables('safeExportContainerName'))]", + "type": "PipelineReference" + }, + "parameters": { + "folderPath": "@triggerBody().folderPath", + "fileName": "@triggerBody().fileName" + } + } + ], + "type": "BlobEventsTrigger", + "typeProperties": { + "blobPathBeginsWith": "[format('/{0}/blobs/', parameters('exportContainerName'))]", + "blobPathEndsWith": "manifest.json", + "ignoreEmptyBlobs": true, + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "events": [ + "Microsoft.Storage.BlobCreated" + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ExecuteETL', variables('safeExportContainerName')))]", + "[resourceId('Microsoft.Resources/deploymentScripts', format('{0}_stopTriggers', parameters('dataFactoryName')))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/triggers", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('updateConfigTriggerName'))]", + "properties": { + "annotations": [], + "pipelines": [ + { + "pipelineReference": { + "referenceName": "[format('{0}_ConfigureExports', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + } + } + ], + "type": "BlobEventsTrigger", + "typeProperties": { + "blobPathBeginsWith": "[format('/{0}/blobs/', parameters('configContainerName'))]", + "blobPathEndsWith": "settings.json", + "ignoreEmptyBlobs": true, + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "events": [ + "Microsoft.Storage.BlobCreated" + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ConfigureExports', variables('safeConfigContainerName')))]", + "[resourceId('Microsoft.Resources/deploymentScripts', format('{0}_stopTriggers', parameters('dataFactoryName')))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/triggers", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('dailyTriggerName'))]", + "properties": { + "pipelines": [ + { + "pipelineReference": { + "referenceName": "[format('{0}_ExportData', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + }, + "parameters": { + "Recurrence": "Daily" + } + } + ], + "type": "ScheduleTrigger", + "typeProperties": { + "recurrence": { + "frequency": "Hour", + "interval": 24, + "startTime": "2023-01-01T01:01:00", + "timeZone": "[reference(resourceId('Microsoft.Resources/deployments', 'azuretimezones'), '2022-09-01').outputs.Timezone.value]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'azuretimezones')]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ExportData', variables('safeConfigContainerName')))]", + "[resourceId('Microsoft.Resources/deploymentScripts', format('{0}_stopTriggers', parameters('dataFactoryName')))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/triggers", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('monthlyTriggerName'))]", + "properties": { + "pipelines": [ + { + "pipelineReference": { + "referenceName": "[format('{0}_ExportData', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + }, + "parameters": { + "Recurrence": "Monthly" + } + } + ], + "type": "ScheduleTrigger", + "typeProperties": { + "recurrence": { + "frequency": "Month", + "interval": 1, + "startTime": "2023-01-05T01:11:00", + "timeZone": "[reference(resourceId('Microsoft.Resources/deployments', 'azuretimezones'), '2022-09-01').outputs.Timezone.value]", + "schedule": { + "monthDays": [ + 5, + 19 + ] + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'azuretimezones')]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ExportData', variables('safeConfigContainerName')))]", + "[resourceId('Microsoft.Resources/deploymentScripts', format('{0}_stopTriggers', parameters('dataFactoryName')))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/pipelines", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_BackfillData', variables('safeConfigContainerName')))]", + "properties": { + "activities": [ + { + "name": "Get Config", + "type": "Lookup", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('fileName')", + "type": "Expression" + }, + "folderPath": { + "value": "@variables('folderPath')", + "type": "Expression" + } + } + } + } + }, + { + "name": "Set backfill end date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "endDate", + "value": { + "value": "@addDays(startOfMonth(utcNow()), -1)", + "type": "Expression" + } + } + }, + { + "name": "Set backfill start date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "startDate", + "value": { + "value": "@subtractFromTime(startOfMonth(utcNow()), activity('Get Config').output.firstRow.retention.ingestion.months, 'Month')", + "type": "Expression" + } + } + }, + { + "name": "Set export start date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Set backfill start date", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "thisMonth", + "value": { + "value": "@startOfMonth(variables('endDate'))", + "type": "Expression" + } + } + }, + { + "name": "Set export end date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Set export start date", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "nextMonth", + "value": { + "value": "@startOfMonth(subtractFromTime(variables('thisMonth'), 1, 'Month'))", + "type": "Expression" + } + } + }, + { + "name": "Every Month", + "type": "Until", + "dependsOn": [ + { + "activity": "Set export end date", + "dependencyConditions": [ + "Succeeded" + ] + }, + { + "activity": "Set backfill end date", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "expression": { + "value": "@less(variables('thisMonth'), variables('startDate'))", + "type": "Expression" + }, + "activities": [ + { + "name": "Update export start date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Backfill data", + "dependencyConditions": [ + "Completed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "thisMonth", + "value": { + "value": "@variables('nextMonth')", + "type": "Expression" + } + } + }, + { + "name": "Update export end date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Update export start date", + "dependencyConditions": [ + "Completed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "nextMonth", + "value": { + "value": "@subtractFromTime(variables('thisMonth'), 1, 'Month')", + "type": "Expression" + } + } + }, + { + "name": "Backfill data", + "type": "ExecutePipeline", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "pipeline": { + "referenceName": "[format('{0}_RunBackfill', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + }, + "waitOnCompletion": true, + "parameters": { + "StartDate": { + "value": "@variables('thisMonth')", + "type": "Expression" + }, + "EndDate": { + "value": "@addDays(addToTime(variables('thisMonth'), 1, 'Month'), -1)", + "type": "Expression" + } + } + } + } + ], + "timeout": "0.12:00:00" + } + } + ], + "concurrency": 1, + "variables": { + "exportName": { + "type": "String" + }, + "storageAccountId": { + "type": "String", + "defaultValue": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "finOpsHub": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + }, + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" + }, + "fileName": { + "type": "String", + "defaultValue": "settings.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" + }, + "endDate": { + "type": "String" + }, + "startDate": { + "type": "String" + }, + "thisMonth": { + "type": "String" + }, + "nextMonth": { + "type": "String" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_RunBackfill', variables('safeConfigContainerName')))]" + ], + "metadata": { + "description": "Runs the backfill job for each month based on retention settings." + } + }, + { + "type": "Microsoft.DataFactory/factories/pipelines", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_RunBackfill', variables('safeConfigContainerName')))]", + "properties": { + "activities": [ + { + "name": "Get Config", + "type": "Lookup", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('fileName')", + "type": "Expression" + }, + "folderPath": { + "value": "@variables('folderPath')", + "type": "Expression" + } + } + } + } + }, + { + "name": "ForEach Export Scope", + "type": "ForEach", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "items": { + "value": "@activity('Get Config').output.firstRow.scopes", + "type": "Expression" + }, + "isSequential": true, + "activities": [ + { + "name": "Set backfill export name", + "type": "SetVariable", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "variableName": "exportName", + "value": { + "value": "@toLower(concat(variables('finOpsHub'), '-monthly-costdetails'))", + "type": "Expression" + } + } + }, + { + "name": "Trigger backfill export", + "type": "WebActivity", + "dependsOn": [ + { + "activity": "Set backfill export name", + "dependencyConditions": [ + "Completed" + ] + } + ], + "policy": { + "timeout": "0.00:05:00", + "retry": 1, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{variables(''resourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports/@{{variables(''exportName'')}}/run?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "POST", + "headers": { + "x-ms-command-name": "[format('FinOpsToolkit.Hubs.config_RunBackfill@{0}', variables('ftkVersion'))]", + "Content-Type": "application/json", + "ClientType": "[format('FinOpsToolkit.Hubs@{0}', variables('ftkVersion'))]" + }, + "body": "{\"timePeriod\" : { \"from\" : \"@{pipeline().parameters.StartDate}\", \"to\" : \"@{pipeline().parameters.EndDate}\" }}", + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('resourceManagementUri')", + "type": "Expression" + } + } + } + } + ] + } + } + ], + "concurrency": 1, + "parameters": { + "StartDate": { + "type": "string" + }, + "EndDate": { + "type": "string" + } + }, + "variables": { + "exportName": { + "type": "String" + }, + "storageAccountId": { + "type": "String", + "defaultValue": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "finOpsHub": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + }, + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" + }, + "fileName": { + "type": "String", + "defaultValue": "settings.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]" + ], + "metadata": { + "description": "Creates and triggers exports for all defined scopes for the specified date range." + } + }, + { + "type": "Microsoft.DataFactory/factories/pipelines", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ExportData', variables('safeConfigContainerName')))]", + "properties": { + "activities": [ + { + "name": "Get Config", + "type": "Lookup", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('fileName')", + "type": "Expression" + }, + "folderPath": { + "value": "@variables('folderPath')", + "type": "Expression" + } + } + } + } + }, + { + "name": "ForEach Export Scope", + "type": "ForEach", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "items": { + "value": "@activity('Get Config').output.firstRow.scopes", + "type": "Expression" + }, + "isSequential": true, + "activities": [ + { + "name": "Get exports for scope", + "type": "WebActivity", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{variables(''resourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "GET", + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('resourceManagementUri')", + "type": "Expression" + } + } + } + }, + { + "name": "Run exports for scope", + "type": "ExecutePipeline", + "dependsOn": [ + { + "activity": "Get exports for scope", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "pipeline": { + "referenceName": "[format('{0}_RunExports', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + }, + "waitOnCompletion": true, + "parameters": { + "ExportScopes": { + "value": "@activity('Get exports for scope').output.value", + "type": "Expression" + }, + "Recurrence": { + "value": "@pipeline().parameters.Recurrence", + "type": "Expression" + } + } + } + } + ] + } + } + ], + "concurrency": 1, + "parameters": { + "Recurrence": { + "type": "string", + "defaultValue": "Daily" + } + }, + "variables": { + "fileName": { + "type": "String", + "defaultValue": "settings.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" + }, + "finOpsHub": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + }, + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_RunExports', variables('safeConfigContainerName')))]" + ], + "metadata": { + "description": "Gets a list of all Cost Management exports configured for this hub based on the scopes defined in settings.json, then runs each export using the config_RunExports pipeline." + } + }, + { + "type": "Microsoft.DataFactory/factories/pipelines", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_RunExports', variables('safeConfigContainerName')))]", + "properties": { + "activities": [ + { + "name": "ForEach export scope", + "type": "ForEach", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "items": { + "value": "@pipeline().parameters.exportScopes", + "type": "Expression" + }, + "isSequential": true, + "activities": [ + { + "name": "If scheduled", + "type": "IfCondition", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "expression": { + "value": "@and(equals(toLower(item().properties.schedule.recurrence), toLower(pipeline().parameters.Recurrence)),startswith(toLower(item().name), toLower(variables('hubName'))))", + "type": "Expression" + }, + "ifTrueActivities": [ + { + "name": "Trigger export", + "type": "WebActivity", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{replace(toLower(concat(variables(''resourceManagementUri''),item().id)), ''com//'', ''com/'')}}/run?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "POST", + "headers": { + "x-ms-command-name": "[format('FinOpsToolkit.Hubs.config_RunExports@{0}', variables('ftkVersion'))]", + "ClientType": "[format('FinOpsToolkit.Hubs@{0}', variables('ftkVersion'))]" + }, + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('resourceManagementUri')", + "type": "Expression" + } + } + } + } + ] + } + } + ] + } + } + ], + "concurrency": 1, + "parameters": { + "ExportScopes": { + "type": "array" + }, + "Recurrence": { + "type": "string", + "defaultValue": "Daily" + } + }, + "variables": { + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" + }, + "hubName": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]" + ], + "metadata": { + "description": "Runs the specified Cost Management exports." + } + }, + { + "type": "Microsoft.DataFactory/factories/pipelines", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ConfigureExports', variables('safeConfigContainerName')))]", + "properties": { + "activities": [ + { + "name": "Get Config", + "type": "Lookup", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('fileName')", + "type": "Expression" + }, + "folderPath": { + "value": "@variables('folderPath')", + "type": "Expression" + } + } + } + } + }, + { + "name": "ForEach Export Scope", + "type": "ForEach", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "items": { + "value": "@activity('Get Config').output.firstRow.scopes", + "type": "Expression" + }, + "isSequential": true, + "activities": [ + { + "name": "Create or update open month focus export", + "type": "WebActivity", + "dependsOn": [ + { + "activity": "Set open month focus export name", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{variables(''resourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports/@{{variables(''exportName'')}}?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "PUT", + "body": { + "value": "[__bicep.getExportBody(parameters('exportContainerName'), variables('focusSchemaVersion'), false())]", + "type": "Expression" + }, + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('ResourceManagementUri')", + "type": "Expression" + } + } + } + }, + { + "name": "Set open month focus export name", + "type": "SetVariable", + "dependsOn": [], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "exportName", + "value": { + "value": "@toLower(concat(variables('finOpsHub'), '-daily-costdetails'))", + "type": "Expression" + } + } + }, + { + "name": "Create or update closed month focus export", + "type": "WebActivity", + "dependsOn": [ + { + "activity": "Set closed month focus export name", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{variables(''ResourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports/@{{variables(''exportName'')}}?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "PUT", + "body": { + "value": "[__bicep.getExportBody(parameters('exportContainerName'), variables('focusSchemaVersion'), true())]", + "type": "Expression" + }, + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('ResourceManagementUri')", + "type": "Expression" + } + } + } + }, + { + "name": "Set closed month focus export name", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Create or update open month focus export", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "exportName", + "value": { + "value": "@toLower(concat(variables('finOpsHub'), '-monthly-costdetails'))", + "type": "Expression" + } + } + } + ] + } + } + ], + "concurrency": 1, + "variables": { + "exportName": { + "type": "String" + }, + "exportScope": { + "type": "String" + }, + "storageAccountId": { + "type": "String", + "defaultValue": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "finOpsHub": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + }, + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" + }, + "fileName": { + "type": "String", + "defaultValue": "settings.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]" + ], + "metadata": { + "description": "Creates Cost Management exports for all scopes." + } + }, + { + "type": "Microsoft.DataFactory/factories/pipelines", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ExecuteETL', variables('safeExportContainerName')))]", + "properties": { + "activities": [ + { + "name": "Wait", + "type": "Wait", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "waitTimeInSeconds": 60 + } + }, + { + "name": "Read Manifest", + "type": "Lookup", + "dependsOn": [ + { + "activity": "Wait", + "dependencyConditions": [ + "Completed" + ] + } + ], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "manifest", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@pipeline().parameters.fileName", + "type": "Expression" + }, + "folderPath": { + "value": "@pipeline().parameters.folderPath", + "type": "Expression" + } + } + } + } + }, + { + "name": "Set Dataset Type", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Read Manifest", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "datasetType", + "value": { + "value": "@activity('Read Manifest').output.firstRow.exportConfig.type", + "type": "Expression" + } + } + }, + { + "name": "Set Dataset Version", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Read Manifest", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "datasetVersion", + "value": { + "value": "@activity('Read Manifest').output.firstRow.exportConfig.dataVersion", + "type": "Expression" + } + } + }, + { + "name": "Set Schema File", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Set Dataset Type", + "dependencyConditions": [ + "Succeeded" + ] + }, + { + "activity": "Set Dataset Version", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "schemaFile", + "value": { + "value": "@toLower(concat(variables('datasetType'), '_', variables('datasetVersion'), '.json'))", + "type": "Expression" + } + } + }, + { + "name": "Set Scope", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Read Manifest", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "scope", + "value": { + "value": "@split(toLower(activity('Read Manifest').output.firstRow.exportConfig.resourceId), '/providers/microsoft.costmanagement/exports/')[0]", + "type": "Expression" + } + } + }, + { + "name": "Set Date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Read Manifest", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "date", + "value": { + "value": "@replace(substring(activity('Read Manifest').output.firstRow.runInfo.startDate, 0, 7), '-', '')", + "type": "Expression" + } + } + }, + { + "name": "Failed to Read Manifest", + "type": "Fail", + "dependsOn": [ + { + "activity": "Set Date", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Set Dataset Type", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Set Scope", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Read Manifest", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Set Dataset Version", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Set Schema File", + "dependencyConditions": [ + "Failed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "message": { + "value": "@concat('Failed to read the manifest file for this export run. Manifest path: ', pipeline().parameters.folderPath)", + "type": "Expression" + }, + "errorCode": "ManifestReadFailed" + } + }, + { + "name": "Check Schema", + "type": "GetMetadata", + "dependsOn": [ + { + "activity": "Set Scope", + "dependencyConditions": [ + "Succeeded" + ] + }, + { + "activity": "Set Date", + "dependencyConditions": [ + "Succeeded" + ] + }, + { + "activity": "Set Schema File", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('schemaFile')", + "type": "Expression" + }, + "folderPath": "[format('{0}/schemas', parameters('configContainerName'))]" + } + }, + "fieldList": [ + "exists" + ], + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + } + }, + { + "name": "Schema Not Found", + "type": "Fail", + "dependsOn": [ + { + "activity": "Check Schema", + "dependencyConditions": [ + "Failed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "message": { + "value": "@concat('The ', variables('schemaFile'), ' schema mapping file was not found. Please confirm version ', variables('datasetVersion'), ' of the ', variables('datasetType'), ' dataset is supported by this version of FinOps hubs. You may need to upgrade to a newer release. To add support for another dataset, you can create a custom mapping file.')", + "type": "Expression" + }, + "errorCode": "SchemaNotFound" + } + }, + { + "name": "For Each Blob", + "type": "ForEach", + "dependsOn": [ + { + "activity": "Check Schema", + "dependencyConditions": [ + "Completed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "items": { + "value": "@activity('Read Manifest').output.firstRow.blobs", + "type": "Expression" + }, + "isSequential": false, + "activities": [ + { + "name": "Execute", + "type": "ExecutePipeline", + "dependsOn": [], + "policy": { + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "pipeline": { + "referenceName": "[format('{0}_ETL_{1}', variables('safeExportContainerName'), variables('safeIngestionContainerName'))]", + "type": "PipelineReference" + }, + "waitOnCompletion": true, + "parameters": { + "blobPath": { + "value": "@item().blobName", + "type": "Expression" + }, + "destinationFolder": { + "value": "@toLower(replace(concat(variables('scope'),'/',variables('date'),'/',variables('datasetType')),'//','/'))", + "type": "Expression" + }, + "schemaFile": { + "value": "@variables('schemaFile')", + "type": "Expression" + } + } + } + } + ] + } + } + ], + "parameters": { + "folderPath": { + "type": "string" + }, + "fileName": { + "type": "string" + } + }, + "variables": { + "scope": { + "type": "String" + }, + "date": { + "type": "String" + }, + "datasetType": { + "type": "String" + }, + "datasetVersion": { + "type": "String" + }, + "schemaFile": { + "type": "String" + } + }, + "annotations": [] + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), 'manifest')]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ETL_{1}', variables('safeExportContainerName'), variables('safeIngestionContainerName')))]" + ], + "metadata": { + "description": "Queues the msexports_ETL_ingestion pipeline." + } + }, + { + "type": "Microsoft.DataFactory/factories/pipelines", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ETL_{1}', variables('safeExportContainerName'), variables('safeIngestionContainerName')))]", + "properties": { + "activities": [ + { + "name": "Set Destination File Name", + "description": "", + "type": "SetVariable", + "dependsOn": [], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "destinationFile", + "value": { + "value": "@replace(last(array(split(pipeline().parameters.blobPath, '/'))), '.csv', '.parquet')\n\n\n\n", + "type": "Expression" + } + } + }, + { + "name": "Load Schema Mappings", + "type": "Lookup", + "dependsOn": [], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@toLower(pipeline().parameters.schemaFile)", + "type": "Expression" + }, + "folderPath": "[format('{0}/schemas', parameters('configContainerName'))]" + } + } + } + }, + { + "name": "Failed to Load Schema", + "type": "Fail", + "dependsOn": [ + { + "activity": "Load Schema Mappings", + "dependencyConditions": [ + "Failed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "message": { + "value": "@concat('Unable to load the ', pipeline().parameters.schemaFile, ' schema file. Please confirm the schema and version are supported for FinOps hubs ingestion. Unsupported files will remain in the msexports container.')", + "type": "Expression" + }, + "errorCode": "SchemaLoadFailed" + } + }, + { + "name": "Delete Target", + "type": "Delete", + "dependsOn": [ + { + "activity": "Set Destination File Name", + "dependencyConditions": [ + "Completed" + ] + }, + { + "activity": "Load Schema Mappings", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "dataset": { + "referenceName": "[variables('safeIngestionContainerName')]", + "type": "DatasetReference", + "parameters": { + "blobPath": { + "value": "@concat(pipeline().parameters.destinationFolder, '/', variables('destinationFile'))", + "type": "Expression" + } + } + }, + "enableLogging": false, + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + } + } + }, + { + "name": "Convert CSV", + "type": "Copy", + "dependsOn": [ + { + "activity": "Delete Target", + "dependencyConditions": [ + "Completed" + ] + } + ], + "policy": { + "timeout": "0.00:05:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "DelimitedTextSource", + "additionalColumns": { + "type": "Expression", + "value": "@activity('Load Schema Mappings').output.firstRow.additionalColumns" + }, + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "DelimitedTextReadSettings" + } + }, + "sink": { + "type": "ParquetSink", + "storeSettings": { + "type": "AzureBlobFSWriteSettings" + }, + "formatSettings": { + "type": "ParquetWriteSettings", + "fileExtension": ".parquet" + } + }, + "enableStaging": false, + "parallelCopies": 1, + "validateDataConsistency": false, + "translator": { + "value": "@activity('Load Schema Mappings').output.firstRow.translator", + "type": "Expression" + } + }, + "inputs": [ + { + "referenceName": "[variables('safeExportContainerName')]", + "type": "DatasetReference", + "parameters": { + "blobPath": { + "value": "@pipeline().parameters.blobPath", + "type": "Expression" + } + } + } + ], + "outputs": [ + { + "referenceName": "[variables('safeIngestionContainerName')]", + "type": "DatasetReference", + "parameters": { + "blobPath": { + "value": "@concat(pipeline().parameters.destinationFolder, '/', variables('destinationFile'))", + "type": "Expression" + } + } + } + ] + }, + { + "name": "Read Hub Config", + "type": "Lookup", + "dependsOn": [ + { + "activity": "Convert CSV", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": false, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": "settings.json", + "folderPath": "[parameters('configContainerName')]" + } + } + } + }, + { + "name": "If Retaining Exports", + "type": "IfCondition", + "dependsOn": [ + { + "activity": "Read Hub Config", + "dependencyConditions": [ + "Completed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "expression": { + "value": "@greater(coalesce(activity('Read Hub Config').output.firstRow.retention.msexports.days, 0), 0)", + "type": "Expression" + }, + "ifFalseActivities": [ + { + "name": "Delete CSV", + "type": "Delete", + "dependsOn": [], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "dataset": { + "referenceName": "[variables('safeExportContainerName')]", + "type": "DatasetReference", + "parameters": { + "blobPath": { + "value": "@pipeline().parameters.blobPath", + "type": "Expression" + } + } + }, + "enableLogging": false, + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + } + } + } + ] + } + } + ], + "parameters": { + "blobPath": { + "type": "String" + }, + "destinationFolder": { + "type": "string" + }, + "schemaFile": { + "type": "string" + } + }, + "variables": { + "destinationFile": { + "type": "String" + } + }, + "annotations": [] + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeIngestionContainerName'))]", + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeExportContainerName'))]" + ], + "metadata": { + "description": "Transforms CSV data to a standard schema and converts to Parquet." + } + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('{0}_startTriggers', parameters('dataFactoryName'))]", + "location": "[if(startsWith(parameters('location'), 'china'), 'chinaeast2', parameters('location'))]", + "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.Resources/deploymentScripts'), parameters('tagsByResource')['Microsoft.Resources/deploymentScripts'], createObject()))]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]": {} + } + }, + "kind": "AzurePowerShell", + "properties": { + "azPowerShellVersion": "8.0", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess", + "scriptContent": "[variables('$fxv#2')]", + "environmentVariables": [ + { + "name": "DataFactorySubscriptionId", + "value": "[subscription().id]" + }, + { + "name": "DataFactoryResourceGroup", + "value": "[resourceGroup().name]" + }, + { + "name": "DataFactoryName", + "value": "[parameters('dataFactoryName')]" + }, + { + "name": "Triggers", + "value": "[join(variables('allHubTriggers'), '|')]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]", + "identityRoleAssignments", + "[resourceId('Microsoft.DataFactory/factories/triggers', parameters('dataFactoryName'), variables('dailyTriggerName'))]", + "[resourceId('Microsoft.DataFactory/factories/triggers', parameters('dataFactoryName'), variables('fileAddedExportTriggerName'))]", + "[resourceId('Microsoft.DataFactory/factories/triggers', parameters('dataFactoryName'), variables('monthlyTriggerName'))]", + "[resourceId('Microsoft.DataFactory/factories/triggers', parameters('dataFactoryName'), variables('updateConfigTriggerName'))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "azuretimezones", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.24.24.22086", + "templateHash": "8239930466136045181" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location to use for the managed identity and deployment script to auto-start triggers. Default = (resource group location)." + } + }, + "timezoneobject": { + "type": "object", + "defaultValue": { + "australiaeast": "Australian Eastern Standard Time", + "australiasoutheast": "Australian Eastern Standard Time", + "brazilsouth": "Brasil Standard Time", + "canadacentral": "Central Standard Time", + "canadaeast": "Eastern Standard Time", + "centralindia": "India Standard Time", + "centralus": "Central Standard Time", + "eastasia": "China Standard Time", + "eastus": "Eastern Standard Time", + "eastus2": "Eastern Standard Time", + "francecentral": "Central European Time", + "germanynorth": "Central European Time", + "germanywestcentral": "Central European Time", + "japaneast": "Japan Standard Time", + "japanwest": "Japan Standard Time", + "koreacentral": "Korea Standard Time", + "koreasouth": "Korea Standard Time", + "northcentralus": "Central Standard Time", + "northeurope": "Central European Time", + "norwayeast": "Central European Time", + "norwaywest": "Central European Time", + "southcentralus": "Central Standard Time", + "southindia": "India Standard Time", + "southeastasia": "Singapore Standard Time", + "switzerlandnorth": "Central European Time", + "switzerlandwest": "Central European Time", + "uksouth": "Greenwich Mean Time", + "ukwest": "Greenwich Mean Time", + "westcentralus": "Central Standard Time", + "westeurope": "Central European Time", + "westindia": "India Standard Time", + "westus": "Pacific Standard Time", + "westus2": "Pacific Standard Time" + } + }, + "utchrs": { + "type": "string", + "defaultValue": "[utcNow('hh')]" + }, + "utcmins": { + "type": "string", + "defaultValue": "[utcNow('mm')]" + }, + "utcsecs": { + "type": "string", + "defaultValue": "[utcNow('ss')]" + } + }, + "variables": { + "loc": "[toLower(replace(parameters('location'), ' ', ''))]", + "timezone": "[coalesce(tryGet(parameters('timezoneobject'), variables('loc')), 'Universal Coordinated Time')]" + }, + "resources": [], + "outputs": { + "AzureRegion": { + "type": "string", + "value": "[parameters('location')]" + }, + "Timezone": { + "type": "string", + "value": "[variables('timezone')]" + }, + "UtcHours": { + "type": "string", + "value": "[parameters('utchrs')]" + }, + "UtcMinutes": { + "type": "string", + "value": "[parameters('utcmins')]" + }, + "UtcSeconds": { + "type": "string", + "value": "[parameters('utcsecs')]" + } + } + } + } + } + ], + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The Resource ID of the Data factory." + }, + "value": "[resourceId('Microsoft.DataFactory/factories', parameters('dataFactoryName'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The Name of the Azure Data Factory instance." + }, + "value": "[parameters('dataFactoryName')]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories', variables('dataFactoryName'))]", + "[resourceId('Microsoft.Resources/deployments', 'keyVault')]", + "[resourceId('Microsoft.Resources/deployments', 'storage')]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "keyVault", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "hubName": { + "value": "[parameters('hubName')]" + }, + "uniqueSuffix": { + "value": "[variables('uniqueSuffix')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('resourceTags')]" + }, + "tagsByResource": { + "value": "[parameters('tagsByResource')]" + }, + "storageAccountKey": { + "value": "[parameters('remoteHubStorageKey')]" + }, + "accessPolicies": { + "value": [ + { + "objectId": "[reference(resourceId('Microsoft.DataFactory/factories', variables('dataFactoryName')), '2018-06-01', 'full').identity.principalId]", + "tenantId": "[subscription().tenantId]", + "permissions": { + "secrets": [ + "get" + ] + } + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.24.24.22086", + "templateHash": "12905161325247490104" + } + }, + "parameters": { + "hubName": { + "type": "string", + "metadata": { + "description": "Required. Name of the hub. Used to ensure unique resource names." + } + }, + "uniqueSuffix": { + "type": "string", + "metadata": { + "description": "Required. Suffix to add to the KeyVault instance name to ensure uniqueness." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "accessPolicies": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. Array of access policies object." + } + }, + "storageAccountKey": { + "type": "securestring", + "metadata": { + "description": "Optional. Create and store a key for a remote storage account." + } + }, + "sku": { + "type": "string", + "defaultValue": "premium", + "allowedValues": [ + "premium", + "standard" + ], + "metadata": { + "description": "Optional. Specifies the SKU for the vault." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Resource tags." + } + }, + "tagsByResource": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags to apply to resources based on their resource type. Resource type specific tags will be merged with tags for all resources." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedAccessPolicies", + "count": "[length(parameters('accessPolicies'))]", + "input": { + "applicationId": "[if(contains(parameters('accessPolicies')[copyIndex('formattedAccessPolicies')], 'applicationId'), parameters('accessPolicies')[copyIndex('formattedAccessPolicies')].applicationId, '')]", + "objectId": "[if(contains(parameters('accessPolicies')[copyIndex('formattedAccessPolicies')], 'objectId'), parameters('accessPolicies')[copyIndex('formattedAccessPolicies')].objectId, '')]", + "permissions": "[parameters('accessPolicies')[copyIndex('formattedAccessPolicies')].permissions]", + "tenantId": "[if(contains(parameters('accessPolicies')[copyIndex('formattedAccessPolicies')], 'tenantId'), parameters('accessPolicies')[copyIndex('formattedAccessPolicies')].tenantId, tenant().tenantId)]" + } + } + ], + "keyVaultPrefix": "[format('{0}-vault', replace(parameters('hubName'), '_', '-'))]", + "keyVaultSuffix": "[format('-{0}', parameters('uniqueSuffix'))]", + "keyVaultName": "[replace(format('{0}{1}', take(variables('keyVaultPrefix'), sub(24, length(variables('keyVaultSuffix')))), variables('keyVaultSuffix')), '--', '-')]", + "keyVaultSecretName": "[format('{0}-storage-key', toLower(parameters('hubName')))]" + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2023-02-01", + "name": "[variables('keyVaultName')]", + "location": "[parameters('location')]", + "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.KeyVault/vaults'), parameters('tagsByResource')['Microsoft.KeyVault/vaults'], createObject()))]", + "properties": { + "enabledForDeployment": true, + "enabledForTemplateDeployment": true, + "enabledForDiskEncryption": true, + "enableSoftDelete": true, + "softDeleteRetentionInDays": 90, + "enableRbacAuthorization": false, + "createMode": "default", + "tenantId": "[subscription().tenantId]", + "accessPolicies": "[variables('formattedAccessPolicies')]", + "sku": { + "name": "[if(startsWith(parameters('location'), 'china'), 'standard', parameters('sku'))]", + "family": "A" + } + } + }, + { + "condition": "[not(empty(parameters('accessPolicies')))]", + "type": "Microsoft.KeyVault/vaults/accessPolicies", + "apiVersion": "2023-02-01", + "name": "[format('{0}/{1}', variables('keyVaultName'), 'add')]", + "properties": { + "accessPolicies": "[variables('formattedAccessPolicies')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" + ] + }, + { + "condition": "[not(empty(parameters('storageAccountKey')))]", + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-02-01", + "name": "[format('{0}/{1}', variables('keyVaultName'), variables('keyVaultSecretName'))]", + "properties": { + "attributes": { + "enabled": true, + "exp": 1702648632, + "nbf": 10000 + }, + "value": "[parameters('storageAccountKey')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" + ] + } + ], + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the key vault." + }, + "value": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the key vault." + }, + "value": "[variables('keyVaultName')]" + }, + "uri": { + "type": "string", + "metadata": { + "description": "The URI of the key vault." + }, + "value": "[reference(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), '2023-02-01').vaultUri]" + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories', variables('dataFactoryName'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the deployed hub instance." + }, + "value": "[parameters('hubName')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "Azure resource location resources were deployed to." + }, + "value": "[parameters('location')]" + }, + "dataFactorytName": { + "type": "string", + "metadata": { + "description": "Name of the Data Factory." + }, + "value": "[variables('dataFactoryName')]" + }, + "storageAccountId": { + "type": "string", + "metadata": { + "description": "Resource ID of the storage account created for the hub instance. This must be used when creating the Cost Management export." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.resourceId.value]" + }, + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Name of the storage account created for the hub instance. This must be used when connecting FinOps toolkit Power BI reports to your data." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.name.value]" + }, + "storageUrlForPowerBI": { + "type": "string", + "metadata": { + "description": "URL to use when connecting custom Power BI reports to your data." + }, + "value": "[format('https://{0}.dfs.{1}/{2}', reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.name.value, environment().suffixes.storage, reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.ingestionContainer.value)]" + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "Object ID of the Data Factory managed identity. This will be needed when configuring managed exports." + }, + "value": "[reference(resourceId('Microsoft.DataFactory/factories', variables('dataFactoryName')), '2018-06-01', 'full').identity.principalId]" + }, + "managedIdentityTenantId": { + "type": "string", + "metadata": { + "description": "Azure AD tenant ID. This will be needed when configuring managed exports." + }, + "value": "[tenant().tenantId]" + } + } + } + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the resource group." + }, + "value": "[parameters('hubName')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resources wer deployed to." + }, + "value": "[parameters('location')]" + }, + "dataFactorytName": { + "type": "string", + "metadata": { + "description": "Name of the Data Factory." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'hub'), '2022-09-01').outputs.dataFactorytName.value]" + }, + "storageAccountId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed storage account." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'hub'), '2022-09-01').outputs.storageAccountId.value]" + }, + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Name of the storage account created for the hub instance. This must be used when connecting FinOps toolkit Power BI reports to your data." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'hub'), '2022-09-01').outputs.storageAccountName.value]" + }, + "storageUrlForPowerBI": { + "type": "string", + "metadata": { + "description": "URL to use when connecting custom Power BI reports to your data." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'hub'), '2022-09-01').outputs.storageUrlForPowerBI.value]" + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "Object ID of the Data Factory managed identity. This will be needed when configuring managed exports." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'hub'), '2022-09-01').outputs.managedIdentityId.value]" + }, + "managedIdentityTenantId": { + "type": "string", + "metadata": { + "description": "Azure AD tenant ID. This will be needed when configuring managed exports." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'hub'), '2022-09-01').outputs.managedIdentityTenantId.value]" + } + } +} \ No newline at end of file diff --git a/docs/deploy/finops-hub-0.4.ui.json b/docs/deploy/finops-hub-0.4.ui.json new file mode 100644 index 000000000..b05043e8a --- /dev/null +++ b/docs/deploy/finops-hub-0.4.ui.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "basics": { + "description": "FinOps hubs are a reliable, trustworthy platform for cost analytics, insights, and optimization. Connect your hub to one or more billing accounts and subscriptions and build custom reports in Power BI or other tools. [Learn more](https://aka.ms/finops/hubs)", + "location": { + "label": "Location", + "resourceTypes": ["Microsoft.DataFactory/factories", "Microsoft.KeyVault/vaults", "Microsoft.Storage/storageAccounts"] + } + } + }, + "resourceTypes": [ + "Microsoft.DataFactory/factories", + "Microsoft.KeyVault/vaults", + "Microsoft.Storage/storageAccounts", + "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.Resources/deploymentScripts" + ], + "basics": [ + { + "name": "hubName", + "type": "Microsoft.Common.TextBox", + "label": "Name", + "defaultValue": "finops-hub", + "toolTip": "Name of the hub. Used to ensure unique resource names.", + "constraints": { + "required": true, + "regex": "^[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]$", + "validationMessage": "Name must be between 3 and 63 characters long and can contain only lowercase letters, numbers, and hyphens. The first and last characters in the name must be alphanumeric." + }, + "visible": true + }, + { + "name": "storageSku", + "type": "Microsoft.Common.DropDown", + "label": "Storage redundancy", + "defaultValue": "Locally-redundant (LRS) - Lowest cost", + "toolTip": "The data in your storage account is always replicated to ensure durability and high availability. Choose a replication strategy that matches your durability requirements. [Learn more](https://go.microsoft.com/fwlink/?linkid=2163103)", + "constraints": { + "required": false, + "allowedValues": [ + { + "label": "Locally-redundant (LRS) - Lowest cost", + "value": "Premium_LRS" + }, + { + "label": "Zone-redundant (ZRS) - High availability", + "value": "Premium_ZRS" + } + ] + }, + "visible": true + } + ], + "steps": [ + { + "name": "tags", + "label": "Tags", + "elements": [ + { + "name": "tagsByResource", + "label": "Tags", + "toolTip": "Tags to apply to resources.", + "type": "Microsoft.Common.TagsByResource", + "resources": [ + "Microsoft.DataFactory/factories", + "Microsoft.KeyVault/vaults", + "Microsoft.Storage/storageAccounts", + "Microsoft.ManagedIdentity/userAssignedIdentities", + "Microsoft.Resources/deploymentScripts" + ] + } + ] + } + ], + "outputs": { + "hubName": "[basics('hubName')]", + "location": "[location()]", + "storageSku": "[basics('storageSku')]", + "tagsByResource": "[steps('tags').tagsByResource]" + } + } +} diff --git a/docs/deploy/finops-hub-latest.json b/docs/deploy/finops-hub-latest.json index 6b8710078..3a2e1c2f2 100644 --- a/docs/deploy/finops-hub-latest.json +++ b/docs/deploy/finops-hub-latest.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.24.24.22086", - "templateHash": "8447317040603248989" + "templateHash": "4509128994982024537" } }, "parameters": { @@ -22,6 +22,13 @@ "description": "Optional. Azure location where all resources should be created. See https://aka.ms/azureregions. Default: Same as deployment." } }, + "fallbackEventGridLocation": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Azure location to use for a temporary Event Grid namespace to register the Microsoft.EventGrid resource provider if the primary location is not supported. The namespace will be deleted and is not used for hub operation. Default: \"\" (same as location)." + } + }, "storageSku": { "type": "string", "defaultValue": "Premium_LRS", @@ -47,11 +54,39 @@ "description": "Optional. Tags to apply to resources based on their resource type. Resource type specific tags will be merged with tags for all resources." } }, - "exportScopes": { + "scopesToMonitor": { "type": "array", "defaultValue": [], "metadata": { - "description": "Optional. List of scope IDs to create exports for." + "description": "Optional. List of scope IDs to monitor and ingest cost for." + } + }, + "exportRetentionInDays": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Number of days of cost data to retain in the ms-cm-exports container. Default: 0." + } + }, + "ingestionRetentionInMonths": { + "type": "int", + "defaultValue": 13, + "metadata": { + "description": "Optional. Number of months of cost data to retain in the ingestion container. Default: 13." + } + }, + "remoteHubStorageUri": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Storage account to push data to for ingestion into a remote hub." + } + }, + "remoteHubStorageKey": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Storage account key to use when pushing data to a remote hub." } } }, @@ -72,6 +107,9 @@ "location": { "value": "[parameters('location')]" }, + "fallbackEventGridLocation": { + "value": "[parameters('fallbackEventGridLocation')]" + }, "storageSku": { "value": "[parameters('storageSku')]" }, @@ -81,8 +119,20 @@ "tagsByResource": { "value": "[parameters('tagsByResource')]" }, - "exportScopes": { - "value": "[parameters('exportScopes')]" + "scopesToMonitor": { + "value": "[parameters('scopesToMonitor')]" + }, + "exportRetentionInDays": { + "value": "[parameters('exportRetentionInDays')]" + }, + "ingestionRetentionInMonths": { + "value": "[parameters('ingestionRetentionInMonths')]" + }, + "remoteHubStorageUri": { + "value": "[parameters('remoteHubStorageUri')]" + }, + "remoteHubStorageKey": { + "value": "[parameters('remoteHubStorageKey')]" } }, "template": { @@ -92,7 +142,7 @@ "_generator": { "name": "bicep", "version": "0.24.24.22086", - "templateHash": "15554300320953845972" + "templateHash": "10941542023228141808" } }, "parameters": { @@ -109,6 +159,13 @@ "description": "Optional. Azure location where all resources should be created. See https://aka.ms/azureregions. Default: (resource group location)." } }, + "fallbackEventGridLocation": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Azure location to use for a temporary Event Grid namespace to register the Microsoft.EventGrid resource provider if the primary location is not supported. The namespace will be deleted and is not used for hub operation. Default: \"\" (same as location)." + } + }, "storageSku": { "type": "string", "defaultValue": "Premium_LRS", @@ -134,17 +191,38 @@ "description": "Optional. Tags to apply to resources based on their resource type. Resource type specific tags will be merged with tags for all resources." } }, - "exportScopes": { + "scopesToMonitor": { "type": "array", "metadata": { - "description": "Optional. List of scope IDs to create exports for." + "description": "Optional. List of scope IDs to monitor and ingest cost for." } }, - "convertToParquet": { - "type": "bool", - "defaultValue": true, + "exportRetentionInDays": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Number of days of cost data to retain in the ms-cm-exports container. Default: 0." + } + }, + "ingestionRetentionInMonths": { + "type": "int", + "defaultValue": 13, + "metadata": { + "description": "Optional. Number of months of cost data to retain in the ingestion container. Default: 13." + } + }, + "remoteHubStorageUri": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Remote storage account for ingestion dataset." + } + }, + "remoteHubStorageKey": { + "type": "securestring", + "defaultValue": "", "metadata": { - "description": "Optional. Indicates whether ingested data should be converted to Parquet. Default: true." + "description": "Optional. Storage account key for remote storage account." } }, "enableDefaultTelemetry": { @@ -156,12 +234,52 @@ } }, "variables": { - "finOpsToolkitVersion": "0.3", + "finOpsToolkitVersion": "0.4", "resourceTags": "[union(parameters('tags'), createObject('cm-resource-parent', format('{0}/providers/Microsoft.Cloud/hubs/{1}', resourceGroup().id, parameters('hubName')), 'ftk-version', variables('finOpsToolkitVersion'), 'ftk-tool', 'FinOps hubs'))]", "uniqueSuffix": "[uniqueString(parameters('hubName'), resourceGroup().id)]", "dataFactoryPrefix": "[format('{0}-engine', replace(parameters('hubName'), '_', '-'))]", "dataFactorySuffix": "[format('-{0}', variables('uniqueSuffix'))]", "dataFactoryName": "[replace(format('{0}{1}', take(variables('dataFactoryPrefix'), sub(63, length(variables('dataFactorySuffix')))), variables('dataFactorySuffix')), '--', '-')]", + "eventGridPrefix": "[format('{0}-ns', replace(parameters('hubName'), '_', '-'))]", + "eventGridSuffix": "[format('-{0}', variables('uniqueSuffix'))]", + "eventGridName": "[replace(format('{0}{1}', take(variables('eventGridPrefix'), sub(50, length(variables('eventGridSuffix')))), variables('eventGridSuffix')), '--', '-')]", + "eventGridContributorRoleId": "1e241071-0855-49ea-94dc-649edcd759de", + "eventGridAllowedLocations": [ + "eastus2", + "westus3", + "northeurope", + "westeurope", + "southeastasia", + "eastasia", + "southcentralus", + "uaenorth", + "eastus", + "centralus", + "westus2", + "uksouth", + "italynorth", + "australiasoutheast", + "brazilsouth", + "ukwest", + "northcentralus", + "centralindia", + "japaneast", + "francecentral", + "canadacentral", + "australiaeast", + "japanwest", + "canadaeast", + "southindia", + "koreacentral", + "koreasouth", + "switzerlandnorth", + "germanywestcentral", + "norwayeast", + "swedencentral", + "polandcentral", + "israelcentral" + ], + "eventGridLocation": "[if(contains(variables('eventGridAllowedLocations'), parameters('location')), parameters('location'), if(contains(variables('eventGridAllowedLocations'), parameters('fallbackEventGridLocation')), parameters('fallbackEventGridLocation'), variables('eventGridAllowedLocations')[0]))]", "telemetryId": "00f120b5-2007-6120-0000-40b000000000" }, "resources": [ @@ -185,6 +303,71 @@ } } }, + { + "type": "Microsoft.EventGrid/namespaces", + "apiVersion": "2023-12-15-preview", + "name": "[variables('eventGridName')]", + "location": "[variables('eventGridLocation')]", + "sku": { + "capacity": 1, + "name": "Standard" + }, + "properties": { + "publicNetworkAccess": "Disabled" + } + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('{0}_cleanup', variables('uniqueSuffix'))]", + "location": "[variables('eventGridLocation')]" + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.EventGrid/namespaces/{0}', variables('eventGridName'))]", + "name": "[guid(variables('eventGridContributorRoleId'), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix'))))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('eventGridContributorRoleId'))]", + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix'))), '2023-01-31').principalId]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix')))]", + "[resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName'))]" + ] + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('{0}_deleteEventGrid', variables('uniqueSuffix'))]", + "location": "[parameters('location')]", + "kind": "AzurePowerShell", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix'))))]": {} + } + }, + "properties": { + "azPowerShellVersion": "8.0", + "scriptContent": "Remove-AzResource -Id $env:resourceId -Force", + "timeout": "PT30M", + "cleanupPreference": "OnSuccess", + "retentionInterval": "PT1H", + "environmentVariables": [ + { + "name": "resourceId", + "value": "[resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName'))]" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix')))]", + "[extensionResourceId(resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName')), 'Microsoft.Authorization/roleAssignments', guid(variables('eventGridContributorRoleId'), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_cleanup', variables('uniqueSuffix')))))]", + "[resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName'))]" + ] + }, { "type": "Microsoft.DataFactory/factories", "apiVersion": "2018-06-01", @@ -194,7 +377,14 @@ "identity": { "type": "SystemAssigned" }, - "properties": "[union(createObject(), createObject('globalConfigurations', createObject('PipelineBillingEnabled', 'true')))]" + "properties": { + "globalConfigurations": { + "PipelineBillingEnabled": "true" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.EventGrid/namespaces', variables('eventGridName'))]" + ] }, { "type": "Microsoft.Resources/deployments", @@ -224,8 +414,14 @@ "tagsByResource": { "value": "[parameters('tagsByResource')]" }, - "exportScopes": { - "value": "[parameters('exportScopes')]" + "scopesToMonitor": { + "value": "[parameters('scopesToMonitor')]" + }, + "msexportRetentionInDays": { + "value": "[parameters('exportRetentionInDays')]" + }, + "ingestionRetentionInMonths": { + "value": "[parameters('ingestionRetentionInMonths')]" } }, "template": { @@ -235,7 +431,7 @@ "_generator": { "name": "bicep", "version": "0.24.24.22086", - "templateHash": "10694508257002464346" + "templateHash": "10866138453633794931" } }, "parameters": { @@ -283,19 +479,39 @@ "description": "Optional. Tags to apply to resources based on their resource type. Resource type specific tags will be merged with tags for all resources." } }, - "exportScopes": { + "scopesToMonitor": { "type": "array", "metadata": { - "description": "Optional. List of scope IDs to create exports for." + "description": "Optional. List of scope IDs to monitor and ingest cost for." + } + }, + "msexportRetentionInDays": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Number of days of cost data to retain in the ms-cm-exports container. Default: 0." + } + }, + "ingestionRetentionInMonths": { + "type": "int", + "defaultValue": 13, + "metadata": { + "description": "Optional. Number of months of cost data to retain in the ingestion container. Default: 13." } } }, "variables": { - "$fxv#0": "0.3", - "$fxv#1": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\nWrite-Output \"Updating settings.json file...\"\r\nWrite-Output \" Storage account: $env:storageAccountName\"\r\nWrite-Output \" Container: $env:containerName\"\r\n\r\n$validateScopes = { $_.Length -gt 45 }\r\n\r\n# Initialize variables\r\n$fileName = 'settings.json'\r\n$filePath = Join-Path -Path . -ChildPath $fileName\r\n$newScopes = $env:exportScopes.Split('|') | Where-Object $validateScopes | ForEach-Object { @{ scope = $_ } }\r\n\r\n# Get storage context\r\n$storageContext = @{\r\n Context = New-AzStorageContext -StorageAccountName $env:storageAccountName -UseConnectedAccount\r\n Container = $env:containerName\r\n}\r\n\r\n# Download existing settings, if they exist\r\n$blob = Get-AzStorageBlobContent @storageContext -Blob $fileName -Destination $filePath -Force\r\nif ($blob) {\r\n Write-Output \"Existing settings.json file found. Updating...\"\r\n $text = Get-Content $filePath -Raw\r\n Write-Output \"---------\"\r\n Write-Output $text\r\n Write-Output \"---------\"\r\n $json = $text | ConvertFrom-Json\r\n\r\n # Rename exportScopes to scopes + convert to object array\r\n if ($json.exportScopes) {\r\n Write-Output \" Updating exportScopes...\"\r\n if ($json.exportScopes[0] -is [string]) {\r\n Write-Output \" Converting string array to object array...\"\r\n $json.exportScopes = $json.exportScopes | Where-Object $validateScopes | ForEach-Object { @{ scope = $_ } }\r\n if (-not ($json.exportScopes -is [array])) {\r\n Write-Output \" Converting single object to object array...\"\r\n $json.exportScopes = @($json.exportScopes)\r\n }\r\n }\r\n\r\n Write-Output \" Renaming to 'scopes'...\"\r\n $json | Add-Member -MemberType NoteProperty -Name scopes -Value $json.exportScopes\r\n $json.PSObject.Properties.Remove('exportScopes')\r\n }\r\n}\r\n\r\n# Set default if not found\r\nif (!$json) {\r\n Write-Output \"No existing settings.json file found. Creating new file...\"\r\n $json = [ordered]@{\r\n '$schema' = 'https://aka.ms/finops/hubs/settings-schema'\r\n type = 'HubInstance'\r\n version = ''\r\n learnMore = 'https://aka.ms/finops/hubs'\r\n scopes = @()\r\n }\r\n}\r\n\r\n# Updating settings\r\nWrite-Output \"Updating version to $env:ftkVersion...\"\r\n$json.version = $env:ftkVersion\r\nif ($newScopes) {\r\n Write-Output \"Merging $($newScopes.Count) scopes...\"\r\n $json.scopes = Compare-Object -ReferenceObject $json.scopes -DifferenceObject $newScopes -Property scope -PassThru -IncludeEqual\r\n\r\n # Remove the SideIndicator property from the Compare-Object output\r\n $json.scopes | ForEach-Object { $_.PSObject.Properties.Remove('SideIndicator') } | ConvertTo-Json\r\n\r\n if (-not ($json.scopes -is [array])) {\r\n $json.scopes = @($json.scopes)\r\n }\r\n Write-Output \"$($json.scopes.Count) scopes found.\"\r\n}\r\n$text = $json | ConvertTo-Json\r\nWrite-Output \"---------\"\r\nWrite-Output $text\r\nWrite-Output \"---------\"\r\n$text | Out-File $filePath\r\n\r\n# Upload new/updated settings\r\nWrite-Output \"Uploading settings.json file...\"\r\nSet-AzStorageBlobContent @storageContext -File $filePath -Force\r\n", + "$fxv#0": "{\r\n \"additionalColumns\": [],\r\n \"translator\": {\r\n \"type\": \"TabularTranslator\",\r\n \"mappings\": [\r\n {\r\n \"source\": { \"name\": \"BilledCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"BilledCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingCurrency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingCurrency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingPeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"BillingPeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingPeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"BillingPeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeClass\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeClass\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeFrequency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeFrequency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargePeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"ChargePeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargePeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"ChargePeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountStatus\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountStatus\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ConsumedQuantity\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ConsumedQuantity\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ConsumedUnit\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ConsumedUnit\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ContractedCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ContractedCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ContractedUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ContractedUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"EffectiveCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"EffectiveCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"InvoiceIssuerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"InvoiceIssuerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ListCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ListCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ListUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ListUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PricingCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingQuantity\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"PricingQuantity\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingUnit\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PricingUnit\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ProviderName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ProviderName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PublisherName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PublisherName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"RegionId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"RegionId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"RegionName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"RegionName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ServiceCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ServiceCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ServiceName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ServiceName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SkuId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SkuId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SkuPriceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SkuPriceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"Tags\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"Tags\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountOwnerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountOwnerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BilledCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BilledCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BilledUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BilledUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingExchangeRate\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BillingExchangeRate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingExchangeRateDate\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_BillingExchangeRateDate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingProfileId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingProfileId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingProfileName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingProfileName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ContractedCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_ContractedCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CostAllocationRuleName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CostAllocationRuleName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CostCenter\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CostCenter\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CustomerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CustomerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CustomerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CustomerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_EffectiveCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_EffectiveCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_EffectiveUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_EffectiveUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceIssuerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceIssuerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceSectionId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceSectionId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceSectionName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceSectionName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ListCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_ListCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PartnerCreditApplied\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PartnerCreditApplied\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PartnerCreditRate\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PartnerCreditRate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingBlockSize\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_PricingBlockSize\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingCurrency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingCurrency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingUnitDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingUnitDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PublisherCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PublisherCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PublisherId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PublisherId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResellerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResellerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResellerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResellerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResourceGroupName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResourceGroupName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResourceType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResourceType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ServicePeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_ServicePeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ServicePeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_ServicePeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuDetails\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuDetails\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuIsCreditEligible\", \"type\": \"Boolean\" },\r\n \"sink\": { \"name\": \"x_SkuIsCreditEligible\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOfferId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOfferId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOrderId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOrderId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOrderName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOrderName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuPartNumber\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuPartNumber\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuRegion\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuRegion\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuServiceFamily\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuServiceFamily\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuTerm\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuTerm\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuTier\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuTier\" }\r\n }\r\n ]\r\n }\r\n}\r\n", + "$fxv#1": "{\r\n \"additionalColumns\": [],\r\n \"translator\": {\r\n \"type\": \"TabularTranslator\",\r\n \"mappings\": [\r\n {\r\n \"source\": { \"name\": \"AvailabilityZone\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"AvailabilityZone\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BilledCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"BilledCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingAccountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingAccountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingCurrency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"BillingCurrency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingPeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"BillingPeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"BillingPeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"BillingPeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeFrequency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeFrequency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargePeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"ChargePeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargePeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"ChargePeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ChargeSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ChargeSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"CommitmentDiscountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"CommitmentDiscountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"EffectiveCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"EffectiveCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"InvoiceIssuerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"InvoiceIssuerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ListCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ListCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ListUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"ListUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PricingCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingQuantity\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"PricingQuantity\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PricingUnit\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PricingUnit\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ProviderName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ProviderName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"PublisherName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"PublisherName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"Region\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"Region\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ResourceType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ResourceType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ServiceCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ServiceCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"ServiceName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"ServiceName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SkuId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SkuId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SkuPriceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SkuPriceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"SubAccountType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"SubAccountType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"Tags\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"Tags\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"UsageQuantity\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"UsageQuantity\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"UsageUnit\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"UsageUnit\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_AccountOwnerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_AccountOwnerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BilledCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BilledCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BilledUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BilledUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingAccountId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingAccountId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingAccountName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingAccountName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingExchangeRate\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_BillingExchangeRate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingExchangeRateDate\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_BillingExchangeRateDate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingProfileId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingProfileId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_BillingProfileName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_BillingProfileName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ChargeId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ChargeId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CostAllocationRuleName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CostAllocationRuleName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CostCenter\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CostCenter\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CustomerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CustomerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_CustomerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_CustomerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_EffectiveCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_EffectiveCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_EffectiveUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_EffectiveUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceIssuerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceIssuerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceSectionId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceSectionId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_InvoiceSectionName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_InvoiceSectionName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_OnDemandCost\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_OnDemandCost\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_OnDemandCostInUsd\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_OnDemandCostInUsd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_OnDemandUnitPrice\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_OnDemandUnitPrice\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PartnerCreditApplied\", \"type\": \"Boolean\" },\r\n \"sink\": { \"name\": \"x_PartnerCreditApplied\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PartnerCreditRate\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_PartnerCreditRate\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingBlockSize\", \"type\": \"Decimal\" },\r\n \"sink\": { \"name\": \"x_PricingBlockSize\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingCurrency\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingCurrency\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PricingUnitDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PricingUnitDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PublisherCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PublisherCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_PublisherId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_PublisherId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResellerId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResellerId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResellerName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResellerName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResourceGroupName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResourceGroupName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ResourceType\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_ResourceType\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ServicePeriodEnd\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_ServicePeriodEnd\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_ServicePeriodStart\", \"type\": \"DateTimeOffset\" },\r\n \"sink\": { \"name\": \"x_ServicePeriodStart\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuDescription\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuDescription\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuDetails\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuDetails\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuIsCreditEligible\", \"type\": \"Boolean\" },\r\n \"sink\": { \"name\": \"x_SkuIsCreditEligible\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterCategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterCategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuMeterSubcategory\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuMeterSubcategory\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOfferId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOfferId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOrderId\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOrderId\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuOrderName\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuOrderName\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuPartNumber\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuPartNumber\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuRegion\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuRegion\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuServiceFamily\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuServiceFamily\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuTerm\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuTerm\" }\r\n },\r\n {\r\n \"source\": { \"name\": \"x_SkuTier\", \"type\": \"String\" },\r\n \"sink\": { \"name\": \"x_SkuTier\" }\r\n }\r\n ]\r\n }\r\n}\r\n", + "$fxv#2": "0.4", + "$fxv#3": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\nWrite-Output \"Updating settings.json file...\"\r\nWrite-Output \" Storage account: $env:storageAccountName\"\r\nWrite-Output \" Container: $env:containerName\"\r\n\r\n$validateScopes = { $_.Length -gt 45 }\r\n\r\n# Initialize variables\r\n$fileName = 'settings.json'\r\n$filePath = Join-Path -Path . -ChildPath $fileName\r\n$newScopes = $env:scopes.Split('|') | Where-Object $validateScopes | ForEach-Object { @{ scope = $_ } }\r\n\r\n# Get storage context\r\n$storageContext = @{\r\n Context = New-AzStorageContext -StorageAccountName $env:storageAccountName -UseConnectedAccount\r\n Container = $env:containerName\r\n}\r\n\r\n# Download existing settings, if they exist\r\n$blob = Get-AzStorageBlobContent @storageContext -Blob $fileName -Destination $filePath -Force\r\nif ($blob)\r\n{\r\n \r\n $text = Get-Content $filePath -Raw\r\n Write-Output \"---------\"\r\n Write-Output $text\r\n Write-Output \"---------\"\r\n $json = $text | ConvertFrom-Json\r\n Write-Output \"Existing settings.json file found. Updating...\"\r\n # Rename exportScopes to scopes + convert to object array\r\n if ($json.exportScopes)\r\n {\r\n Write-Output \" Updating exportScopes...\"\r\n if ($json.exportScopes[0] -is [string])\r\n {\r\n Write-Output \" Converting string array to object array...\"\r\n $json.exportScopes = $json.exportScopes | Where-Object $validateScopes | ForEach-Object { @{ scope = $_ } }\r\n if (-not ($json.exportScopes -is [array]))\r\n {\r\n Write-Output \" Converting single object to object array...\"\r\n $json.exportScopes = @($json.exportScopes)\r\n }\r\n }\r\n\r\n Write-Output \" Renaming to 'scopes'...\"\r\n $json | Add-Member -MemberType NoteProperty -Name scopes -Value $json.exportScopes\r\n $json.PSObject.Properties.Remove('exportScopes')\r\n }\r\n}\r\n\r\n# Set default if not found\r\nif (!$json)\r\n{\r\n Write-Output \"No existing settings.json file found. Creating new file...\"\r\n $json = [ordered]@{\r\n '$schema' = 'https://aka.ms/finops/hubs/settings-schema'\r\n type = 'HubInstance'\r\n version = ''\r\n learnMore = 'https://aka.ms/finops/hubs'\r\n scopes = @()\r\n retention = @{\r\n 'msexports' = @{\r\n days = 0\r\n }\r\n 'ingestion' = @{\r\n months = 13\r\n }\r\n }\r\n }\r\n\r\n $text = $json | ConvertTo-Json\r\n Write-Output \"---------\"\r\n Write-Output $text\r\n Write-Output \"---------\"\r\n}\r\n\r\n# Set values from inputs\r\n$json.scopes = $env:scopes.Split('|') | ForEach-Object { @{ 'scope' = $_ } }\r\n$json.retention.msexports.days = [Int32]::Parse($env:msexportRetentionInDays)\r\n$json.retention.ingestion.months = [Int32]::Parse($env:ingestionRetentionInMonths)\r\n\r\n# Updating settings\r\nWrite-Output \"Updating version to $env:ftkVersion...\"\r\n$json.version = $env:ftkVersion\r\nif ($newScopes)\r\n{\r\n Write-Output \"Merging $($newScopes.Count) scopes...\"\r\n $json.scopes = Compare-Object -ReferenceObject $json.scopes -DifferenceObject $newScopes -Property scope -PassThru -IncludeEqual\r\n\r\n # Remove the SideIndicator property from the Compare-Object output\r\n $json.scopes | ForEach-Object { $_.PSObject.Properties.Remove('SideIndicator') } | ConvertTo-Json\r\n\r\n if (-not ($json.scopes -is [array]))\r\n {\r\n $json.scopes = @($json.scopes)\r\n }\r\n Write-Output \"$($json.scopes.Count) scopes found.\"\r\n}\r\n$text = $json | ConvertTo-Json\r\nWrite-Output \"---------\"\r\nWrite-Output $text\r\nWrite-Output \"---------\"\r\n$text | Out-File $filePath\r\n\r\n# Upload new/updated settings\r\nWrite-Output \"Uploading settings.json file...\"\r\nSet-AzStorageBlobContent @storageContext -File $filePath -Force | Out-Null\r\n\r\n# Save focusSchemaFile file to storage\r\n$schemaFiles = $env:schemaFiles | ConvertFrom-Json -Depth 10\r\nWrite-Output \"Uploading ${$schemaFiles.PSObject.Properties.Count} schema files...\"\r\n$schemaFiles.PSObject.Properties | ForEach-Object {\r\n $fileName = \"$($_.Name).json\"\r\n $tempPath = \"./$fileName\"\r\n Write-Output \" Uploading $($_.Name).json...\"\r\n $_.Value | Out-File $tempPath\r\n Set-AzStorageBlobContent @storageContext -File $tempPath -Blob \"schemas/$fileName\" -Force | Out-Null\r\n}\r\n", "safeHubName": "[replace(replace(toLower(parameters('hubName')), '-', ''), '_', '')]", "storageAccountSuffix": "[parameters('uniqueSuffix')]", "storageAccountName": "[format('{0}{1}', take(variables('safeHubName'), sub(24, length(variables('storageAccountSuffix')))), variables('storageAccountSuffix'))]", + "schemaFiles": { + "focuscost_1.0": "[variables('$fxv#0')]", + "focuscost_1.0-preview(v1)": "[variables('$fxv#1')]" + }, "blobUploadRbacRoles": [ "ba92f5b4-2d11-453d-a403-e96b0029c9fe", "e40ec5ca-96e0-45a2-b4ff-59039f2c2b59" @@ -304,7 +520,7 @@ "resources": [ { "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2021-08-01", + "apiVersion": "2022-09-01", "name": "[variables('storageAccountName')]", "location": "[parameters('location')]", "sku": { @@ -321,7 +537,7 @@ }, { "type": "Microsoft.Storage/storageAccounts/blobServices", - "apiVersion": "2021-06-01", + "apiVersion": "2022-09-01", "name": "[format('{0}/{1}', variables('storageAccountName'), 'default')]", "dependsOn": [ "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" @@ -329,7 +545,7 @@ }, { "type": "Microsoft.Storage/storageAccounts/blobServices/containers", - "apiVersion": "2021-06-01", + "apiVersion": "2022-09-01", "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', 'config')]", "properties": { "publicAccess": "None", @@ -341,7 +557,7 @@ }, { "type": "Microsoft.Storage/storageAccounts/blobServices/containers", - "apiVersion": "2021-06-01", + "apiVersion": "2022-09-01", "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', 'msexports')]", "properties": { "publicAccess": "None", @@ -353,7 +569,7 @@ }, { "type": "Microsoft.Storage/storageAccounts/blobServices/containers", - "apiVersion": "2021-06-01", + "apiVersion": "2022-09-01", "name": "[format('{0}/{1}/{2}', variables('storageAccountName'), 'default', 'ingestion')]", "properties": { "publicAccess": "None", @@ -391,7 +607,7 @@ { "type": "Microsoft.Resources/deploymentScripts", "apiVersion": "2020-10-01", - "name": "uploadSettings", + "name": "[format('{0}_uploadSettings', variables('storageAccountName'))]", "kind": "AzurePowerShell", "location": "[if(startsWith(parameters('location'), 'china'), 'chinaeast2', parameters('location'))]", "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.Resources/deploymentScripts'), parameters('tagsByResource')['Microsoft.Resources/deploymentScripts'], createObject()))]", @@ -407,11 +623,19 @@ "environmentVariables": [ { "name": "ftkVersion", - "value": "[variables('$fxv#0')]" + "value": "[variables('$fxv#2')]" + }, + { + "name": "scopes", + "value": "[join(parameters('scopesToMonitor'), '|')]" + }, + { + "name": "msexportRetentionInDays", + "value": "[string(parameters('msexportRetentionInDays'))]" }, { - "name": "exportScopes", - "value": "[join(parameters('exportScopes'), '|')]" + "name": "ingestionRetentionInMonths", + "value": "[string(parameters('ingestionRetentionInMonths'))]" }, { "name": "storageAccountName", @@ -420,9 +644,13 @@ { "name": "containerName", "value": "config" + }, + { + "name": "schemaFiles", + "value": "[string(variables('schemaFiles'))]" } ], - "scriptContent": "[variables('$fxv#1')]" + "scriptContent": "[variables('$fxv#3')]" }, "dependsOn": [ "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', variables('storageAccountName'), 'default', 'config')]", @@ -484,24 +712,30 @@ "dataFactoryName": { "value": "[variables('dataFactoryName')]" }, - "convertToParquet": { - "value": "[parameters('convertToParquet')]" - }, - "keyVaultName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'keyVault'), '2022-09-01').outputs.name.value]" - }, "storageAccountName": { "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.name.value]" }, "exportContainerName": { "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.exportContainer.value]" }, + "configContainerName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.configContainer.value]" + }, "ingestionContainerName": { "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.ingestionContainer.value]" }, + "keyVaultName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'keyVault'), '2022-09-01').outputs.name.value]" + }, "location": { "value": "[parameters('location')]" }, + "hubName": { + "value": "[parameters('hubName')]" + }, + "remoteHubStorageUri": { + "value": "[parameters('remoteHubStorageUri')]" + }, "tags": { "value": "[variables('resourceTags')]" }, @@ -516,14 +750,47 @@ "_generator": { "name": "bicep", "version": "0.24.24.22086", - "templateHash": "5738194981634133446" + "templateHash": "2392183082773057596" } }, + "functions": [ + { + "namespace": "__bicep", + "members": { + "getExportBody": { + "parameters": [ + { + "type": "string", + "name": "exportContainerName" + }, + { + "type": "string", + "name": "focusSchemaVersion" + }, + { + "type": "bool", + "name": "isMonthly" + } + ], + "output": { + "type": "string", + "value": "[format('{{ \"properties\": {{ \"definition\": {{ \"dataSet\": {{ \"configuration\": {{ \"dataVersion\": \"{0}\", \"filters\": [] }}, \"granularity\": \"Daily\" }}, \"timeframe\": \"{1}\", \"type\": \"FocusCost\" }}, \"deliveryInfo\": {{ \"destination\": {{ \"container\": \"{2}\", \"rootFolderPath\": \"@{{item().scope}}\", \"type\": \"AzureBlob\", \"resourceId\": \"@{{variables(''storageAccountId'')}}\" }} }}, \"schedule\": {{ \"recurrence\": \"{3}\", \"recurrencePeriod\": {{ \"from\": \"2024-01-01T00:00:00.000Z\", \"to\": \"2050-02-01T00:00:00.000Z\" }}, \"status\": \"Inactive\" }}, \"format\": \"Csv\", \"partitionData\": true, \"dataOverwriteBehavior\": \"CreateNewReport\", \"compressionMode\": \"None\" }}, \"id\": \"@{{variables(''resourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports/@{{variables(''exportName'')}}\", \"name\": \"@{{variables(''exportName'')}}\", \"type\": \"Microsoft.CostManagement/reports\", \"identity\": {{ \"type\": \"systemAssigned\" }}, \"location\": \"global\" }}', parameters('focusSchemaVersion'), if(parameters('isMonthly'), 'MonthToDate', 'TheLastMonth'), parameters('exportContainerName'), if(parameters('isMonthly'), 'Monthly', 'Daily'))]" + } + } + } + } + ], "parameters": { + "hubName": { + "type": "string", + "metadata": { + "description": "Required. Name of the FinOps hub instance." + } + }, "dataFactoryName": { "type": "string", "metadata": { - "description": "Optional. Name of the hub. Used to ensure unique resource names. Default: \"finops-hub\"." + "description": "Required. Name of the Data Factory instance." } }, "keyVaultName": { @@ -550,11 +817,10 @@ "description": "Required. The name of the container where normalized data is ingested." } }, - "convertToParquet": { - "type": "bool", - "defaultValue": true, + "configContainerName": { + "type": "string", "metadata": { - "description": "Optional. Indicates whether ingested data should be converted to Parquet. Default: true." + "description": "Required. The name of the container where normalized data is ingested." } }, "location": { @@ -564,6 +830,12 @@ "description": "Optional. The location to use for the managed identity and deployment script to auto-start triggers. Default = (resource group location)." } }, + "remoteHubStorageUri": { + "type": "string", + "metadata": { + "description": "Optional. Remote storage account for ingestion dataset." + } + }, "tags": { "type": "object", "defaultValue": {}, @@ -580,32 +852,13 @@ } }, "variables": { - "copy": [ - { - "name": "focusCostMappings", - "count": "[length(range(0, length(variables('focusCostColumns'))))]", - "input": { - "source": { - "name": "[variables('focusCostColumns')[range(0, length(variables('focusCostColumns')))[copyIndex('focusCostMappings')]].name]", - "type": "[variables('focusCostColumns')[range(0, length(variables('focusCostColumns')))[copyIndex('focusCostMappings')]].type]" - }, - "sink": { - "name": "[variables('focusCostColumns')[range(0, length(variables('focusCostColumns')))[copyIndex('focusCostMappings')]].name]" - } - } - } - ], - "$fxv#0": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\n# Init outputs\r\n$DeploymentScriptOutputs = @{}\r\n\r\n# \r\n$adfParams = @{\r\n ResourceGroupName = $env:DataFactoryResourceGroup\r\n DataFactoryName = $env:DataFactoryName\r\n}\r\n\r\n# Delete old triggers\r\n$triggers = Get-AzDataFactoryV2Trigger @adfParams -ErrorAction SilentlyContinue `\r\n| Where-Object { $_.Name -match '^msexports?$' }\r\n$DeploymentScriptOutputs[\"stopTriggers\"] = $triggers | Stop-AzDataFactoryV2Trigger -Force -ErrorAction SilentlyContinue\r\n$DeploymentScriptOutputs[\"deleteTriggers\"] = $triggers | Remove-AzDataFactoryV2Trigger -Force -ErrorAction SilentlyContinue\r\n\r\n# Delete old pipelines\r\n$DeploymentScriptOutputs[\"pipelines\"] = Get-AzDataFactoryV2Pipeline @adfParams -ErrorAction SilentlyContinue `\r\n| Where-Object { $_.Name -match '^msexports_(extract|transform)$' } `\r\n| Remove-AzDataFactoryV2Pipeline -Force -ErrorAction SilentlyContinue", - "$fxv#1": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\nParam(\r\n [switch] $Stop\r\n)\r\n\r\n# Init outputs\r\n$DeploymentScriptOutputs = @{}\r\n\r\nif (-not $Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n\r\n# Loop thru triggers\r\n$env:Triggers.Split('|') `\r\n| ForEach-Object {\r\n $trigger = $_\r\n if ($Stop) {\r\n Write-Host \"Stopping trigger $trigger...\" -NoNewline\r\n $triggerOutput = Stop-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force `\r\n -ErrorAction SilentlyContinue # Ignore errors, since the trigger may not exist\r\n } else {\r\n Write-Host \"Starting trigger $trigger...\" -NoNewline\r\n $triggerOutput = Start-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force\r\n }\r\n if ($triggerOutput) {\r\n Write-Host 'done'\r\n } else {\r\n Write-Host 'failed'\r\n }\r\n $DeploymentScriptOutputs[$trigger] = $triggerOutput\r\n}\r\n\r\nif ($Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n", - "$fxv#2": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\nParam(\r\n [switch] $Stop\r\n)\r\n\r\n# Init outputs\r\n$DeploymentScriptOutputs = @{}\r\n\r\nif (-not $Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n\r\n# Loop thru triggers\r\n$env:Triggers.Split('|') `\r\n| ForEach-Object {\r\n $trigger = $_\r\n if ($Stop) {\r\n Write-Host \"Stopping trigger $trigger...\" -NoNewline\r\n $triggerOutput = Stop-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force `\r\n -ErrorAction SilentlyContinue # Ignore errors, since the trigger may not exist\r\n } else {\r\n Write-Host \"Starting trigger $trigger...\" -NoNewline\r\n $triggerOutput = Start-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force\r\n }\r\n if ($triggerOutput) {\r\n Write-Host 'done'\r\n } else {\r\n Write-Host 'failed'\r\n }\r\n $DeploymentScriptOutputs[$trigger] = $triggerOutput\r\n}\r\n\r\nif ($Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n", - "datasetPropsDelimitedText": { - "columnDelimiter": ",", - "compressionLevel": "Optimal", - "escapeChar": "\"", - "firstRowAsHeader": true, - "quoteChar": "\"" - }, - "datasetPropsCommon": { + "$fxv#0": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\n# Init outputs\r\n$DeploymentScriptOutputs = @{}\r\n\r\n# \r\n$adfParams = @{\r\n ResourceGroupName = $env:DataFactoryResourceGroup\r\n DataFactoryName = $env:DataFactoryName\r\n}\r\n\r\n# Delete old triggers\r\n$triggers = Get-AzDataFactoryV2Trigger @adfParams -ErrorAction SilentlyContinue `\r\n| Where-Object { $_.Name -match '^msexports(_(setup|daily|monthly|extract))?$' }\r\n$DeploymentScriptOutputs[\"stopTriggers\"] = $triggers | Stop-AzDataFactoryV2Trigger -Force -ErrorAction SilentlyContinue\r\n$DeploymentScriptOutputs[\"deleteTriggers\"] = $triggers | Remove-AzDataFactoryV2Trigger -Force -ErrorAction SilentlyContinue\r\n\r\n# Delete old pipelines\r\n$DeploymentScriptOutputs[\"pipelines\"] = Get-AzDataFactoryV2Pipeline @adfParams -ErrorAction SilentlyContinue `\r\n| Where-Object { $_.Name -match '^msexports_(backfill|extract|fill|get|run|setup|transform)$' } `\r\n| Remove-AzDataFactoryV2Pipeline -Force -ErrorAction SilentlyContinue\r\n", + "$fxv#1": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\nParam(\r\n [switch] $Stop\r\n)\r\n\r\n# Init outputs\r\n$DeploymentScriptOutputs = @{}\r\n\r\nif (-not $Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n\r\n# Loop thru triggers\r\n$env:Triggers.Split('|') `\r\n| ForEach-Object {\r\n $trigger = $_\r\n if ($Stop) {\r\n Write-Output \"Stopping trigger $trigger...\"\r\n $triggerOutput = Stop-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force `\r\n -ErrorAction SilentlyContinue # Ignore errors, since the trigger may not exist\r\n } else {\r\n Write-Output \"Starting trigger $trigger...\"\r\n $triggerOutput = Start-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force\r\n }\r\n if ($triggerOutput) {\r\n Write-Output \"done...\"\r\n } else {\r\n Write-Output \"failed...\"\r\n }\r\n $DeploymentScriptOutputs[$trigger] = $triggerOutput\r\n}\r\n\r\nif ($Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n", + "$fxv#2": "# Copyright (c) Microsoft Corporation.\r\n# Licensed under the MIT License.\r\n\r\nParam(\r\n [switch] $Stop\r\n)\r\n\r\n# Init outputs\r\n$DeploymentScriptOutputs = @{}\r\n\r\nif (-not $Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n\r\n# Loop thru triggers\r\n$env:Triggers.Split('|') `\r\n| ForEach-Object {\r\n $trigger = $_\r\n if ($Stop) {\r\n Write-Output \"Stopping trigger $trigger...\"\r\n $triggerOutput = Stop-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force `\r\n -ErrorAction SilentlyContinue # Ignore errors, since the trigger may not exist\r\n } else {\r\n Write-Output \"Starting trigger $trigger...\"\r\n $triggerOutput = Start-AzDataFactoryV2Trigger `\r\n -ResourceGroupName $env:DataFactoryResourceGroup `\r\n -DataFactoryName $env:DataFactoryName `\r\n -Name $trigger `\r\n -Force\r\n }\r\n if ($triggerOutput) {\r\n Write-Output \"done...\"\r\n } else {\r\n Write-Output \"failed...\"\r\n }\r\n $DeploymentScriptOutputs[$trigger] = $triggerOutput\r\n}\r\n\r\nif ($Stop) {\r\n Start-Sleep -Seconds 10\r\n}\r\n", + "focusSchemaVersion": "1.0", + "ftkVersion": "0.4", + "exportApiVersion": "2023-07-01-preview", + "datasetPropsDefault": { "location": { "type": "AzureBlobFSLocation", "fileName": { @@ -613,680 +866,1378 @@ "type": "Expression" }, "folderPath": { - "value": "@{dataset().folderName}", + "value": "@{dataset().folderPath}", "type": "Expression" } } }, "safeExportContainerName": "[replace(format('{0}', parameters('exportContainerName')), '-', '_')]", "safeIngestionContainerName": "[replace(format('{0}', parameters('ingestionContainerName')), '-', '_')]", - "exportFileAddedTriggerName": "[format('{0}_FileAdded', variables('safeExportContainerName'))]", + "safeConfigContainerName": "[replace(format('{0}', parameters('configContainerName')), '-', '_')]", + "fileAddedExportTriggerName": "[format('{0}_FileAdded', variables('safeExportContainerName'))]", + "updateConfigTriggerName": "[format('{0}_SettingsUpdated', variables('safeConfigContainerName'))]", + "dailyTriggerName": "[format('{0}_DailySchedule', variables('safeConfigContainerName'))]", + "monthlyTriggerName": "[format('{0}_MonthlySchedule', variables('safeConfigContainerName'))]", "allHubTriggers": [ - "[variables('exportFileAddedTriggerName')]" + "[variables('fileAddedExportTriggerName')]", + "[variables('updateConfigTriggerName')]", + "[variables('dailyTriggerName')]", + "[variables('monthlyTriggerName')]" ], "autoStartRbacRoles": [ "673868aa-7521-48a0-acc6-0f60742d39f5", "e40ec5ca-96e0-45a2-b4ff-59039f2c2b59" ], - "focusCostColumns": [ - { - "name": "AvailabilityZone", - "type": "String" - }, - { - "name": "BilledCost", - "type": "Decimal" - }, - { - "name": "BillingAccountId", - "type": "String" - }, - { - "name": "BillingAccountName", - "type": "String" - }, - { - "name": "BillingAccountType", - "type": "String" - }, - { - "name": "BillingCurrency", - "type": "String" - }, - { - "name": "BillingPeriodEnd", - "type": "DateTime" - }, - { - "name": "BillingPeriodStart", - "type": "DateTime" - }, - { - "name": "ChargeCategory", - "type": "String" - }, - { - "name": "ChargeDescription", - "type": "String" - }, - { - "name": "ChargeFrequency", - "type": "String" + "storageRbacRoles": [ + "17d1049b-9a84-46fb-8f53-869881c3d3ab", + "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "acdd72a7-3385-48ef-bd42-f606fba81ae7", + "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9" + ] + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('{0}_triggerManager', parameters('dataFactoryName'))]", + "location": "[parameters('location')]", + "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.ManagedIdentity/userAssignedIdentities'), parameters('tagsByResource')['Microsoft.ManagedIdentity/userAssignedIdentities'], createObject()))]" + }, + { + "copy": { + "name": "identityRoleAssignments", + "count": "[length(variables('autoStartRbacRoles'))]" }, - { - "name": "ChargePeriodEnd", - "type": "DateTime" + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.DataFactory/factories/{0}', parameters('dataFactoryName'))]", + "name": "[guid(resourceId('Microsoft.DataFactory/factories', parameters('dataFactoryName')), variables('autoStartRbacRoles')[copyIndex()], resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('autoStartRbacRoles')[copyIndex()])]", + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))), '2023-01-31').principalId]", + "principalType": "ServicePrincipal" }, - { - "name": "ChargePeriodStart", - "type": "DateTime" + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]" + ] + }, + { + "copy": { + "name": "pipelineIdentityRoleAssignments", + "count": "[length(variables('storageRbacRoles'))]" }, - { - "name": "ChargeSubcategory", - "type": "String" + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), variables('storageRbacRoles')[copyIndex()], resourceId('Microsoft.DataFactory/factories', parameters('dataFactoryName')))]", + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('storageRbacRoles')[copyIndex()])]", + "principalId": "[reference(resourceId('Microsoft.DataFactory/factories', parameters('dataFactoryName')), '2018-06-01', 'full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('{0}_deleteOldResources', parameters('dataFactoryName'))]", + "location": "[if(startsWith(parameters('location'), 'china'), 'chinaeast2', parameters('location'))]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]": {} + } }, - { - "name": "CommitmentDiscountCategory", - "type": "String" + "kind": "AzurePowerShell", + "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.Resources/deploymentScripts'), parameters('tagsByResource')['Microsoft.Resources/deploymentScripts'], createObject()))]", + "properties": { + "azPowerShellVersion": "8.0", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess", + "scriptContent": "[variables('$fxv#0')]", + "environmentVariables": [ + { + "name": "DataFactorySubscriptionId", + "value": "[subscription().id]" + }, + { + "name": "DataFactoryResourceGroup", + "value": "[resourceGroup().name]" + }, + { + "name": "DataFactoryName", + "value": "[parameters('dataFactoryName')]" + } + ] }, - { - "name": "CommitmentDiscountId", - "type": "String" + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]", + "identityRoleAssignments" + ] + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('{0}_stopTriggers', parameters('dataFactoryName'))]", + "location": "[parameters('location')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]": {} + } }, - { - "name": "CommitmentDiscountName", - "type": "String" + "kind": "AzurePowerShell", + "tags": "[parameters('tags')]", + "properties": { + "azPowerShellVersion": "8.0", + "retentionInterval": "PT1H", + "cleanupPreference": "OnSuccess", + "scriptContent": "[variables('$fxv#1')]", + "arguments": "-Stop", + "environmentVariables": [ + { + "name": "DataFactorySubscriptionId", + "value": "[subscription().id]" + }, + { + "name": "DataFactoryResourceGroup", + "value": "[resourceGroup().name]" + }, + { + "name": "DataFactoryName", + "value": "[parameters('dataFactoryName')]" + }, + { + "name": "Triggers", + "value": "[join(variables('allHubTriggers'), '|')]" + } + ] }, - { - "name": "CommitmentDiscountType", - "type": "String" - }, - { - "name": "EffectiveCost", - "type": "Decimal" - }, - { - "name": "InvoiceIssuerName", - "type": "String" - }, - { - "name": "ListCost", - "type": "Decimal" - }, - { - "name": "ListUnitPrice", - "type": "Decimal" - }, - { - "name": "PricingCategory", - "type": "String" - }, - { - "name": "PricingQuantity", - "type": "Decimal" - }, - { - "name": "PricingUnit", - "type": "String" - }, - { - "name": "ProviderName", - "type": "String" - }, - { - "name": "PublisherName", - "type": "String" - }, - { - "name": "Region", - "type": "String" - }, - { - "name": "ResourceId", - "type": "String" - }, - { - "name": "ResourceName", - "type": "String" - }, - { - "name": "ResourceType", - "type": "String" - }, - { - "name": "ServiceCategory", - "type": "String" - }, - { - "name": "ServiceName", - "type": "String" - }, - { - "name": "SkuId", - "type": "String" - }, - { - "name": "SkuPriceId", - "type": "String" - }, - { - "name": "SubAccountId", - "type": "String" - }, - { - "name": "SubAccountName", - "type": "String" - }, - { - "name": "SubAccountType", - "type": "String" - }, - { - "name": "Tags", - "type": "String" - }, - { - "name": "UsageQuantity", - "type": "Decimal" - }, - { - "name": "UsageUnit", - "type": "String" - }, - { - "name": "x_AccountName", - "type": "String" - }, - { - "name": "x_AccountOwnerId", - "type": "String" - }, - { - "name": "x_BilledCostInUsd", - "type": "Decimal" - }, - { - "name": "x_BilledUnitPrice", - "type": "Decimal" - }, - { - "name": "x_BillingAccountId", - "type": "String" - }, - { - "name": "x_BillingAccountName", - "type": "String" - }, - { - "name": "x_BillingExchangeRate", - "type": "Decimal" - }, - { - "name": "x_BillingExchangeRateDate", - "type": "DateTime" - }, - { - "name": "x_BillingProfileId", - "type": "String" - }, - { - "name": "x_BillingProfileName", - "type": "String" - }, - { - "name": "x_ChargeId", - "type": "String" - }, - { - "name": "x_CostAllocationRuleName", - "type": "String" - }, - { - "name": "x_CostCenter", - "type": "String" - }, - { - "name": "x_CustomerId", - "type": "String" - }, - { - "name": "x_CustomerName", - "type": "String" - }, - { - "name": "x_EffectiveCostInUsd", - "type": "Decimal" - }, - { - "name": "x_EffectiveUnitPrice", - "type": "Decimal" - }, - { - "name": "x_InvoiceId", - "type": "String" - }, - { - "name": "x_InvoiceIssuerId", - "type": "String" - }, - { - "name": "x_InvoiceSectionId", - "type": "String" - }, - { - "name": "x_InvoiceSectionName", - "type": "String" - }, - { - "name": "x_OnDemandCost", - "type": "Decimal" - }, - { - "name": "x_OnDemandCostInUsd", - "type": "Decimal" - }, - { - "name": "x_OnDemandUnitPrice", - "type": "Decimal" - }, - { - "name": "x_PartnerCreditApplied", - "type": "Boolean" - }, - { - "name": "x_PartnerCreditRate", - "type": "Decimal" - }, - { - "name": "x_PricingBlockSize", - "type": "Decimal" - }, - { - "name": "x_PricingCurrency", - "type": "String" - }, - { - "name": "x_PricingSubcategory", - "type": "String" - }, - { - "name": "x_PricingUnitDescription", - "type": "String" - }, - { - "name": "x_PublisherCategory", - "type": "String" - }, - { - "name": "x_PublisherId", - "type": "String" - }, - { - "name": "x_ResellerId", - "type": "String" - }, - { - "name": "x_ResellerName", - "type": "String" - }, - { - "name": "x_ResourceGroupName", - "type": "String" - }, - { - "name": "x_ResourceType", - "type": "String" - }, - { - "name": "x_ServicePeriodEnd", - "type": "DateTime" - }, - { - "name": "x_ServicePeriodStart", - "type": "DateTime" - }, - { - "name": "x_SkuDescription", - "type": "String" - }, - { - "name": "x_SkuDetails", - "type": "String" - }, - { - "name": "x_SkuIsCreditEligible", - "type": "Boolean" - }, - { - "name": "x_SkuMeterCategory", - "type": "String" - }, - { - "name": "x_SkuMeterId", - "type": "String" - }, - { - "name": "x_SkuMeterName", - "type": "String" - }, - { - "name": "x_SkuMeterSubcategory", - "type": "String" - }, - { - "name": "x_SkuOfferId", - "type": "String" - }, - { - "name": "x_SkuOrderId", - "type": "String" - }, - { - "name": "x_SkuOrderName", - "type": "String" - }, - { - "name": "x_SkuPartNumber", - "type": "String" + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]", + "identityRoleAssignments" + ] + }, + { + "type": "Microsoft.DataFactory/factories/linkedservices", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), parameters('keyVaultName'))]", + "properties": { + "annotations": [], + "parameters": {}, + "type": "AzureKeyVault", + "typeProperties": { + "baseUrl": "[reference(format('Microsoft.KeyVault/vaults/{0}', parameters('keyVaultName')), '2023-02-01').vaultUri]" + } + } + }, + { + "type": "Microsoft.DataFactory/factories/linkedservices", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), parameters('storageAccountName'))]", + "properties": { + "annotations": [], + "parameters": {}, + "type": "AzureBlobFS", + "typeProperties": { + "url": "[reference(format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName')), '2021-08-01').primaryEndpoints.dfs]" + } + } + }, + { + "condition": "[not(empty(parameters('remoteHubStorageUri')))]", + "type": "Microsoft.DataFactory/factories/linkedservices", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), 'remoteHubStorage')]", + "properties": { + "annotations": [], + "parameters": {}, + "type": "AzureBlobFS", + "typeProperties": { + "url": "[parameters('remoteHubStorageUri')]", + "accountKey": { + "type": "AzureKeyVaultSecret", + "store": { + "referenceName": "[parameters('keyVaultName')]", + "type": "LinkedServiceReference" + }, + "secretName": "[format('{0}-storage-key', toLower(parameters('hubName')))]" + } + } }, - { - "name": "x_SkuRegion", - "type": "String" + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('keyVaultName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/datasets", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", + "properties": { + "annotations": [], + "parameters": { + "fileName": { + "type": "String", + "defaultValue": "settings.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" + } + }, + "type": "Json", + "typeProperties": "[variables('datasetPropsDefault')]", + "linkedServiceName": { + "parameters": {}, + "referenceName": "[parameters('storageAccountName')]", + "type": "LinkedServiceReference" + } }, - { - "name": "x_SkuServiceFamily", - "type": "String" + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/datasets", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), 'manifest')]", + "properties": { + "annotations": [], + "parameters": { + "fileName": { + "type": "String", + "defaultValue": "manifest.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('exportContainerName')]" + } + }, + "type": "Json", + "typeProperties": "[variables('datasetPropsDefault')]", + "linkedServiceName": { + "parameters": {}, + "referenceName": "[parameters('storageAccountName')]", + "type": "LinkedServiceReference" + } }, - { - "name": "x_SkuTerm", - "type": "String" + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/datasets", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('safeExportContainerName'))]", + "properties": { + "annotations": [], + "parameters": { + "blobPath": { + "type": "String" + } + }, + "type": "DelimitedText", + "typeProperties": { + "location": { + "type": "AzureBlobFSLocation", + "fileName": { + "value": "@{dataset().blobPath}", + "type": "Expression" + }, + "fileSystem": "[variables('safeExportContainerName')]" + }, + "columnDelimiter": ",", + "escapeChar": "\"", + "quoteChar": "\"", + "firstRowAsHeader": true + }, + "linkedServiceName": { + "parameters": {}, + "referenceName": "[parameters('storageAccountName')]", + "type": "LinkedServiceReference" + } }, - { - "name": "x_SkuTier", - "type": "String" - } - ] - }, - "resources": [ + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('storageAccountName'))]" + ] + }, { - "type": "Microsoft.Resources/deploymentScripts", - "apiVersion": "2020-10-01", - "name": "[format('{0}_deleteOldResources', parameters('dataFactoryName'))]", - "location": "[parameters('location')]", - "identity": { - "type": "UserAssigned", - "userAssignedIdentities": { - "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]": {} + "type": "Microsoft.DataFactory/factories/datasets", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('safeIngestionContainerName'))]", + "properties": { + "annotations": [], + "parameters": { + "blobPath": { + "type": "String" + } + }, + "type": "Parquet", + "typeProperties": { + "location": { + "type": "AzureBlobFSLocation", + "fileName": { + "value": "@{dataset().blobPath}", + "type": "Expression" + }, + "fileSystem": "[variables('safeIngestionContainerName')]" + } + }, + "linkedServiceName": { + "parameters": {}, + "referenceName": "[if(empty(parameters('remoteHubStorageUri')), parameters('storageAccountName'), 'remoteHubStorage')]", + "type": "LinkedServiceReference" } }, - "kind": "AzurePowerShell", - "tags": "[parameters('tags')]", + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), 'remoteHubStorage')]", + "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), parameters('storageAccountName'))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/triggers", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('fileAddedExportTriggerName'))]", "properties": { - "azPowerShellVersion": "8.0", - "retentionInterval": "PT1H", - "cleanupPreference": "OnSuccess", - "scriptContent": "[variables('$fxv#0')]", - "environmentVariables": [ - { - "name": "DataFactorySubscriptionId", - "value": "[subscription().id]" - }, - { - "name": "DataFactoryResourceGroup", - "value": "[resourceGroup().name]" - }, + "annotations": [], + "pipelines": [ { - "name": "DataFactoryName", - "value": "[parameters('dataFactoryName')]" + "pipelineReference": { + "referenceName": "[format('{0}_ExecuteETL', variables('safeExportContainerName'))]", + "type": "PipelineReference" + }, + "parameters": { + "folderPath": "@triggerBody().folderPath", + "fileName": "@triggerBody().fileName" + } } - ] + ], + "type": "BlobEventsTrigger", + "typeProperties": { + "blobPathBeginsWith": "[format('/{0}/blobs/', parameters('exportContainerName'))]", + "blobPathEndsWith": "manifest.json", + "ignoreEmptyBlobs": true, + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "events": [ + "Microsoft.Storage.BlobCreated" + ] + } }, "dependsOn": [ - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]", - "identityRoleAssignments" + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ExecuteETL', variables('safeExportContainerName')))]", + "[resourceId('Microsoft.Resources/deploymentScripts', format('{0}_stopTriggers', parameters('dataFactoryName')))]" ] }, { - "type": "Microsoft.ManagedIdentity/userAssignedIdentities", - "apiVersion": "2023-01-31", - "name": "[format('{0}_triggerManager', parameters('dataFactoryName'))]", - "location": "[parameters('location')]", - "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.ManagedIdentity/userAssignedIdentities'), parameters('tagsByResource')['Microsoft.ManagedIdentity/userAssignedIdentities'], createObject()))]" + "type": "Microsoft.DataFactory/factories/triggers", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('updateConfigTriggerName'))]", + "properties": { + "annotations": [], + "pipelines": [ + { + "pipelineReference": { + "referenceName": "[format('{0}_ConfigureExports', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + } + } + ], + "type": "BlobEventsTrigger", + "typeProperties": { + "blobPathBeginsWith": "[format('/{0}/blobs/', parameters('configContainerName'))]", + "blobPathEndsWith": "settings.json", + "ignoreEmptyBlobs": true, + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "events": [ + "Microsoft.Storage.BlobCreated" + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ConfigureExports', variables('safeConfigContainerName')))]", + "[resourceId('Microsoft.Resources/deploymentScripts', format('{0}_stopTriggers', parameters('dataFactoryName')))]" + ] }, { - "copy": { - "name": "identityRoleAssignments", - "count": "[length(variables('autoStartRbacRoles'))]" - }, - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.DataFactory/factories/{0}', parameters('dataFactoryName'))]", - "name": "[guid(resourceId('Microsoft.DataFactory/factories', parameters('dataFactoryName')), variables('autoStartRbacRoles')[copyIndex()], resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]", + "type": "Microsoft.DataFactory/factories/triggers", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('dailyTriggerName'))]", "properties": { - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('autoStartRbacRoles')[copyIndex()])]", - "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))), '2023-01-31').principalId]", - "principalType": "ServicePrincipal" + "pipelines": [ + { + "pipelineReference": { + "referenceName": "[format('{0}_ExportData', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + }, + "parameters": { + "Recurrence": "Daily" + } + } + ], + "type": "ScheduleTrigger", + "typeProperties": { + "recurrence": { + "frequency": "Hour", + "interval": 24, + "startTime": "2023-01-01T01:01:00", + "timeZone": "[reference(resourceId('Microsoft.Resources/deployments', 'azuretimezones'), '2022-09-01').outputs.Timezone.value]" + } + } }, "dependsOn": [ - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]" + "[resourceId('Microsoft.Resources/deployments', 'azuretimezones')]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ExportData', variables('safeConfigContainerName')))]", + "[resourceId('Microsoft.Resources/deploymentScripts', format('{0}_stopTriggers', parameters('dataFactoryName')))]" ] }, { - "type": "Microsoft.Resources/deploymentScripts", - "apiVersion": "2020-10-01", - "name": "[format('{0}_stopHubTriggers', parameters('dataFactoryName'))]", - "location": "[if(startsWith(parameters('location'), 'china'), 'chinaeast2', parameters('location'))]", - "identity": { - "type": "UserAssigned", - "userAssignedIdentities": { - "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName'))))]": {} + "type": "Microsoft.DataFactory/factories/triggers", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('monthlyTriggerName'))]", + "properties": { + "pipelines": [ + { + "pipelineReference": { + "referenceName": "[format('{0}_ExportData', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + }, + "parameters": { + "Recurrence": "Monthly" + } + } + ], + "type": "ScheduleTrigger", + "typeProperties": { + "recurrence": { + "frequency": "Month", + "interval": 1, + "startTime": "2023-01-05T01:11:00", + "timeZone": "[reference(resourceId('Microsoft.Resources/deployments', 'azuretimezones'), '2022-09-01').outputs.Timezone.value]", + "schedule": { + "monthDays": [ + 5, + 19 + ] + } + } } }, - "kind": "AzurePowerShell", - "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.Resources/deploymentScripts'), parameters('tagsByResource')['Microsoft.Resources/deploymentScripts'], createObject()))]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'azuretimezones')]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ExportData', variables('safeConfigContainerName')))]", + "[resourceId('Microsoft.Resources/deploymentScripts', format('{0}_stopTriggers', parameters('dataFactoryName')))]" + ] + }, + { + "type": "Microsoft.DataFactory/factories/pipelines", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_BackfillData', variables('safeConfigContainerName')))]", "properties": { - "azPowerShellVersion": "8.0", - "retentionInterval": "PT1H", - "cleanupPreference": "OnSuccess", - "scriptContent": "[variables('$fxv#1')]", - "arguments": "-Stop", - "environmentVariables": [ + "activities": [ { - "name": "DataFactorySubscriptionId", - "value": "[subscription().id]" + "name": "Get Config", + "type": "Lookup", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('fileName')", + "type": "Expression" + }, + "folderPath": { + "value": "@variables('folderPath')", + "type": "Expression" + } + } + } + } + }, + { + "name": "Set backfill end date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "endDate", + "value": { + "value": "@addDays(startOfMonth(utcNow()), -1)", + "type": "Expression" + } + } + }, + { + "name": "Set backfill start date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "startDate", + "value": { + "value": "@subtractFromTime(startOfMonth(utcNow()), activity('Get Config').output.firstRow.retention.ingestion.months, 'Month')", + "type": "Expression" + } + } + }, + { + "name": "Set export start date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Set backfill start date", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "thisMonth", + "value": { + "value": "@startOfMonth(variables('endDate'))", + "type": "Expression" + } + } }, { - "name": "DataFactoryResourceGroup", - "value": "[resourceGroup().name]" + "name": "Set export end date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Set export start date", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "nextMonth", + "value": { + "value": "@startOfMonth(subtractFromTime(variables('thisMonth'), 1, 'Month'))", + "type": "Expression" + } + } }, { - "name": "DataFactoryName", - "value": "[parameters('dataFactoryName')]" + "name": "Every Month", + "type": "Until", + "dependsOn": [ + { + "activity": "Set export end date", + "dependencyConditions": [ + "Succeeded" + ] + }, + { + "activity": "Set backfill end date", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "expression": { + "value": "@less(variables('thisMonth'), variables('startDate'))", + "type": "Expression" + }, + "activities": [ + { + "name": "Update export start date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Backfill data", + "dependencyConditions": [ + "Completed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "thisMonth", + "value": { + "value": "@variables('nextMonth')", + "type": "Expression" + } + } + }, + { + "name": "Update export end date", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Update export start date", + "dependencyConditions": [ + "Completed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "variableName": "nextMonth", + "value": { + "value": "@subtractFromTime(variables('thisMonth'), 1, 'Month')", + "type": "Expression" + } + } + }, + { + "name": "Backfill data", + "type": "ExecutePipeline", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "pipeline": { + "referenceName": "[format('{0}_RunBackfill', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + }, + "waitOnCompletion": true, + "parameters": { + "StartDate": { + "value": "@variables('thisMonth')", + "type": "Expression" + }, + "EndDate": { + "value": "@addDays(addToTime(variables('thisMonth'), 1, 'Month'), -1)", + "type": "Expression" + } + } + } + } + ], + "timeout": "0.12:00:00" + } + } + ], + "concurrency": 1, + "variables": { + "exportName": { + "type": "String" }, - { - "name": "Triggers", - "value": "[join(variables('allHubTriggers'), '|')]" + "storageAccountId": { + "type": "String", + "defaultValue": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "finOpsHub": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + }, + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" + }, + "fileName": { + "type": "String", + "defaultValue": "settings.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" + }, + "endDate": { + "type": "String" + }, + "startDate": { + "type": "String" + }, + "thisMonth": { + "type": "String" + }, + "nextMonth": { + "type": "String" } - ] + } }, "dependsOn": [ - "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]", - "identityRoleAssignments" - ] - }, - { - "type": "Microsoft.DataFactory/factories/linkedservices", - "apiVersion": "2018-06-01", - "name": "[format('{0}/{1}', parameters('dataFactoryName'), 'keyVault')]", - "properties": { - "annotations": [], - "parameters": {}, - "type": "AzureKeyVault", - "typeProperties": { - "baseUrl": "[reference(resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName')), '2022-11-01').vaultUri]" - } + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_RunBackfill', variables('safeConfigContainerName')))]" + ], + "metadata": { + "description": "Runs the backfill job for each month based on retention settings." } }, { - "type": "Microsoft.DataFactory/factories/linkedservices", + "type": "Microsoft.DataFactory/factories/pipelines", "apiVersion": "2018-06-01", - "name": "[format('{0}/{1}', parameters('dataFactoryName'), 'storage')]", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_RunBackfill', variables('safeConfigContainerName')))]", "properties": { - "annotations": [], - "parameters": {}, - "type": "AzureBlobFS", - "typeProperties": { - "url": "[reference(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2022-09-01').primaryEndpoints.dfs]", - "accountKey": { - "type": "AzureKeyVaultSecret", - "store": { - "referenceName": "keyVault", - "type": "LinkedServiceReference" + "activities": [ + { + "name": "Get Config", + "type": "Lookup", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false }, - "secretName": "[parameters('storageAccountName')]" + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('fileName')", + "type": "Expression" + }, + "folderPath": { + "value": "@variables('folderPath')", + "type": "Expression" + } + } + } + } + }, + { + "name": "ForEach Export Scope", + "type": "ForEach", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "items": { + "value": "@activity('Get Config').output.firstRow.scopes", + "type": "Expression" + }, + "isSequential": true, + "activities": [ + { + "name": "Set backfill export name", + "type": "SetVariable", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "variableName": "exportName", + "value": { + "value": "@toLower(concat(variables('finOpsHub'), '-monthly-costdetails'))", + "type": "Expression" + } + } + }, + { + "name": "Trigger backfill export", + "type": "WebActivity", + "dependsOn": [ + { + "activity": "Set backfill export name", + "dependencyConditions": [ + "Completed" + ] + } + ], + "policy": { + "timeout": "0.00:05:00", + "retry": 1, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{variables(''resourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports/@{{variables(''exportName'')}}/run?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "POST", + "headers": { + "x-ms-command-name": "[format('FinOpsToolkit.Hubs.config_RunBackfill@{0}', variables('ftkVersion'))]", + "Content-Type": "application/json", + "ClientType": "[format('FinOpsToolkit.Hubs@{0}', variables('ftkVersion'))]" + }, + "body": "{\"timePeriod\" : { \"from\" : \"@{pipeline().parameters.StartDate}\", \"to\" : \"@{pipeline().parameters.EndDate}\" }}", + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('resourceManagementUri')", + "type": "Expression" + } + } + } + } + ] + } } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), 'keyVault')]" - ] - }, - { - "type": "Microsoft.DataFactory/factories/datasets", - "apiVersion": "2018-06-01", - "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('safeExportContainerName'))]", - "properties": { - "annotations": [], + ], + "concurrency": 1, "parameters": { - "fileName": { - "type": "String" + "StartDate": { + "type": "string" }, - "folderName": { - "type": "String" + "EndDate": { + "type": "string" } }, - "type": "DelimitedText", - "typeProperties": "[union(variables('datasetPropsCommon'), variables('datasetPropsDelimitedText'), createObject('compressionCodec', 'none'))]", - "linkedServiceName": { - "parameters": {}, - "referenceName": "storage", - "type": "LinkedServiceReference" + "variables": { + "exportName": { + "type": "String" + }, + "storageAccountId": { + "type": "String", + "defaultValue": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "finOpsHub": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + }, + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" + }, + "fileName": { + "type": "String", + "defaultValue": "settings.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" + } } }, "dependsOn": [ - "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), 'keyVault')]", - "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), 'storage')]" - ] + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]" + ], + "metadata": { + "description": "Creates and triggers exports for all defined scopes for the specified date range." + } }, { - "type": "Microsoft.DataFactory/factories/datasets", + "type": "Microsoft.DataFactory/factories/pipelines", "apiVersion": "2018-06-01", - "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('safeIngestionContainerName'))]", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ExportData', variables('safeConfigContainerName')))]", "properties": { - "annotations": [], + "activities": [ + { + "name": "Get Config", + "type": "Lookup", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('fileName')", + "type": "Expression" + }, + "folderPath": { + "value": "@variables('folderPath')", + "type": "Expression" + } + } + } + } + }, + { + "name": "ForEach Export Scope", + "type": "ForEach", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "items": { + "value": "@activity('Get Config').output.firstRow.scopes", + "type": "Expression" + }, + "isSequential": true, + "activities": [ + { + "name": "Get exports for scope", + "type": "WebActivity", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{variables(''resourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "GET", + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('resourceManagementUri')", + "type": "Expression" + } + } + } + }, + { + "name": "Run exports for scope", + "type": "ExecutePipeline", + "dependsOn": [ + { + "activity": "Get exports for scope", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "pipeline": { + "referenceName": "[format('{0}_RunExports', variables('safeConfigContainerName'))]", + "type": "PipelineReference" + }, + "waitOnCompletion": true, + "parameters": { + "ExportScopes": { + "value": "@activity('Get exports for scope').output.value", + "type": "Expression" + }, + "Recurrence": { + "value": "@pipeline().parameters.Recurrence", + "type": "Expression" + } + } + } + } + ] + } + } + ], + "concurrency": 1, "parameters": { + "Recurrence": { + "type": "string", + "defaultValue": "Daily" + } + }, + "variables": { "fileName": { - "type": "String" + "type": "String", + "defaultValue": "settings.json" }, - "folderName": { - "type": "String" + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" + }, + "finOpsHub": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + }, + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" } - }, - "type": "[if(parameters('convertToParquet'), 'Parquet', 'DelimitedText')]", - "typeProperties": "[union(variables('datasetPropsCommon'), if(parameters('convertToParquet'), createObject(), variables('datasetPropsDelimitedText')), createObject('compressionCodec', 'gzip'))]", - "linkedServiceName": { - "parameters": {}, - "referenceName": "storage", - "type": "LinkedServiceReference" } }, "dependsOn": [ - "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), 'keyVault')]", - "[resourceId('Microsoft.DataFactory/factories/linkedservices', parameters('dataFactoryName'), 'storage')]" - ] + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_RunExports', variables('safeConfigContainerName')))]" + ], + "metadata": { + "description": "Gets a list of all Cost Management exports configured for this hub based on the scopes defined in settings.json, then runs each export using the config_RunExports pipeline." + } }, { - "type": "Microsoft.DataFactory/factories/triggers", + "type": "Microsoft.DataFactory/factories/pipelines", "apiVersion": "2018-06-01", - "name": "[format('{0}/{1}', parameters('dataFactoryName'), variables('exportFileAddedTriggerName'))]", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_RunExports', variables('safeConfigContainerName')))]", "properties": { - "annotations": [], - "pipelines": [ + "activities": [ { - "pipelineReference": { - "referenceName": "[format('{0}_ExecuteETL', parameters('exportContainerName'))]", - "type": "PipelineReference" - }, - "parameters": { - "folderName": "@triggerBody().folderPath", - "fileName": "@triggerBody().fileName" + "name": "ForEach export scope", + "type": "ForEach", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "items": { + "value": "@pipeline().parameters.exportScopes", + "type": "Expression" + }, + "isSequential": true, + "activities": [ + { + "name": "If scheduled", + "type": "IfCondition", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "expression": { + "value": "@and(equals(toLower(item().properties.schedule.recurrence), toLower(pipeline().parameters.Recurrence)),startswith(toLower(item().name), toLower(variables('hubName'))))", + "type": "Expression" + }, + "ifTrueActivities": [ + { + "name": "Trigger export", + "type": "WebActivity", + "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{replace(toLower(concat(variables(''resourceManagementUri''),item().id)), ''com//'', ''com/'')}}/run?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "POST", + "headers": { + "x-ms-command-name": "[format('FinOpsToolkit.Hubs.config_RunExports@{0}', variables('ftkVersion'))]", + "ClientType": "[format('FinOpsToolkit.Hubs@{0}', variables('ftkVersion'))]" + }, + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('resourceManagementUri')", + "type": "Expression" + } + } + } + } + ] + } + } + ] } } ], - "type": "BlobEventsTrigger", - "typeProperties": { - "blobPathBeginsWith": "[format('/{0}/blobs/', parameters('exportContainerName'))]", - "blobPathEndsWith": ".csv", - "ignoreEmptyBlobs": true, - "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", - "events": [ - "Microsoft.Storage.BlobCreated" - ] + "concurrency": 1, + "parameters": { + "ExportScopes": { + "type": "array" + }, + "Recurrence": { + "type": "string", + "defaultValue": "Daily" + } + }, + "variables": { + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" + }, + "hubName": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + } } }, "dependsOn": [ - "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ExecuteETL', variables('safeExportContainerName')))]", - "[resourceId('Microsoft.Resources/deploymentScripts', format('{0}_stopHubTriggers', parameters('dataFactoryName')))]" - ] + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]" + ], + "metadata": { + "description": "Runs the specified Cost Management exports." + } }, { "type": "Microsoft.DataFactory/factories/pipelines", "apiVersion": "2018-06-01", - "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ExecuteETL', variables('safeExportContainerName')))]", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ConfigureExports', variables('safeConfigContainerName')))]", "properties": { "activities": [ { - "name": "Execute", - "type": "ExecutePipeline", + "name": "Get Config", + "type": "Lookup", "dependsOn": [], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, "userProperties": [], "typeProperties": { - "pipeline": { - "referenceName": "[format('{0}_ETL_{1}', variables('safeExportContainerName'), variables('safeIngestionContainerName'))]", - "type": "PipelineReference" - }, - "waitOnCompletion": false, - "parameters": { - "folderName": { - "value": "@pipeline().parameters.folderName", - "type": "Expression" + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false }, - "fileName": { - "value": "@pipeline().parameters.fileName", - "type": "Expression" + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('fileName')", + "type": "Expression" + }, + "folderPath": { + "value": "@variables('folderPath')", + "type": "Expression" + } } } } + }, + { + "name": "ForEach Export Scope", + "type": "ForEach", + "dependsOn": [ + { + "activity": "Get Config", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "userProperties": [], + "typeProperties": { + "items": { + "value": "@activity('Get Config').output.firstRow.scopes", + "type": "Expression" + }, + "isSequential": true, + "activities": [ + { + "name": "Create or update open month focus export", + "type": "WebActivity", + "dependsOn": [ + { + "activity": "Set open month focus export name", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{variables(''resourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports/@{{variables(''exportName'')}}?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "PUT", + "body": { + "value": "[__bicep.getExportBody(parameters('exportContainerName'), variables('focusSchemaVersion'), false())]", + "type": "Expression" + }, + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('ResourceManagementUri')", + "type": "Expression" + } + } + } + }, + { + "name": "Set open month focus export name", + "type": "SetVariable", + "dependsOn": [], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "exportName", + "value": { + "value": "@toLower(concat(variables('finOpsHub'), '-daily-costdetails'))", + "type": "Expression" + } + } + }, + { + "name": "Create or update closed month focus export", + "type": "WebActivity", + "dependsOn": [ + { + "activity": "Set closed month focus export name", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "timeout": "0.00:05:00", + "retry": 2, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "url": { + "value": "[format('@{{variables(''ResourceManagementUri'')}}@{{item().scope}}/providers/Microsoft.CostManagement/exports/@{{variables(''exportName'')}}?api-version={0}', variables('exportApiVersion'))]", + "type": "Expression" + }, + "method": "PUT", + "body": { + "value": "[__bicep.getExportBody(parameters('exportContainerName'), variables('focusSchemaVersion'), true())]", + "type": "Expression" + }, + "authentication": { + "type": "MSI", + "resource": { + "value": "@variables('ResourceManagementUri')", + "type": "Expression" + } + } + } + }, + { + "name": "Set closed month focus export name", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Create or update open month focus export", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "exportName", + "value": { + "value": "@toLower(concat(variables('finOpsHub'), '-monthly-costdetails'))", + "type": "Expression" + } + } + } + ] + } } ], - "parameters": { - "folderName": { - "type": "string" + "concurrency": 1, + "variables": { + "exportName": { + "type": "String" + }, + "exportScope": { + "type": "String" + }, + "storageAccountId": { + "type": "String", + "defaultValue": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + }, + "finOpsHub": { + "type": "String", + "defaultValue": "[parameters('hubName')]" + }, + "resourceManagementUri": { + "type": "String", + "defaultValue": "[environment().resourceManager]" }, "fileName": { - "type": "string" + "type": "String", + "defaultValue": "settings.json" + }, + "folderPath": { + "type": "String", + "defaultValue": "[parameters('configContainerName')]" } - }, - "annotations": [] + } }, "dependsOn": [ - "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ETL_{1}', variables('safeExportContainerName'), variables('safeIngestionContainerName')))]" - ] + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]" + ], + "metadata": { + "description": "Creates Cost Management exports for all scopes." + } }, { "type": "Microsoft.DataFactory/factories/pipelines", "apiVersion": "2018-06-01", - "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ETL_{1}', variables('safeExportContainerName'), variables('safeIngestionContainerName')))]", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ExecuteETL', variables('safeExportContainerName')))]", "properties": { "activities": [ { @@ -1299,8 +2250,8 @@ } }, { - "name": "Set FolderArray", - "type": "SetVariable", + "name": "Read Manifest", + "type": "Lookup", "dependsOn": [ { "activity": "Wait", @@ -1309,23 +2260,74 @@ ] } ], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "manifest", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@pipeline().parameters.fileName", + "type": "Expression" + }, + "folderPath": { + "value": "@pipeline().parameters.folderPath", + "type": "Expression" + } + } + } + } + }, + { + "name": "Set Dataset Type", + "type": "SetVariable", + "dependsOn": [ + { + "activity": "Read Manifest", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "secureOutput": false, + "secureInput": false + }, "userProperties": [], "typeProperties": { - "variableName": "folderArray", + "variableName": "datasetType", "value": { - "value": "@split(pipeline().parameters.folderName, '/')", + "value": "@activity('Read Manifest').output.firstRow.exportConfig.type", "type": "Expression" } } }, { - "name": "Set FolderCount", + "name": "Set Dataset Version", "type": "SetVariable", "dependsOn": [ { - "activity": "Set FolderArray", + "activity": "Read Manifest", "dependencyConditions": [ - "Completed" + "Succeeded" ] } ], @@ -1335,18 +2337,27 @@ }, "userProperties": [], "typeProperties": { - "variableName": "folderCount", - "value": "@length(split(pipeline().parameters.folderName, '/'))" + "variableName": "datasetVersion", + "value": { + "value": "@activity('Read Manifest').output.firstRow.exportConfig.dataVersion", + "type": "Expression" + } } }, { - "name": "Set SecondToLastFolder", + "name": "Set Schema File", "type": "SetVariable", "dependsOn": [ { - "activity": "Set FolderCount", + "activity": "Set Dataset Type", "dependencyConditions": [ - "Completed" + "Succeeded" + ] + }, + { + "activity": "Set Dataset Version", + "dependencyConditions": [ + "Succeeded" ] } ], @@ -1356,16 +2367,19 @@ }, "userProperties": [], "typeProperties": { - "variableName": "secondToLastFolder", - "value": "@variables('folderArray')[sub(variables('folderCount'), 2)]" + "variableName": "schemaFile", + "value": { + "value": "@toLower(concat(variables('datasetType'), '_', variables('datasetVersion'), '.json'))", + "type": "Expression" + } } }, { - "name": "Set ThirdToLastFolder", + "name": "Set Scope", "type": "SetVariable", "dependsOn": [ { - "activity": "Set SecondToLastFolder", + "activity": "Read Manifest", "dependencyConditions": [ "Succeeded" ] @@ -1377,57 +2391,169 @@ }, "userProperties": [], "typeProperties": { - "variableName": "thirdToLastFolder", - "value": "@variables('folderArray')[sub(variables('folderCount'), 3)]" + "variableName": "scope", + "value": { + "value": "@split(toLower(activity('Read Manifest').output.firstRow.exportConfig.resourceId), '/providers/microsoft.costmanagement/exports/')[0]", + "type": "Expression" + } } }, { - "name": "Set FourthToLastFolder", + "name": "Set Date", "type": "SetVariable", "dependsOn": [ { - "activity": "Set ThirdToLastFolder", + "activity": "Read Manifest", + "dependencyConditions": [ + "Succeeded" + ] + } + ], + "policy": { + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "variableName": "date", + "value": { + "value": "@replace(substring(activity('Read Manifest').output.firstRow.runInfo.startDate, 0, 7), '-', '')", + "type": "Expression" + } + } + }, + { + "name": "Failed to Read Manifest", + "type": "Fail", + "dependsOn": [ + { + "activity": "Set Date", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Set Dataset Type", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Set Scope", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Read Manifest", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Set Dataset Version", + "dependencyConditions": [ + "Failed" + ] + }, + { + "activity": "Set Schema File", + "dependencyConditions": [ + "Failed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "message": { + "value": "@concat('Failed to read the manifest file for this export run. Manifest path: ', pipeline().parameters.folderPath)", + "type": "Expression" + }, + "errorCode": "ManifestReadFailed" + } + }, + { + "name": "Check Schema", + "type": "GetMetadata", + "dependsOn": [ + { + "activity": "Set Scope", + "dependencyConditions": [ + "Succeeded" + ] + }, + { + "activity": "Set Date", + "dependencyConditions": [ + "Succeeded" + ] + }, + { + "activity": "Set Schema File", "dependencyConditions": [ "Succeeded" ] } - ], - "policy": { - "secureOutput": false, - "secureInput": false - }, - "userProperties": [], - "typeProperties": { - "variableName": "fourthToLastFolder", - "value": "@variables('folderArray')[sub(variables('folderCount'), 4)]" + ], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@variables('schemaFile')", + "type": "Expression" + }, + "folderPath": "[format('{0}/schemas', parameters('configContainerName'))]" + } + }, + "fieldList": [ + "exists" + ], + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } } }, { - "name": "Set Scope", - "type": "SetVariable", + "name": "Schema Not Found", + "type": "Fail", "dependsOn": [ { - "activity": "Set FourthToLastFolder", + "activity": "Check Schema", "dependencyConditions": [ - "Completed" + "Failed" ] } ], "userProperties": [], "typeProperties": { - "variableName": "scope", - "value": { - "value": "[format('@replace(split(pipeline().parameters.folderName, if(greater(length(variables(''secondToLastFolder'')), 12), variables(''thirdToLastFolder''), variables(''fourthToLastFolder'')))[0], ''{0}'', ''{1}'')', parameters('exportContainerName'), parameters('ingestionContainerName'))]", + "message": { + "value": "@concat('The ', variables('schemaFile'), ' schema mapping file was not found. Please confirm version ', variables('datasetVersion'), ' of the ', variables('datasetType'), ' dataset is supported by this version of FinOps hubs. You may need to upgrade to a newer release. To add support for another dataset, you can create a custom mapping file.')", "type": "Expression" - } + }, + "errorCode": "SchemaNotFound" } }, { - "name": "Set Metric", - "type": "SetVariable", + "name": "For Each Blob", + "type": "ForEach", "dependsOn": [ { - "activity": "Set Scope", + "activity": "Check Schema", "dependencyConditions": [ "Completed" ] @@ -1435,72 +2561,161 @@ ], "userProperties": [], "typeProperties": { - "variableName": "metric", - "value": { - "value": "focuscost", + "items": { + "value": "@activity('Read Manifest').output.firstRow.blobs", "type": "Expression" - } + }, + "isSequential": false, + "activities": [ + { + "name": "Execute", + "type": "ExecutePipeline", + "dependsOn": [], + "policy": { + "secureInput": false + }, + "userProperties": [], + "typeProperties": { + "pipeline": { + "referenceName": "[format('{0}_ETL_{1}', variables('safeExportContainerName'), variables('safeIngestionContainerName'))]", + "type": "PipelineReference" + }, + "waitOnCompletion": true, + "parameters": { + "blobPath": { + "value": "@item().blobName", + "type": "Expression" + }, + "destinationFolder": { + "value": "@toLower(replace(concat(variables('scope'),'/',variables('date'),'/',variables('datasetType')),'//','/'))", + "type": "Expression" + }, + "schemaFile": { + "value": "@variables('schemaFile')", + "type": "Expression" + } + } + } + } + ] } + } + ], + "parameters": { + "folderPath": { + "type": "string" + }, + "fileName": { + "type": "string" + } + }, + "variables": { + "scope": { + "type": "String" + }, + "date": { + "type": "String" }, + "datasetType": { + "type": "String" + }, + "datasetVersion": { + "type": "String" + }, + "schemaFile": { + "type": "String" + } + }, + "annotations": [] + }, + "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), 'manifest')]", + "[resourceId('Microsoft.DataFactory/factories/pipelines', parameters('dataFactoryName'), format('{0}_ETL_{1}', variables('safeExportContainerName'), variables('safeIngestionContainerName')))]" + ], + "metadata": { + "description": "Queues the msexports_ETL_ingestion pipeline." + } + }, + { + "type": "Microsoft.DataFactory/factories/pipelines", + "apiVersion": "2018-06-01", + "name": "[format('{0}/{1}', parameters('dataFactoryName'), format('{0}_ETL_{1}', variables('safeExportContainerName'), variables('safeIngestionContainerName')))]", + "properties": { + "activities": [ { - "name": "Set Date", + "name": "Set Destination File Name", + "description": "", "type": "SetVariable", - "dependsOn": [ - { - "activity": "Set Metric", - "dependencyConditions": [ - "Completed" - ] - } - ], + "dependsOn": [], + "policy": { + "secureOutput": false, + "secureInput": false + }, "userProperties": [], "typeProperties": { - "variableName": "date", + "variableName": "destinationFile", "value": { - "value": "@substring(if(greater(length(variables('secondToLastFolder')), 12), variables('secondToLastFolder'), variables('thirdToLastFolder')), 0, 6)", + "value": "@replace(last(array(split(pipeline().parameters.blobPath, '/'))), '.csv', '.parquet')\n\n\n\n", "type": "Expression" } } }, { - "name": "Set Destination File Name", - "description": "", - "type": "SetVariable", - "dependsOn": [ - { - "activity": "Set Date", - "dependencyConditions": [ - "Completed" - ] - } - ], + "name": "Load Schema Mappings", + "type": "Lookup", + "dependsOn": [], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false + }, "userProperties": [], "typeProperties": { - "variableName": "destinationFile", - "value": { - "value": "[format('@replace(pipeline().parameters.fileName, ''.csv'', ''{0}'')', if(parameters('convertToParquet'), '.parquet', '.csv.gz'))]", - "type": "Expression" + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, + "dataset": { + "referenceName": "[variables('safeConfigContainerName')]", + "type": "DatasetReference", + "parameters": { + "fileName": { + "value": "@toLower(pipeline().parameters.schemaFile)", + "type": "Expression" + }, + "folderPath": "[format('{0}/schemas', parameters('configContainerName'))]" + } } } }, { - "name": "Set Destination Folder Name", - "type": "SetVariable", + "name": "Failed to Load Schema", + "type": "Fail", "dependsOn": [ { - "activity": "Set Destination File Name", + "activity": "Load Schema Mappings", "dependencyConditions": [ - "Completed" + "Failed" ] } ], "userProperties": [], "typeProperties": { - "variableName": "destinationFolder", - "value": { - "value": "@replace(concat(variables('scope'),variables('date'),'/',variables('metric')),'//','/')", + "message": { + "value": "@concat('Unable to load the ', pipeline().parameters.schemaFile, ' schema file. Please confirm the schema and version are supported for FinOps hubs ingestion. Unsupported files will remain in the msexports container.')", "type": "Expression" - } + }, + "errorCode": "SchemaLoadFailed" } }, { @@ -1508,10 +2723,16 @@ "type": "Delete", "dependsOn": [ { - "activity": "Set Destination Folder Name", + "activity": "Set Destination File Name", "dependencyConditions": [ "Completed" ] + }, + { + "activity": "Load Schema Mappings", + "dependencyConditions": [ + "Succeeded" + ] } ], "policy": { @@ -1527,12 +2748,8 @@ "referenceName": "[variables('safeIngestionContainerName')]", "type": "DatasetReference", "parameters": { - "folderName": { - "value": "@variables('destinationFolder')", - "type": "Expression" - }, - "fileName": { - "value": "@variables('destinationFile')", + "blobPath": { + "value": "@concat(pipeline().parameters.destinationFolder, '/', variables('destinationFile'))", "type": "Expression" } } @@ -1557,7 +2774,7 @@ } ], "policy": { - "timeout": "0.12:00:00", + "timeout": "0.00:05:00", "retry": 0, "retryIntervalInSeconds": 30, "secureOutput": false, @@ -1567,6 +2784,10 @@ "typeProperties": { "source": { "type": "DelimitedTextSource", + "additionalColumns": { + "type": "Expression", + "value": "@activity('Load Schema Mappings').output.firstRow.additionalColumns" + }, "storeSettings": { "type": "AzureBlobFSReadSettings", "recursive": true, @@ -1577,18 +2798,21 @@ } }, "sink": { - "type": "DelimitedTextSink", + "type": "ParquetSink", "storeSettings": { "type": "AzureBlobFSWriteSettings" }, - "formatSettings": "[if(parameters('convertToParquet'), createObject('type', 'ParquetWriteSettings', 'fileExtension', '.parquet'), createObject('type', 'DelimitedTextWriteSettings', 'quoteAllText', true(), 'fileExtension', '.csv.gz'))]" + "formatSettings": { + "type": "ParquetWriteSettings", + "fileExtension": ".parquet" + } }, "enableStaging": false, "parallelCopies": 1, "validateDataConsistency": false, "translator": { - "type": "TabularTranslator", - "mappings": "[variables('focusCostMappings')]" + "value": "@activity('Load Schema Mappings').output.firstRow.translator", + "type": "Expression" } }, "inputs": [ @@ -1596,12 +2820,8 @@ "referenceName": "[variables('safeExportContainerName')]", "type": "DatasetReference", "parameters": { - "folderName": { - "value": "@pipeline().parameters.folderName", - "type": "Expression" - }, - "fileName": { - "value": "@pipeline().parameters.fileName", + "blobPath": { + "value": "@pipeline().parameters.blobPath", "type": "Expression" } } @@ -1612,12 +2832,8 @@ "referenceName": "[variables('safeIngestionContainerName')]", "type": "DatasetReference", "parameters": { - "folderName": { - "value": "@variables('destinationFolder')", - "type": "Expression" - }, - "fileName": { - "value": "@variables('destinationFile')", + "blobPath": { + "value": "@concat(pipeline().parameters.destinationFolder, '/', variables('destinationFile'))", "type": "Expression" } } @@ -1625,8 +2841,8 @@ ] }, { - "name": "Delete CSV", - "type": "Delete", + "name": "Read Hub Config", + "type": "Lookup", "dependsOn": [ { "activity": "Convert CSV", @@ -1644,80 +2860,111 @@ }, "userProperties": [], "typeProperties": { + "source": { + "type": "JsonSource", + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": false, + "enablePartitionDiscovery": false + }, + "formatSettings": { + "type": "JsonReadSettings" + } + }, "dataset": { - "referenceName": "[variables('safeExportContainerName')]", + "referenceName": "[variables('safeConfigContainerName')]", "type": "DatasetReference", "parameters": { - "folderName": { - "value": "@pipeline().parameters.folderName", - "type": "Expression" + "fileName": "settings.json", + "folderPath": "[parameters('configContainerName')]" + } + } + } + }, + { + "name": "If Retaining Exports", + "type": "IfCondition", + "dependsOn": [ + { + "activity": "Read Hub Config", + "dependencyConditions": [ + "Completed" + ] + } + ], + "userProperties": [], + "typeProperties": { + "expression": { + "value": "@greater(coalesce(activity('Read Hub Config').output.firstRow.retention.msexports.days, 0), 0)", + "type": "Expression" + }, + "ifFalseActivities": [ + { + "name": "Delete CSV", + "type": "Delete", + "dependsOn": [], + "policy": { + "timeout": "0.12:00:00", + "retry": 0, + "retryIntervalInSeconds": 30, + "secureOutput": false, + "secureInput": false }, - "fileName": { - "value": "@pipeline().parameters.fileName", - "type": "Expression" + "userProperties": [], + "typeProperties": { + "dataset": { + "referenceName": "[variables('safeExportContainerName')]", + "type": "DatasetReference", + "parameters": { + "blobPath": { + "value": "@pipeline().parameters.blobPath", + "type": "Expression" + } + } + }, + "enableLogging": false, + "storeSettings": { + "type": "AzureBlobFSReadSettings", + "recursive": true, + "enablePartitionDiscovery": false + } } } - }, - "enableLogging": false, - "storeSettings": { - "type": "AzureBlobFSReadSettings", - "recursive": true, - "enablePartitionDiscovery": false - } + ] } } ], "parameters": { - "fileName": { + "blobPath": { + "type": "String" + }, + "destinationFolder": { "type": "string" }, - "folderName": { + "schemaFile": { "type": "string" } }, "variables": { "destinationFile": { "type": "String" - }, - "destinationFolder": { - "type": "String" - }, - "folderArray": { - "type": "Array" - }, - "folderCount": { - "type": "Integer" - }, - "secondToLastFolder": { - "type": "String" - }, - "thirdToLastFolder": { - "type": "String" - }, - "fourthToLastFolder": { - "type": "String" - }, - "scope": { - "type": "String" - }, - "date": { - "type": "String" - }, - "metric": { - "type": "String" } }, "annotations": [] }, "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeConfigContainerName'))]", "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeIngestionContainerName'))]", "[resourceId('Microsoft.DataFactory/factories/datasets', parameters('dataFactoryName'), variables('safeExportContainerName'))]" - ] + ], + "metadata": { + "description": "Transforms CSV data to a standard schema and converts to Parquet." + } }, { "type": "Microsoft.Resources/deploymentScripts", "apiVersion": "2020-10-01", - "name": "[format('{0}_startHubTriggers', parameters('dataFactoryName'))]", + "name": "[format('{0}_startTriggers', parameters('dataFactoryName'))]", "location": "[if(startsWith(parameters('location'), 'china'), 'chinaeast2', parameters('location'))]", "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.Resources/deploymentScripts'), parameters('tagsByResource')['Microsoft.Resources/deploymentScripts'], createObject()))]", "identity": { @@ -1754,8 +3001,124 @@ "dependsOn": [ "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}_triggerManager', parameters('dataFactoryName')))]", "identityRoleAssignments", - "[resourceId('Microsoft.DataFactory/factories/triggers', parameters('dataFactoryName'), variables('exportFileAddedTriggerName'))]" + "[resourceId('Microsoft.DataFactory/factories/triggers', parameters('dataFactoryName'), variables('dailyTriggerName'))]", + "[resourceId('Microsoft.DataFactory/factories/triggers', parameters('dataFactoryName'), variables('fileAddedExportTriggerName'))]", + "[resourceId('Microsoft.DataFactory/factories/triggers', parameters('dataFactoryName'), variables('monthlyTriggerName'))]", + "[resourceId('Microsoft.DataFactory/factories/triggers', parameters('dataFactoryName'), variables('updateConfigTriggerName'))]" ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "azuretimezones", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.24.24.22086", + "templateHash": "8239930466136045181" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. The location to use for the managed identity and deployment script to auto-start triggers. Default = (resource group location)." + } + }, + "timezoneobject": { + "type": "object", + "defaultValue": { + "australiaeast": "Australian Eastern Standard Time", + "australiasoutheast": "Australian Eastern Standard Time", + "brazilsouth": "Brasil Standard Time", + "canadacentral": "Central Standard Time", + "canadaeast": "Eastern Standard Time", + "centralindia": "India Standard Time", + "centralus": "Central Standard Time", + "eastasia": "China Standard Time", + "eastus": "Eastern Standard Time", + "eastus2": "Eastern Standard Time", + "francecentral": "Central European Time", + "germanynorth": "Central European Time", + "germanywestcentral": "Central European Time", + "japaneast": "Japan Standard Time", + "japanwest": "Japan Standard Time", + "koreacentral": "Korea Standard Time", + "koreasouth": "Korea Standard Time", + "northcentralus": "Central Standard Time", + "northeurope": "Central European Time", + "norwayeast": "Central European Time", + "norwaywest": "Central European Time", + "southcentralus": "Central Standard Time", + "southindia": "India Standard Time", + "southeastasia": "Singapore Standard Time", + "switzerlandnorth": "Central European Time", + "switzerlandwest": "Central European Time", + "uksouth": "Greenwich Mean Time", + "ukwest": "Greenwich Mean Time", + "westcentralus": "Central Standard Time", + "westeurope": "Central European Time", + "westindia": "India Standard Time", + "westus": "Pacific Standard Time", + "westus2": "Pacific Standard Time" + } + }, + "utchrs": { + "type": "string", + "defaultValue": "[utcNow('hh')]" + }, + "utcmins": { + "type": "string", + "defaultValue": "[utcNow('mm')]" + }, + "utcsecs": { + "type": "string", + "defaultValue": "[utcNow('ss')]" + } + }, + "variables": { + "loc": "[toLower(replace(parameters('location'), ' ', ''))]", + "timezone": "[coalesce(tryGet(parameters('timezoneobject'), variables('loc')), 'Universal Coordinated Time')]" + }, + "resources": [], + "outputs": { + "AzureRegion": { + "type": "string", + "value": "[parameters('location')]" + }, + "Timezone": { + "type": "string", + "value": "[variables('timezone')]" + }, + "UtcHours": { + "type": "string", + "value": "[parameters('utchrs')]" + }, + "UtcMinutes": { + "type": "string", + "value": "[parameters('utcmins')]" + }, + "UtcSeconds": { + "type": "string", + "value": "[parameters('utcsecs')]" + } + } + } + } } ], "outputs": { @@ -1777,6 +3140,7 @@ } }, "dependsOn": [ + "[resourceId('Microsoft.DataFactory/factories', variables('dataFactoryName'))]", "[resourceId('Microsoft.Resources/deployments', 'keyVault')]", "[resourceId('Microsoft.Resources/deployments', 'storage')]" ] @@ -1806,8 +3170,8 @@ "tagsByResource": { "value": "[parameters('tagsByResource')]" }, - "storageAccountName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.name.value]" + "storageAccountKey": { + "value": "[parameters('remoteHubStorageKey')]" }, "accessPolicies": { "value": [ @@ -1830,7 +3194,7 @@ "_generator": { "name": "bicep", "version": "0.24.24.22086", - "templateHash": "10770478197596540923" + "templateHash": "12905161325247490104" } }, "parameters": { @@ -1860,10 +3224,10 @@ "description": "Optional. Array of access policies object." } }, - "storageAccountName": { - "type": "string", + "storageAccountKey": { + "type": "securestring", "metadata": { - "description": "Required. Name of the storage account to store access keys for." + "description": "Optional. Create and store a key for a remote storage account." } }, "sku": { @@ -1907,12 +3271,13 @@ ], "keyVaultPrefix": "[format('{0}-vault', replace(parameters('hubName'), '_', '-'))]", "keyVaultSuffix": "[format('-{0}', parameters('uniqueSuffix'))]", - "keyVaultName": "[replace(format('{0}{1}', take(variables('keyVaultPrefix'), sub(24, length(variables('keyVaultSuffix')))), variables('keyVaultSuffix')), '--', '-')]" + "keyVaultName": "[replace(format('{0}{1}', take(variables('keyVaultPrefix'), sub(24, length(variables('keyVaultSuffix')))), variables('keyVaultSuffix')), '--', '-')]", + "keyVaultSecretName": "[format('{0}-storage-key', toLower(parameters('hubName')))]" }, "resources": [ { "type": "Microsoft.KeyVault/vaults", - "apiVersion": "2022-11-01", + "apiVersion": "2023-02-01", "name": "[variables('keyVaultName')]", "location": "[parameters('location')]", "tags": "[union(parameters('tags'), if(contains(parameters('tagsByResource'), 'Microsoft.KeyVault/vaults'), parameters('tagsByResource')['Microsoft.KeyVault/vaults'], createObject()))]", @@ -1935,7 +3300,7 @@ { "condition": "[not(empty(parameters('accessPolicies')))]", "type": "Microsoft.KeyVault/vaults/accessPolicies", - "apiVersion": "2022-11-01", + "apiVersion": "2023-02-01", "name": "[format('{0}/{1}', variables('keyVaultName'), 'add')]", "properties": { "accessPolicies": "[variables('formattedAccessPolicies')]" @@ -1945,16 +3310,17 @@ ] }, { + "condition": "[not(empty(parameters('storageAccountKey')))]", "type": "Microsoft.KeyVault/vaults/secrets", - "apiVersion": "2022-11-01", - "name": "[format('{0}/{1}', variables('keyVaultName'), parameters('storageAccountName'))]", + "apiVersion": "2023-02-01", + "name": "[format('{0}/{1}', variables('keyVaultName'), variables('keyVaultSecretName'))]", "properties": { "attributes": { "enabled": true, "exp": 1702648632, "nbf": 10000 }, - "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2022-09-01').keys[0].value]" + "value": "[parameters('storageAccountKey')]" }, "dependsOn": [ "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]" @@ -1981,14 +3347,13 @@ "metadata": { "description": "The URI of the key vault." }, - "value": "[reference(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), '2022-11-01').vaultUri]" + "value": "[reference(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName')), '2023-02-01').vaultUri]" } } } }, "dependsOn": [ - "[resourceId('Microsoft.DataFactory/factories', variables('dataFactoryName'))]", - "[resourceId('Microsoft.Resources/deployments', 'storage')]" + "[resourceId('Microsoft.DataFactory/factories', variables('dataFactoryName'))]" ] } ], @@ -2034,6 +3399,20 @@ "description": "URL to use when connecting custom Power BI reports to your data." }, "value": "[format('https://{0}.dfs.{1}/{2}', reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.name.value, environment().suffixes.storage, reference(resourceId('Microsoft.Resources/deployments', 'storage'), '2022-09-01').outputs.ingestionContainer.value)]" + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "Object ID of the Data Factory managed identity. This will be needed when configuring managed exports." + }, + "value": "[reference(resourceId('Microsoft.DataFactory/factories', variables('dataFactoryName')), '2018-06-01', 'full').identity.principalId]" + }, + "managedIdentityTenantId": { + "type": "string", + "metadata": { + "description": "Azure AD tenant ID. This will be needed when configuring managed exports." + }, + "value": "[tenant().tenantId]" } } } @@ -2082,6 +3461,20 @@ "description": "URL to use when connecting custom Power BI reports to your data." }, "value": "[reference(resourceId('Microsoft.Resources/deployments', 'hub'), '2022-09-01').outputs.storageUrlForPowerBI.value]" + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "Object ID of the Data Factory managed identity. This will be needed when configuring managed exports." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'hub'), '2022-09-01').outputs.managedIdentityId.value]" + }, + "managedIdentityTenantId": { + "type": "string", + "metadata": { + "description": "Azure AD tenant ID. This will be needed when configuring managed exports." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'hub'), '2022-09-01').outputs.managedIdentityTenantId.value]" } } } \ No newline at end of file diff --git a/docs/deploy/governance-workbook-0.4.json b/docs/deploy/governance-workbook-0.4.json new file mode 100644 index 000000000..9a1e8a2f2 --- /dev/null +++ b/docs/deploy/governance-workbook-0.4.json @@ -0,0 +1,8282 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.24.24.22086", + "templateHash": "11639158207496465968" + } + }, + "parameters": { + "displayName": { + "type": "string", + "defaultValue": "Governance", + "metadata": { + "description": "Optional. Display name for the workbook used in the Gallery. Must be unique in the resource group." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location of the resources. Default: Same as deployment. See https://aka.ms/azureregions." + } + }, + "description": { + "type": "string", + "defaultValue": "Reports to help you optimize your cost.", + "metadata": { + "description": "Optional. Workbook description." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags for all resources." + } + }, + "enableDefaultTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable telemetry to track anonymous module usage trends, monitor for bugs, and improve future releases." + } + } + }, + "variables": { + "$fxv#0": { + "version": "Notebook/1.0", + "items": [ + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "19b06e9e-eec2-4a7e-935d-92d77b2f87a3", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Overview", + "subTarget": "RC_Overview", + "preText": "", + "style": "link" + }, + { + "id": "528e35b9-aca4-423f-9267-50f62011a3cb", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Virtual machine", + "subTarget": "RC_VM", + "style": "link" + }, + { + "id": "7faacfc6-663e-4ff5-bb64-f86d995f9563", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Storage + backup", + "subTarget": "RC_Storage", + "style": "link" + }, + { + "id": "c17ce2c0-83e6-4e5c-9c3e-f34cbf887e73", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Network", + "subTarget": "RC_Network", + "style": "link" + }, + { + "id": "2f4e49d7-3198-4173-af1c-4cf4c5178000", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "PaaS", + "subTarget": "RC_PaaS", + "style": "link" + }, + { + "id": "f8f7e1fc-8f5d-442a-9788-3eabbf8ab275", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Security", + "subTarget": "RC_Security", + "style": "link" + }, + { + "id": "80ad2db8-a21e-43e9-bd28-75d8d606eaf5", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Monitoring", + "subTarget": "RC_Monitoring", + "style": "link" + }, + { + "id": "6fc0fef0-a016-4923-9239-b641eb5bdc4f", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Services retirement", + "subTarget": "RC_ServicesRetirement", + "style": "link" + }, + { + "id": "e40dbf66-2abe-4bcf-acd7-1ee6d8fc950b", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Resource age", + "subTarget": "RC_Age", + "style": "link" + }, + { + "id": "e112c6e1-db5e-4b0e-99e9-2edac0eba177", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Tag explorer", + "subTarget": "RC_Tag", + "style": "link" + }, + { + "id": "840cd5ea-6b74-484b-846f-01d424b295cd", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Cost management", + "subTarget": "RC_Cost", + "style": "link" + }, + { + "id": "5436a8c9-73c4-4121-a814-dd6fbb0c0d0c", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Usage + limits", + "subTarget": "RC_Quota", + "style": "link" + }, + { + "id": "fa81b57a-8f3c-4502-beb0-128a7fc35f7c", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Compliance", + "subTarget": "RC_Compliance", + "style": "link" + }, + { + "id": "e3acf38e-2dc4-423e-b91d-a173280b5808", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Governance", + "subTarget": "RC_Governance", + "style": "link" + } + ] + }, + "name": "RC_Menu" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "value::tenant" + ], + "parameters": [ + { + "id": "5704765e-092d-41cb-b856-e5d1d5337ac5", + "version": "KqlParameterItem/1.0", + "name": "ManagementGroup", + "label": "Management group", + "type": 2, + "query": "resourcecontainers\r\n| where type == \"microsoft.management/managementgroups\"\r\n| extend name = properties.displayName\r\n| project name", + "crossComponentResources": [ + "value::tenant" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resources/tenants", + "value": null + }, + { + "id": "30297a43-7d69-4daf-93c9-8170d5a995b0", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "resourcecontainers\r\n| where type == \"microsoft.resources/subscriptions\"\r\n| where properties['managementGroupAncestorsChain'] contains '{ManagementGroup:label}'", + "crossComponentResources": [ + "value::tenant" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resources/tenants", + "value": [] + } + ], + "style": "above", + "queryType": 1, + "resourceType": "microsoft.resources/tenants" + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "RC_Age" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "RC_Cost" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "RC_Quota" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "RC_Compliance" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "RC_ServicesRetirement" + } + ], + "name": "parameters - 0" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Welcome the Azure governance workbook" + }, + "name": "Welcome" + }, + { + "type": 1, + "content": { + "json": "### Reference: [Governance in the Microsoft Cloud Adoption Framework for Azure](https://learn.microsoft.com/azure/cloud-adoption-framework/govern/)", + "style": "upsell" + }, + "name": "Reference" + }, + { + "type": 1, + "content": { + "json": "The objective of this workbook is to provide a comprehensive overview of the governance posture of your Azure environment. It offers the standard metrics aligned with the Cloud Adoption Framework and has the capability to identify and apply recommendations to identify non compliance. This workbook is part of the [FinOps toolkit](https://aka.ms/finops/toolkit).\r\n\r\n## Overview of the Cloud Adoption Framework\r\n\r\n* The CAF Govern methodology provides a structured approach for establishing and optimizing cloud governance in Azure. The guidance is relevant for organizations across any industry. It covers essential categories of cloud governance, such as regulatory compliance, security, operations, cost, data, resource management, and artificial intelligence (AI).\r\n\r\n* Cloud governance is how you control cloud use across your organization. Cloud governance sets up guardrails that regulate cloud interactions. These guardrails are a framework of policies, procedures, and tools you use to establish control. Policies define acceptable and unacceptable cloud activity, and the procedures and tools you use ensure all cloud usage aligns with those policies. Successful cloud governance prevents all unauthorized or unmanaged cloud usage.\r\n\r\n* To assess your transformation journey, try the [governance benchmark tool](https://learn.microsoft.com/assessments/b1891add-7646-4d60-a875-32a4ab26327e/).\r\n\r\n\r\n\r\n\r\n" + }, + "name": "text - Overview" + }, + { + "type": 1, + "content": { + "json": "## Prerequisites\r\n\r\nThis workbook will present various cost-related details in the form of governance, networking, storage, VMs, web apps, SQL, and cost information to educate the business about cost related to various resources.\r\n\r\nThis workbook requires the following least-privileged (minimum) roles:\r\n\r\n * **Reader** : allows you to import the workbook without saving it and view all of the workbook tabs except the *Cost management* tab.\r\n * **Cost Management Reader**: allows you to view the costs in the *Cost management* tab \r\n * **Workbook Contributor** : allows you to import and save the workbook\r\n\r\n\r\n" + }, + "name": "text - 7" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\n| summarize count()", + "size": 3, + "title": "Count of all resources", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + } + }, + "name": "Count of all resources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| summarize Count=count(id) by subscriptionId\r\n| order by Count desc", + "size": 3, + "title": "Resource count per subscription (Top 10)", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "type", + "formatter": 1 + } + ], + "rowLimit": 10, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "subscriptionId", + "label": "Subscription name" + } + ] + }, + "sortBy": [], + "tileSettings": { + "titleContent": { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + "leftContent": { + "columnMatch": "Count", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal", + "maximumFractionDigits": 2, + "maximumSignificantDigits": 3 + } + } + }, + "showBorder": false, + "rowLimit": 10, + "sortCriteriaField": "count_type", + "sortOrderField": 2 + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "subscriptionId", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "Count", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "chartSettings": { + "xAxis": "subscriptionId", + "yAxis": [ + "Count" + ], + "showLegend": true, + "seriesLabelSettings": [ + { + "seriesName": "subscriptionId", + "color": "greenDark" + } + ] + }, + "mapSettings": { + "locInfo": "LatLong", + "sizeSettings": "Count", + "sizeAggregation": "Sum", + "legendMetric": "Count", + "legendAggregation": "Sum", + "itemColorSettings": { + "type": "heatmap", + "colorAggregation": "Sum", + "nodeColorField": "Count", + "heatmapPalette": "greenRed" + } + } + }, + "name": "Resource count per subscription (Top 10)" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources \r\n| extend type = case(\r\ntype contains 'microsoft.netapp/netappaccounts', 'NetApp Accounts',\r\ntype contains \"microsoft.compute\", \"Azure Compute\",\r\ntype contains \"microsoft.logic\", \"LogicApps\",\r\ntype contains 'microsoft.keyvault/vaults', \"Key Vaults\",\r\ntype contains 'microsoft.storage/storageaccounts', \"Storage Accounts\",\r\ntype contains 'microsoft.compute/availabilitysets', 'Availability Sets',\r\ntype contains 'microsoft.operationalinsights/workspaces', 'Azure Monitor Resources',\r\ntype contains 'microsoft.operationsmanagement', 'Operations Management Resources',\r\ntype contains 'microsoft.insights', 'Azure Monitor Resources',\r\ntype contains 'microsoft.desktopvirtualization/applicationgroups', 'WVD Application Groups',\r\ntype contains 'microsoft.desktopvirtualization/workspaces', 'WVD Workspaces',\r\ntype contains 'microsoft.desktopvirtualization/hostpools', 'WVD Hostpools',\r\ntype contains 'microsoft.recoveryservices/vaults', 'Backup Vaults',\r\ntype contains 'microsoft.web', 'App Services',\r\ntype contains 'microsoft.managedidentity/userassignedidentities','Managed Identities',\r\ntype contains 'microsoft.storagesync/storagesyncservices', 'Azure File Sync',\r\ntype contains 'microsoft.hybridcompute/machines', 'ARC Machines',\r\ntype contains 'Microsoft.EventHub', 'Event Hub',\r\ntype contains 'Microsoft.EventGrid', 'Event Grid',\r\ntype contains 'Microsoft.Sql', 'SQL Resources',\r\ntype contains 'Microsoft.HDInsight/clusters', 'HDInsight Clusters',\r\ntype contains 'microsoft.devtestlab', 'DevTest Labs Resources',\r\ntype contains 'microsoft.containerinstance', 'Container Instances Resources',\r\ntype contains 'microsoft.portal/dashboards', 'Azure Dashboards',\r\ntype contains 'microsoft.containerregistry/registries', 'Container Registry',\r\ntype contains 'microsoft.automation', 'Automation Resources',\r\ntype contains 'sendgrid.email/accounts', 'SendGrid Accounts',\r\ntype contains 'microsoft.datafactory/factories', 'Data Factory',\r\ntype contains 'microsoft.databricks/workspaces', 'Databricks Workspaces',\r\ntype contains 'microsoft.machinelearningservices/workspaces', 'Machine Learnings Workspaces',\r\ntype contains 'microsoft.alertsmanagement/smartdetectoralertrules', 'Azure Monitor Resources',\r\ntype contains 'microsoft.apimanagement/service', 'API Management Services',\r\ntype contains 'microsoft.dbforpostgresql', 'PostgreSQL Resources',\r\ntype contains 'microsoft.scheduler/jobcollections', 'Scheduler Job Collections',\r\ntype contains 'microsoft.visualstudio/account', 'Azure DevOps Organization',\r\ntype contains 'microsoft.network/', 'Network Resources',\r\ntype contains 'microsoft.migrate/' or type contains 'microsoft.offazure', 'Azure Migrate Resources',\r\ntype contains 'microsoft.servicebus/namespaces', 'Service Bus Namespaces',\r\ntype contains 'microsoft.classic', 'ASM Obsolete Resources',\r\ntype contains 'microsoft.resources/templatespecs', 'Template Spec Resources',\r\ntype contains 'microsoft.virtualmachineimages', 'VM Image Templates',\r\ntype contains 'microsoft.documentdb', 'CosmosDB DB Resources',\r\ntype contains 'microsoft.alertsmanagement/actionrules', 'Azure Monitor Resources',\r\ntype contains 'microsoft.kubernetes/connectedclusters', 'ARC Kubernetes Clusters',\r\ntype contains 'microsoft.purview', 'Purview Resources',\r\ntype contains 'microsoft.security', 'Security Resources',\r\ntype contains 'microsoft.cdn', 'CDN Resources',\r\ntype contains 'microsoft.devices','IoT Resources',\r\ntype contains 'microsoft.datamigration', 'Data Migraiton Services',\r\ntype contains 'microsoft.cognitiveservices', 'Congitive Services',\r\ntype contains 'microsoft.customproviders', 'Custom Providers',\r\ntype contains 'microsoft.appconfiguration', 'App Services',\r\ntype contains 'microsoft.search', 'Search Services',\r\ntype contains 'microsoft.maps', 'Maps',\r\ntype contains 'microsoft.containerservice/managedclusters', 'AKS',\r\ntype contains 'microsoft.signalrservice', 'SignalR',\r\ntype contains 'microsoft.resourcegraph/queries', 'Resource Graph Queries',\r\ntype contains 'microsoft.batch', 'MS Batch',\r\ntype contains 'microsoft.analysisservices', 'Analysis Services',\r\ntype contains 'microsoft.synapse/workspaces', 'Synapse Workspaces',\r\ntype contains 'microsoft.synapse/workspaces/sqlpools', 'Synapse SQL Pools',\r\ntype contains 'microsoft.kusto/clusters', 'ADX Clusters',\r\ntype contains 'microsoft.resources/deploymentscripts', 'Deployment Scripts',\r\ntype contains 'microsoft.aad/domainservices', 'AD Domain Services',\r\ntype contains 'microsoft.labservices/labaccounts', 'Lab Accounts',\r\ntype contains 'microsoft.automanage/accounts', 'Automanage Accounts',\r\ntype contains 'microsoft.relay/namespaces', 'Azure Relay',\r\ntype contains 'microsoft.notificationhubs/namespaces', 'Notification Hubs',\r\ntype contains 'microsoft.digitaltwins/digitaltwinsinstances', 'Digital Twins',\r\nstrcat(\"Not Translated: \", type))\r\n| summarize count() by type\r\n| order by count_ desc", + "size": 3, + "title": "Resource number by type (Top 10)", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "type", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "rowLimit": 10 + } + }, + "name": "Resource number by type" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| summarize count() by location", + "size": 3, + "title": "Resource number by location", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "map", + "mapSettings": { + "locInfo": "AzureLoc", + "sizeSettings": "count_", + "sizeAggregation": "Sum", + "legendMetric": "count_", + "legendAggregation": "Sum", + "itemColorSettings": { + "type": "heatmap", + "colorAggregation": "Sum", + "nodeColorField": "count_", + "heatmapPalette": "greenRed" + }, + "labelSettings": "location", + "locInfoColumn": "location" + } + }, + "name": "Resource number by location" + } + ] + }, + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "group - Overview metrics" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Overview" + }, + "name": "RC_Overview" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Things to know before creating a virtual machine\r\nThere's always a multitude of design considerations when you build out an application infrastructure in Azure. These aspects of a virtual machine are important to think about to manage virtual machine properly:\r\n- The names of your application resources\r\n- The location where the resources are stored\r\n- The size of the virtual machine\r\n- The maximum number of virtual machines that can be created\r\n- The operating system that the virtual machine runs\r\n- The configuration of the virtual machine after it starts\r\n- The related resources that the virtual machine needs\r\n" + }, + "name": "text - 13" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources | where type =~ 'Microsoft.Compute/virtualMachines'\n| summarize count() by tostring(properties.storageProfile.osDisk.osType)", + "size": 3, + "title": "Virtual machine count per OS type", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "properties_storageProfile_osDisk_osType", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "name": "Virtual machine count per OS type" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project SKU = tostring(properties.hardwareProfile.vmSize)\r\n| summarize count() by SKU\r\n| order by count_ desc", + "size": 1, + "title": "VM by VM type/size", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "barchart", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "SKU", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "rowLimit": 10 + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "SKU", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "count_", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "name": "VM by VM type/size" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type=~ 'microsoft.compute/virtualmachinescalesets'\r\n| project subscriptionId, name, location, resourceGroup, Capacity = toint(sku.capacity), Tier = sku.name\r\n| order by Capacity desc", + "size": 0, + "title": "Virtual machine scale set capacity and size", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "name": "query - virtual machine scale set capacity and size" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources | where type == \"microsoft.compute/virtualmachines\"\r\n| extend osDiskId= tostring(properties.storageProfile.osDisk.managedDisk.id)\r\n | join kind=leftouter(resources\r\n | where type =~ 'microsoft.compute/disks'\r\n | where properties !has 'Unattached'\r\n | where properties has 'osType'\r\n | project OS = tostring(properties.osType), osSku = tostring(sku.name), osDiskSizeGB = toint(properties.diskSizeGB), osDiskId=tostring(id)) on osDiskId\r\n | join kind=leftouter(Resources\r\n | where type =~ 'microsoft.compute/disks'\r\n | where properties !has \"osType\"\r\n | where properties !has 'Unattached'\r\n | project sku = tostring(sku.name), diskSizeGB = toint(properties.diskSizeGB), id = managedBy\r\n | summarize sum(diskSizeGB), count(sku) by id, sku) on id\r\n| project vmId=id, subscriptionId, resourceGroup, OS, location, osDiskId, osSku, osDiskSizeGB, DataDisksGB=sum_diskSizeGB, diskSkuCount=count_sku\r\n| sort by diskSkuCount desc", + "size": 0, + "title": "Compute disks", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "location", + "formatter": 17 + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "vmId", + "label": "Resource Name" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "location", + "label": "Region" + }, + { + "columnId": "osDiskId", + "label": "OS Disk" + }, + { + "columnId": "osSku", + "label": "OS Disk SKU" + }, + { + "columnId": "osDiskSizeGB", + "label": "OS Disk Size" + } + ] + } + }, + "name": "Compute disks" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| where type =~ 'microsoft.compute/virtualmachines'\r\n| extend nics=array_length(properties.networkProfile.networkInterfaces)\r\n| mv-expand nic=properties.networkProfile.networkInterfaces\r\n| where nics == 1 or nic.properties.primary =~ 'true' or isempty(nic)\r\n| project vmId = id, vmName = name, vmSize=tostring(properties.hardwareProfile.vmSize), nicId = tostring(nic.id)\r\n\t| join kind=leftouter (\r\n \t\tResources\r\n \t\t| where type =~ 'microsoft.network/networkinterfaces'\r\n \t\t| extend ipConfigsCount=array_length(properties.ipConfigurations)\r\n \t\t| mv-expand ipconfig=properties.ipConfigurations\r\n \t\t| where ipConfigsCount == 1 or ipconfig.properties.primary =~ 'true'\r\n \t\t| project nicId = id, privateIP= tostring(ipconfig.properties.privateIPAddress), publicIpId = tostring(ipconfig.properties.publicIPAddress.id), subscriptionId) on nicId\r\n| project-away nicId1\r\n| summarize by vmId, subscriptionId, vmSize, nicId, privateIP, publicIpId\r\n\t| join kind=leftouter (\r\n \t\tResources\r\n \t\t| where type =~ 'microsoft.network/publicipaddresses'\r\n \t\t| project publicIpId = id, publicIpAddress = tostring(properties.ipAddress)) on publicIpId\r\n| project-away publicIpId1\r\n| sort by publicIpAddress desc", + "size": 0, + "title": "Compute networking", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "vmId", + "label": "Resource name" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + }, + { + "columnId": "vmSize", + "label": "VM size" + }, + { + "columnId": "nicId", + "label": "Network interface" + }, + { + "columnId": "privateIP", + "label": "Private IP" + }, + { + "columnId": "publicIpId", + "label": "Public IP" + }, + { + "columnId": "publicIpAddress", + "label": "Public IP address" + } + ] + } + }, + "name": "Compute networking" + }, + { + "type": 1, + "content": { + "json": "# Managed disk utilization" + }, + "name": "text - 16" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "ec135d58-9c6b-4998-bd1e-75871c540d7f", + "version": "KqlParameterItem/1.0", + "name": "laworkspace", + "label": "Log Analytics workspace", + "type": 5, + "description": "LA workspaces configured in virtual machines insight settings", + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "where type =~ 'microsoft.operationalinsights/workspaces' | project id", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": [] + } + ], + "style": "above", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "name": "Log Analytics workspace selector" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "InsightsMetrics\n| where Origin == \"vm.azm.ms\"\n| where Namespace == \"LogicalDisk\"\n| where Name == \"FreeSpacePercentage\"\n| extend t=parse_json(Tags)\n| summarize arg_max(TimeGenerated, *) by tostring(t[\"vm.azm.ms/mountId\"]), Computer // arg_max over TimeGenerated returns the latest record\n| project Computer, TimeGenerated, t[\"vm.azm.ms/mountId\"], Val\n", + "size": 4, + "title": "Managed disks free space", + "timeContext": { + "durationMs": 604800000 + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "crossComponentResources": [ + "{laworkspace}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Val", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": ">=", + "thresholdValue": "90", + "representation": "4", + "text": "{0}{1}" + }, + { + "operator": ">=", + "thresholdValue": "50", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 1, + "options": { + "style": "decimal", + "useGrouping": false, + "maximumFractionDigits": 0 + } + } + } + ], + "labelSettings": [ + { + "columnId": "Computer", + "label": "Computer" + }, + { + "columnId": "TimeGenerated", + "label": "TimeGenerated" + }, + { + "columnId": "t_vm.azm.ms/mountId", + "label": "Drive" + }, + { + "columnId": "Val", + "label": "Free space percentage" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "laworkspace", + "comparison": "isNotEqualTo" + }, + "name": "Managed disks free space" + }, + { + "type": 1, + "content": { + "json": "# Compute optimization" + }, + "name": "text - 9" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type == \"microsoft.advisor/recommendations\"\r\n| where tostring (properties.category) has \"Cost\"\r\n| where properties.shortDescription.problem has \"underutilized\"\r\n| where properties.impactedField has \"Compute\" or properties.impactedField has \"Container\" or properties.impactedField has \"Web\"\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,subscriptionId,Recommendation=tostring(properties.shortDescription.problem)\r\n", + "size": 0, + "title": "Underused assets", + "noDataMessage": "No underused asset", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "customWidth": "100", + "name": "Underused assets" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "37cdc20d-07c3-466c-84bb-4d8050932641", + "version": "KqlParameterItem/1.0", + "name": "OrphanDisks", + "label": "Orphaned", + "type": 10, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n { \"value\":\"!=\", \"label\":\"No\" },\r\n { \"value\":\"==\", \"label\":\"Yes\" }\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "!=" + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "Disks" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources \r\n| where type contains \"microsoft.compute/disks\" \r\n| extend diskState = tostring(properties.diskState)\r\n| where managedBy {OrphanDisks} \"\" or diskState {OrphanDisks} 'Unattached'\r\n| project id, subscriptionId, resourceGroup, diskState, location", + "size": 0, + "title": "Managed disks", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "location", + "formatter": 17 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource Name" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + }, + { + "columnId": "resourceGroup", + "label": "Resource group" + }, + { + "columnId": "diskState", + "label": "Disk state" + }, + { + "columnId": "location", + "label": "Region" + } + ] + } + }, + "name": "Managed disks" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "2d9b8893-0af4-480a-9ac7-639efb771ecb", + "version": "KqlParameterItem/1.0", + "name": "OrphanNIC", + "label": "Orphaned", + "type": 10, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n { \"value\":\"has 'virtualmachine' or isnotnull(privateEndPoint)\", \"label\":\"No\" },\r\n { \"value\":\"!has 'virtualmachine' and isnull(privateEndPoint)\", \"label\":\"Yes\" }\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "has 'virtualmachine' or isnotnull(privateEndPoint)" + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "NICs - Copy" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| where type has \"microsoft.network/networkinterfaces\"\r\n| extend VM = properties.virtualMachine.id\r\n| extend privateEndPoint = properties['privateEndpoint']['id']\r\n| where properties {OrphanNIC}\r\n| where properties['linkedResourceType'] != \"Microsoft.Netapp/volumes\"\r\n| project id, subscriptionId, resourceGroup, location, VM, privateEndPoint, properties\r\n", + "size": 0, + "title": "NICs", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "location", + "formatter": 17 + }, + { + "columnMatch": "properties", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource name" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + }, + { + "columnId": "resourceGroup", + "label": "Resource group" + }, + { + "columnId": "location", + "label": "Region" + }, + { + "columnId": "VM", + "label": "Virtual machine" + }, + { + "columnId": "privateEndPoint", + "label": "Private end point" + }, + { + "columnId": "properties", + "label": "Details" + } + ] + } + }, + "name": "NICs" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "98d786aa-8835-493f-9fe4-fe5da150392b", + "version": "KqlParameterItem/1.0", + "name": "VMState", + "label": "Virtual machine state", + "type": 2, + "query": "resources\r\n| where type == \"microsoft.compute/virtualmachines\"\r\n| extend state = properties['extended']['instanceView']['powerState']['displayStatus']\r\n| summarize by tostring(state)", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null + } + ], + "style": "above", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "name": "parameters - VMState" + }, + { + "type": 1, + "content": { + "json": "Select a virtual machine state to display the list of resource.", + "style": "info" + }, + "conditionalVisibility": { + "parameterName": "VMState", + "comparison": "isEqualTo" + }, + "name": "text - VMState" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources | where type == \"microsoft.compute/virtualmachines\"\r\n| extend vmState = tostring(properties.extended.instanceView.powerState.displayStatus)\r\n| extend vmState = iif(isempty(vmState), \"VM State Unknown\", (vmState))\r\n| summarize count() by vmState", + "size": 3, + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "tileSettings": { + "titleContent": { + "columnMatch": "vmState", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "showBorder": false + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "vmState", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "count_", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "name": "query - VM state chart" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.compute/virtualmachines\"\r\n| extend vmSize = tostring(properties.hardwareProfile.vmSize)\r\n| extend vmState = properties['extended']['instanceView']['powerState']['displayStatus']\r\n| where vmState == '{VMState}'\r\n| project id, subscriptionId, resourceGroup, vmState, vmSize, location", + "size": 0, + "title": "Virtual machine list by powerstate", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": false + } + }, + { + "columnMatch": "id", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "vmSize", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "19.1429ch" + } + }, + { + "columnMatch": "location", + "formatter": 17, + "formatOptions": { + "customColumnWidthSetting": "108px" + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource Name" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "vmState", + "label": "VM State" + }, + { + "columnId": "vmSize", + "label": "VM Size" + }, + { + "columnId": "location", + "label": "Region" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "VMState", + "comparison": "isNotEqualTo" + }, + "name": "query - VM list by powerstate" + }, + { + "type": 1, + "content": { + "json": "States and billing status of Azure virtual machines : https://learn.microsoft.com/azure/virtual-machines/states-billing", + "style": "info" + }, + "name": "Info VM states" + } + ] + }, + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "group - VMQueries" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_VM" + }, + "name": "RC_VM" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Storage account + backup" + }, + "name": "text - 9" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'microsoft.storagesync/storagesyncservices'\r\n\tor type =~ 'microsoft.recoveryservices/vaults'\r\n\tor type =~ 'microsoft.storage/storageaccounts'\r\n\tor type =~ 'microsoft.keyvault/vaults'\r\n| extend type = case(\r\n\ttype =~ 'microsoft.storagesync/storagesyncservices', 'Azure File Sync',\r\n\ttype =~ 'microsoft.recoveryservices/vaults', 'Azure Backup',\r\n\ttype =~ 'microsoft.storage/storageaccounts', 'Storage Accounts',\r\n\ttype =~ 'microsoft.keyvault/vaults', 'Key Vaults',\r\n\tstrcat(\"Not Translated: \", type))\r\n| where type !has \"Not Translated\"\r\n| summarize count() by type", + "size": 3, + "title": "Count of all resource types", + "noDataMessage": "No resources found", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Resource", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true, + "finalBy": "Resource" + } + }, + "tileSettings": { + "titleContent": { + "columnMatch": "type", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "showBorder": false, + "sortCriteriaField": "type", + "sortOrderField": 1 + } + }, + "name": "query - Storage - Resource Overview " + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'microsoft.storagesync/storagesyncservices'\r\n\tor type =~ 'microsoft.recoveryservices/vaults'\r\n\tor type =~ 'microsoft.storage/storageaccounts'\r\n\tor type =~ 'microsoft.keyvault/vaults'\r\n| extend type = case(\r\n\ttype =~ 'microsoft.storagesync/storagesyncservices', 'Azure File Sync',\r\n\ttype =~ 'microsoft.recoveryservices/vaults', 'Azure Backup',\r\n\ttype =~ 'microsoft.storage/storageaccounts', 'Storage Accounts',\r\n\ttype =~ 'microsoft.keyvault/vaults', 'Key Vaults',\r\n\tstrcat(\"Not Translated: \", type))\r\n| extend Sku = case(\r\n\ttype !has 'Key Vaults', sku.name,\r\n\ttype =~ 'Key Vaults', properties.sku.name,\r\n\t' ')\r\n| extend Details = pack_all()\r\n| project Resource=id, type, kind, subscriptionId, resourceGroup, Sku, Details", + "size": 0, + "title": "Resource details", + "noDataMessage": "No resources found", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "Resource", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + }, + { + "columnMatch": "Details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View Details", + "linkIsContextBlade": true + } + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true, + "finalBy": "Resource" + } + } + }, + "name": "query - Storage - Resource Detailed" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "e94aafa3-c5d9-4523-89f0-4e87aa754511", + "version": "KqlParameterItem/1.0", + "name": "Resources", + "label": "Storage accounts", + "type": 5, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "where type =~ 'microsoft.storage/storageaccounts'\n| order by name asc\n| extend Rank = row_number()\n| project value = id, label = id, selected = Rank <= 5", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "resourceTypeFilter": { + "microsoft.storage/storageaccounts": true + }, + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": [ + "value::all" + ] + }, + { + "id": "c4b69c01-2263-4ada-8d9c-43433b739ff3", + "version": "KqlParameterItem/1.0", + "name": "TimeRange", + "type": 4, + "isRequired": true, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 300000, + "createdTime": "2018-08-06T23:52:38.87Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + }, + { + "durationMs": 900000, + "createdTime": "2018-08-06T23:52:38.871Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + }, + { + "durationMs": 1800000, + "createdTime": "2018-08-06T23:52:38.871Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + }, + { + "durationMs": 3600000, + "createdTime": "2018-08-06T23:52:38.871Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + }, + { + "durationMs": 14400000, + "createdTime": "2018-08-06T23:52:38.871Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + }, + { + "durationMs": 43200000, + "createdTime": "2018-08-06T23:52:38.871Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + }, + { + "durationMs": 86400000, + "createdTime": "2018-08-06T23:52:38.871Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + }, + { + "durationMs": 172800000, + "createdTime": "2018-08-06T23:52:38.871Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + }, + { + "durationMs": 259200000, + "createdTime": "2018-08-06T23:52:38.871Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + }, + { + "durationMs": 604800000, + "createdTime": "2018-08-06T23:52:38.871Z", + "isInitialTime": false, + "grain": 1, + "useDashboardTimeRange": false + } + ], + "allowCustom": true + }, + "value": { + "durationMs": 172800000 + }, + "label": "Time range" + }, + { + "id": "9b48988f-dcd2-48cc-b233-5999ed32149f", + "version": "KqlParameterItem/1.0", + "name": "Message", + "type": 1, + "query": "where type == 'microsoft.storage/storageaccounts' \n| summarize Selected = countif(id in ({Resources:value})), Total = count()\n| extend Selected = iff(Selected > 200, 200, Selected)\n| project Message = strcat('# ', Selected, ' / ', Total)", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + { + "id": "070b2474-4e01-478d-a7fa-6c20ad8ea1ad", + "version": "KqlParameterItem/1.0", + "name": "ResourceName", + "type": 1, + "isRequired": true, + "isHiddenWhenLocked": true, + "criteriaData": [ + { + "condition": "else result = 'Storage account'", + "criteriaContext": { + "operator": "Default", + "rightValType": "param", + "resultValType": "static", + "resultVal": "Storage account" + } + } + ] + }, + { + "id": "c6c32b32-6eb4-44d5-9cad-156d5d50ec3e", + "version": "KqlParameterItem/1.0", + "name": "ResourceImageUrl", + "type": 1, + "description": "used as a parameter for No Subcriptions workbook template", + "isHiddenWhenLocked": true + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters - 1", + "styleSettings": { + "margin": "15px 0 0 0" + } + }, + { + "type": 1, + "content": { + "json": "## Storage accounts details" + }, + "name": "text - 8" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "27d282bb-38ae-4ceb-b2bb-063db08ec6bc", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Overview", + "subTarget": "Overview" + }, + { + "id": "9a52f588-fff8-47fe-b56d-81b8068ff6f7", + "cellValue": "selectedTab", + "linkTarget": "parameter", + "linkLabel": "Capacity", + "subTarget": "Capacity" + } + ] + }, + "name": "Navigation links", + "styleSettings": { + "margin": "10px 0 0 0" + } + }, + { + "type": 1, + "content": { + "json": "### Overview section" + }, + "conditionalVisibility": { + "parameterName": "1", + "comparison": "isEqualTo", + "value": "2" + }, + "name": "text - 4" + }, + { + "type": 10, + "content": { + "chartId": "workbookdb19a8d8-91af-44ea-951d-5ffa133b2ebe", + "version": "MetricsItem/2.0", + "size": 2, + "chartType": 0, + "resourceType": "microsoft.storage/storageaccounts", + "metricScope": 0, + "resourceParameter": "Resources", + "resourceIds": [ + "{Resources}" + ], + "timeContextFromParameter": "TimeRange", + "timeContext": { + "durationMs": 172800000 + }, + "metrics": [ + { + "namespace": "microsoft.storage/storageaccounts", + "metric": "microsoft.storage/storageaccounts-Transaction-Transactions", + "aggregation": 1 + }, + { + "namespace": "microsoft.storage/storageaccounts", + "metric": "microsoft.storage/storageaccounts-Transaction-SuccessE2ELatency", + "aggregation": 4 + }, + { + "namespace": "microsoft.storage/storageaccounts", + "metric": "microsoft.storage/storageaccounts-Transaction-SuccessServerLatency", + "aggregation": 4 + }, + { + "namespace": "microsoft.storage/storageaccounts", + "metric": "microsoft.storage/storageaccounts-Transaction-Transactions", + "aggregation": 1, + "splitBy": "ResponseType", + "splitBySortOrder": -1, + "splitByLimit": 4, + "columnName": "Errors" + } + ], + "resourceLimit": 200, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "insights", + "showIcon": true + } + }, + { + "columnMatch": "Subscription", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "Name", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts-Transaction-Transactions$|Transactions$", + "formatter": 8, + "formatOptions": { + "min": 0, + "palette": "blue", + "showIcon": true, + "aggregation": "Sum" + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal", + "maximumFractionDigits": 1 + } + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts-Transaction-Transactions Timeline$|Transactions Timeline$", + "formatter": 21, + "formatOptions": { + "min": 0, + "palette": "blue", + "showIcon": true + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts-Transaction-SuccessE2ELatency$|microsoft.storage/storageaccounts-Transaction-SuccessServerLatency$|E2E Latency$|Server Latency$", + "formatter": 8, + "formatOptions": { + "min": 0, + "palette": "blue", + "linkTarget": "WorkbookTemplate", + "showIcon": true, + "workbookContext": { + "componentIdSource": "column", + "componentId": "Name", + "resourceIdsSource": "column", + "resourceIds": "Name", + "templateIdSource": "static", + "templateId": "Community-Workbooks/Individual Storage/Performance", + "typeSource": "static", + "type": "workbook", + "gallerySource": "static", + "gallery": "microsoft.storage/storageaccounts" + } + }, + "numberFormat": { + "unit": 23, + "options": { + "style": "decimal", + "maximumFractionDigits": 2 + } + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts-Transaction-SuccessE2ELatency Timeline$|E2E Latency Timeline$", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts-Transaction-SuccessServerLatency Timeline", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "Success/Errors", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "success/Errors", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": ".*\\/Errors", + "formatter": 8, + "formatOptions": { + "min": 0, + "palette": "gray", + "linkTarget": "WorkbookTemplate", + "showIcon": true, + "workbookContext": { + "componentIdSource": "column", + "componentId": "Name", + "resourceIdsSource": "column", + "resourceIds": "Name", + "templateIdSource": "static", + "templateId": "Community-Workbooks/Individual Storage/Failures", + "typeSource": "static", + "type": "workbook", + "gallerySource": "static", + "gallery": "microsoft.storage/storageaccounts" + } + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal", + "maximumFractionDigits": 1 + } + } + }, + { + "columnMatch": "Server Latency Timeline", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + } + ], + "rowLimit": 10000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Subscription" + ], + "expandTopLevel": true, + "finalBy": "Name" + }, + "sortBy": [ + { + "itemKey": "$gen_heatmap_microsoft.storage/storageaccounts-Transaction-Transactions$|Transactions$_3", + "sortOrder": 2 + } + ], + "labelSettings": [ + { + "columnId": "Subscription", + "label": "Subscription" + }, + { + "columnId": "microsoft.storage/storageaccounts-Transaction-Transactions", + "label": "Transactions" + }, + { + "columnId": "microsoft.storage/storageaccounts-Transaction-Transactions Timeline", + "label": "Transactions timeline" + }, + { + "columnId": "microsoft.storage/storageaccounts-Transaction-SuccessE2ELatency", + "label": "E2E latency" + }, + { + "columnId": "microsoft.storage/storageaccounts-Transaction-SuccessE2ELatency Timeline", + "label": "E2E latency timeline" + }, + { + "columnId": "microsoft.storage/storageaccounts-Transaction-SuccessServerLatency", + "label": "Server latency" + }, + { + "columnId": "microsoft.storage/storageaccounts-Transaction-SuccessServerLatency Timeline", + "label": "Server latency timeline" + } + ] + }, + "sortBy": [ + { + "itemKey": "$gen_heatmap_microsoft.storage/storageaccounts-Transaction-Transactions$|Transactions$_3", + "sortOrder": 2 + } + ], + "showExportToExcel": true + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "Overview" + }, + "showPin": true, + "name": "storage account metrics", + "styleSettings": { + "margin": "0 10px 0 10px" + } + }, + { + "type": 1, + "content": { + "json": "## Capacity section" + }, + "conditionalVisibility": { + "parameterName": "1", + "comparison": "isEqualTo", + "value": "2" + }, + "name": "text - 6" + }, + { + "type": 10, + "content": { + "chartId": "workbookdb19a8d8-91af-44ea-951d-5ffa133b2ebe", + "version": "MetricsItem/2.0", + "size": 3, + "chartType": 0, + "resourceType": "microsoft.storage/storageaccounts", + "metricScope": 0, + "resourceParameter": "Resources", + "resourceIds": [ + "{Resources}" + ], + "timeContextFromParameter": "TimeRange", + "timeContext": { + "durationMs": 172800000 + }, + "metrics": [ + { + "namespace": "microsoft.storage/storageaccounts", + "metric": "microsoft.storage/storageaccounts-Capacity-UsedCapacity", + "aggregation": 4 + }, + { + "namespace": "microsoft.storage/storageaccounts/blobservices", + "metric": "microsoft.storage/storageaccounts/blobservices-Capacity-BlobCapacity", + "aggregation": 4 + }, + { + "namespace": "microsoft.storage/storageaccounts/fileservices", + "metric": "microsoft.storage/storageaccounts/fileservices-Capacity-FileCapacity", + "aggregation": 4 + }, + { + "namespace": "microsoft.storage/storageaccounts/queueservices", + "metric": "microsoft.storage/storageaccounts/queueservices-Capacity-QueueCapacity", + "aggregation": 4 + }, + { + "namespace": "microsoft.storage/storageaccounts/tableservices", + "metric": "microsoft.storage/storageaccounts/tableservices-Capacity-TableCapacity", + "aggregation": 4 + } + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "insights", + "showIcon": true + } + }, + { + "columnMatch": "Subscription", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "Name", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts-Capacity-UsedCapacity$|microsoft.storage/storageaccounts/blobservices-Capacity-BlobCapacity$|microsoft.storage/storageaccounts/fileservices-Capacity-FileCapacity$|microsoft.storage/storageaccounts/queueservices-Capacity-QueueCapacity$|microsoft.storage/storageaccounts/tableservices-Capacity-TableCapacity$", + "formatter": 8, + "formatOptions": { + "min": 0, + "palette": "blue", + "linkTarget": "WorkbookTemplate", + "showIcon": true, + "workbookContext": { + "componentIdSource": "column", + "componentId": "Name", + "resourceIdsSource": "column", + "resourceIds": "Name", + "templateIdSource": "static", + "templateId": "Community-Workbooks/Individual Storage/Capacity", + "typeSource": "static", + "type": "workbook", + "gallerySource": "static", + "gallery": "microsoft.storage/storageaccounts" + } + }, + "numberFormat": { + "unit": 2, + "options": { + "style": "decimal", + "maximumFractionDigits": 1 + } + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts-Capacity-UsedCapacity Timeline$|Account used capacity Timeline$", + "formatter": 21, + "formatOptions": { + "min": 0, + "palette": "blue", + "showIcon": true + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts/blobservices-Capacity-BlobCapacity Timeline$|Blob capacity Timeline$", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts/fileservices-Capacity-FileCapacity Timeline$|File capacity Timeline$", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts/queueservices-Capacity-QueueCapacity Timeline$|Queue capacity Timeline$", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "microsoft.storage/storageaccounts/tableservices-Capacity-TableCapacity Timeline$|Table capacity Timeline$", + "formatter": 5, + "formatOptions": { + "showIcon": true + } + } + ], + "rowLimit": 10000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Subscription" + ], + "expandTopLevel": true, + "finalBy": "Name" + }, + "sortBy": [ + { + "itemKey": "$gen_link_$gen_group_0", + "sortOrder": 1 + } + ], + "labelSettings": [ + { + "columnId": "Subscription", + "label": "Subscription" + }, + { + "columnId": "microsoft.storage/storageaccounts-Capacity-UsedCapacity", + "label": "Account used capacity" + }, + { + "columnId": "microsoft.storage/storageaccounts-Capacity-UsedCapacity Timeline", + "label": "Account used capacity timeline" + }, + { + "columnId": "microsoft.storage/storageaccounts/blobservices-Capacity-BlobCapacity", + "label": "Blob capacity" + }, + { + "columnId": "microsoft.storage/storageaccounts/blobservices-Capacity-BlobCapacity Timeline", + "label": "Blob capacity timeline" + }, + { + "columnId": "microsoft.storage/storageaccounts/fileservices-Capacity-FileCapacity", + "label": "File capacity" + }, + { + "columnId": "microsoft.storage/storageaccounts/fileservices-Capacity-FileCapacity Timeline", + "label": "File capacity timeline" + }, + { + "columnId": "microsoft.storage/storageaccounts/queueservices-Capacity-QueueCapacity", + "label": "Queue capacity" + }, + { + "columnId": "microsoft.storage/storageaccounts/queueservices-Capacity-QueueCapacity Timeline", + "label": "Queue capacity timeline" + }, + { + "columnId": "microsoft.storage/storageaccounts/tableservices-Capacity-TableCapacity", + "label": "Table capacity" + }, + { + "columnId": "microsoft.storage/storageaccounts/tableservices-Capacity-TableCapacity Timeline", + "label": "Table capacity timeline" + } + ] + }, + "sortBy": [ + { + "itemKey": "$gen_link_$gen_group_0", + "sortOrder": 1 + } + ], + "showExportToExcel": true + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "Capacity" + }, + "showPin": true, + "name": "storage account capacity metrics", + "styleSettings": { + "margin": "0 10px 0 10px" + } + } + ] + }, + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo", + "value": "" + }, + "name": "Storage account + backup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "Azure Backup now provides a set of customizable reporting templates to help you generate audit evidence reports for backup in an easier way. [Learn more](https://aka.ms/BCDRAuditReportTemplates).", + "style": "upsell" + }, + "name": "AuditText" + }, + { + "type": 1, + "content": { + "json": "## Backup details\r\n### Manage and securely backup your resources\r\nExplore and monitor backup estate at scale in real time across vaults." + }, + "name": "text - 8" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "2373a24f-ad32-4909-a7f6-59b373dcde6c", + "version": "KqlParameterItem/1.0", + "name": "Workspaces", + "label": "Workspace", + "type": 5, + "description": "LA workspaces configured in vault diagnostic settings", + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "where type =~ 'microsoft.operationalinsights/workspaces' | project id", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": [] + } + ], + "style": "above", + "doNotRunWhenHidden": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "100", + "name": "Filters1" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Workspaces}" + ], + "parameters": [ + { + "id": "2965ad33-1401-47c9-8f4b-9b7126f87014", + "version": "KqlParameterItem/1.0", + "name": "TimeRange", + "label": "Time Range", + "type": 4, + "description": "Period of time for which reports should be viewed", + "isRequired": true, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 259200000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 1209600000 + }, + { + "durationMs": 2419200000 + }, + { + "durationMs": 2592000000 + }, + { + "durationMs": 5184000000 + }, + { + "durationMs": 7776000000 + } + ], + "allowCustom": true + }, + "value": { + "durationMs": 604800000 + } + }, + { + "id": "efede5fa-f577-4766-b9b6-6ba4e525f844", + "version": "KqlParameterItem/1.0", + "name": "DataSourceSubscription", + "label": "Datasource Subscription", + "type": 6, + "description": "Use to filter for datasources within a specific subscription", + "isRequired": true, + "multiSelect": true, + "quote": "", + "delimiter": ",", + "query": "let RangeStart = startofday({TimeRange:start});\r\nlet RangeEnd = iff(startofday({TimeRange:end}) == startofday(now()) ,startofday({TimeRange:end}) - 1d , startofday({TimeRange:end}));\r\nlet VaultSubscriptionList = \"*\";\r\nlet VaultLocationList = \"*\";\r\nlet VaultList = \"*\";\r\nlet VaultTypeList = \"*\";\r\nlet ExcludeLegacyEvent = true;\r\nlet BackupSolutionList = \"*\";\r\nlet ProtectionInfoList = \"*\";\r\nlet Item_search = \"*;*\";\r\nlet ItemArray = split(Item_search, \";\");\r\nlet ItemArray_length = array_length(ItemArray);\r\nlet BackupInstanceName = iff(ItemArray_length == 2, ItemArray[1], ItemArray[0] );\r\nlet DatasourceSetName = iff(ItemArray_length == 2, ItemArray[0], \"\");\r\nlet DisplayAllFields = false;\r\n_AzureBackup_GetBackupInstances(RangeStart, RangeEnd, VaultSubscriptionList, VaultLocationList, VaultList, VaultTypeList, ExcludeLegacyEvent, BackupSolutionList, ProtectionInfoList, DatasourceSetName, BackupInstanceName, DisplayAllFields)\r\n| distinct tostring(split(tostring(todynamic(DatasourceResourceId)),\"/\")[2])", + "crossComponentResources": [ + "{Workspaces}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "selectAllValue": "*", + "showDefault": false + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "value": [] + }, + { + "id": "256c7e33-df90-4956-aaf3-699aeaad912f", + "version": "KqlParameterItem/1.0", + "name": "DataSourceLocation", + "label": "Data source location", + "type": 2, + "description": "Use to filter for data sources within a specific location", + "isRequired": true, + "multiSelect": true, + "quote": "", + "delimiter": ",", + "query": "let RangeStart = startofday({TimeRange:start});\r\nlet RangeEnd = iff(startofday({TimeRange:end}) == startofday(now()) ,startofday({TimeRange:end}) - 1d , startofday({TimeRange:end}));\r\nlet VaultSubscriptionList = todynamic( replace(\"/subscriptions/\", \"\", @\"{DataSourceSubscription}\"));\r\nlet VaultLocationList = \"*\";\r\nlet VaultList = \"*\";\r\nlet VaultTypeList = \"*\";\r\nlet ExcludeLegacyEvent = true;\r\nlet BackupSolutionList = \"*\";\r\nlet ProtectionInfoList = \"*\";\r\nlet Item_search = \"*;*\";\r\nlet ItemArray = split(Item_search, \";\");\r\nlet ItemArray_length = array_length(ItemArray);\r\nlet BackupInstanceName = iff(ItemArray_length == 2, ItemArray[1], ItemArray[0] );\r\nlet DatasourceSetName = iff(ItemArray_length == 2, ItemArray[0], \"\");\r\nlet DisplayAllFields = false;\r\n_AzureBackup_GetBackupInstances(RangeStart, RangeEnd, VaultSubscriptionList, VaultLocationList, VaultList, VaultTypeList, ExcludeLegacyEvent, BackupSolutionList, ProtectionInfoList, DatasourceSetName, BackupInstanceName, DisplayAllFields)\r\n| distinct VaultLocation", + "crossComponentResources": [ + "{Workspaces}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "selectAllValue": "*", + "showDefault": false + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "value": [ + "value::all" + ] + }, + { + "id": "16ad110f-4ea3-44d6-826b-4ea3bbd68c93", + "version": "KqlParameterItem/1.0", + "name": "JobOperation", + "label": "Job Operation", + "type": 2, + "description": "Use to filter for a particular operation type", + "isRequired": true, + "multiSelect": true, + "quote": "", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "selectAllValue": "*", + "showDefault": false + }, + "jsonData": "\r\n[ \r\n{ \"value\": \"Backup\", \t\t\t\t\t\t\"label\": \"Backup\" },\r\n{ \"value\": \"Restore\", \t\t\t\t\t\t\"label\": \"Restore\" }\r\n]", + "value": [ + "value::all" + ] + }, + { + "id": "6a6222bf-a28a-4c98-9d74-838e74497167", + "version": "KqlParameterItem/1.0", + "name": "JobStatus", + "label": "Job Status", + "type": 2, + "description": "Use to filter for a particular job status", + "isRequired": true, + "multiSelect": true, + "quote": "", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "selectAllValue": "*", + "showDefault": false + }, + "jsonData": "\r\n[ \r\n{ \"value\": \"Completed\", \t\t\t\t\t\t\"label\": \"Completed\" },\r\n{ \"value\": \"Failed\", \t\t\t\"label\": \"Failed\" },\r\n\r\n{ \"value\": \"CompletedWithWarnings\", \t\t\t\t\t\t\"label\": \"CompletedWithWarnings\" },\r\n{ \"value\": \"Cancelled\", \"label\": \"Cancelled\" }\r\n]", + "value": [ + "value::all" + ] + }, + { + "id": "849a6401-cbaf-44b9-a733-0819f8923791", + "version": "KqlParameterItem/1.0", + "name": "SearchItem", + "label": "Search Item", + "type": 1, + "description": "Use to search for an item by name" + } + ], + "style": "above", + "doNotRunWhenHidden": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "Workspaces", + "comparison": "isNotEqualTo" + }, + "name": "Filters2" + }, + { + "type": 1, + "content": { + "json": "## Backup job history" + }, + "conditionalVisibility": { + "parameterName": "Workspaces", + "comparison": "isNotEqualTo" + }, + "name": "Heading2" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "\r\nlet RangeStart = startofday({TimeRange:start});\r\nlet RangeEnd = iff(startofday({TimeRange:end}) == startofday(now()) ,startofday({TimeRange:end}) - 1d , startofday({TimeRange:end}));\r\nlet DataSourceSubscriptionList = todynamic( replace(\"/subscriptions/\", \"\", @\"{DataSourceSubscription}\"));\r\nlet DataSourceLocationList = todynamic( @\"{DataSourceLocation}\"); \r\nlet VaultTypeList = \"*\";\r\nlet VaultList = \"*\";\r\nlet ExcludeLegacyEvent = true;\r\nlet BackupSolutionList = \"*\";\r\nlet ProtectionInfoList = \"*\";\r\nlet Item_search = \"*;*\";\r\nlet ItemArray = split(Item_search, \";\");\r\nlet ItemArray_length = array_length(ItemArray);\r\nlet BackupInstanceName = iff(ItemArray_length == 2, ItemArray[1], ItemArray[0] );\r\nlet DatasourceSetName = iff(ItemArray_length == 2, ItemArray[0], \"\");\r\nlet JobOperationList = todynamic( @\"{JobOperation}\"); \r\nlet JobStatusList = todynamic( @\"{JobStatus}\");\r\nlet JobFailureCodeList = \"*\";\r\nlet ExcludeLog = true; \r\n_AzureBackup_GetJobs(RangeStart, RangeEnd, DataSourceSubscriptionList, DataSourceLocationList, VaultList, VaultTypeList, ExcludeLegacyEvent, BackupSolutionList, JobOperationList, JobStatusList, JobFailureCodeList, DatasourceSetName, BackupInstanceName, ExcludeLog)\r\n| where BackupInstanceFriendlyName contains iff(isnotempty('{SearchItem}'),'{SearchItem}',BackupInstanceFriendlyName)\r\n| sort by BackupInstanceId\r\n| summarize count() by Status", + "size": 3, + "title": "Jobs by Status", + "noDataMessage": "No record found for the selected time and scope.", + "showRefreshButton": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "crossComponentResources": [ + "{Workspaces}" + ], + "visualization": "piechart", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "UniqueId", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "DurationInSecs", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "customWidth": "0", + "conditionalVisibility": { + "parameterName": "Workspaces", + "comparison": "isNotEqualTo" + }, + "name": "Chart1", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Workspaces}" + ], + "parameters": [ + { + "id": "7a64467f-eec7-495b-9099-233fb7bceb08", + "version": "KqlParameterItem/1.0", + "name": "RowsPerPage", + "label": "Rows per page", + "type": 2, + "description": "Number of rows to display in a single page", + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n { \"value\":10, \"label\":\"10\", \"selected\":true },\r\n { \"value\":25, \"label\":\"25\" },\r\n { \"value\":50, \"label\":\"50\" },\r\n { \"value\":100, \"label\":\"100\" },\r\n { \"value\":250, \"label\":\"250\" },\r\n { \"value\":500, \"label\":\"500\" },\r\n { \"value\":1000, \"label\":\"1000\" }\r\n]" + }, + { + "id": "5c65bc61-a721-42b7-960b-3fe7a6170eb6", + "version": "KqlParameterItem/1.0", + "name": "Page", + "type": 2, + "description": "Page number", + "isRequired": true, + "query": "\r\nlet RangeStart = startofday({TimeRange:start});\r\nlet RangeEnd = iff(startofday({TimeRange:end}) == startofday(now()) ,startofday({TimeRange:end}) - 1d , startofday({TimeRange:end}));\r\nlet DataSourceSubscriptionList = todynamic( replace(\"/subscriptions/\", \"\", @\"{DataSourceSubscription}\"));\r\nlet DataSourceLocationList = todynamic( @\"{DataSourceLocation}\"); \r\nlet VaultTypeList = \"*\";\r\nlet VaultList = \"*\";\r\nlet ExcludeLegacyEvent = true;\r\nlet BackupSolutionList = \"*\";\r\nlet ProtectionInfoList = \"*\";\r\nlet Item_search = \"*;*\";\r\nlet ItemArray = split(Item_search, \";\");\r\nlet ItemArray_length = array_length(ItemArray);\r\nlet BackupInstanceName = iff(ItemArray_length == 2, ItemArray[1], ItemArray[0] );\r\nlet DatasourceSetName = iff(ItemArray_length == 2, ItemArray[0], \"\");\r\nlet JobOperationList = todynamic( @\"{JobOperation}\"); \r\nlet JobStatusList = todynamic( @\"{JobStatus}\");\r\nlet JobFailureCodeList = \"*\";\r\nlet ExcludeLog = true; \r\nlet backupItem = '{SearchItem}';\r\n_AzureBackup_GetJobs(RangeStart, RangeEnd, DataSourceSubscriptionList, DataSourceLocationList, VaultList, VaultTypeList, ExcludeLegacyEvent, BackupSolutionList, JobOperationList, JobStatusList, JobFailureCodeList, DatasourceSetName, BackupInstanceName, ExcludeLog)\r\n| where BackupInstanceFriendlyName contains backupItem\r\n| summarize c=count()\r\n| project num = (c-1)/toint('{RowsPerPage}') + 1\r\n| project nums = range(1,num,1), num\r\n| mvexpand nums\r\n| project nums = tostring(nums), num = strcat(tostring(nums),\" of \",tostring(num))\r\n\r\n", + "crossComponentResources": [ + "{Workspaces}" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "value": "1" + } + ], + "style": "above", + "doNotRunWhenHidden": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "Workspaces", + "comparison": "isNotEqualTo" + }, + "name": "Filters3" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "\r\nlet RangeStart = startofday({TimeRange:start});\r\nlet RangeEnd = iff(startofday({TimeRange:end}) == startofday(now()) ,startofday({TimeRange:end}) - 1d , startofday({TimeRange:end}));\r\nlet DataSourceSubscriptionList = todynamic( replace(\"/subscriptions/\", \"\", @\"{DataSourceSubscription}\"));\r\nlet DataSourceLocationList = todynamic( @\"{DataSourceLocation}\"); \r\nlet VaultTypeList = \"*\";\r\nlet VaultList = \"*\";\r\nlet ExcludeLegacyEvent = true;\r\nlet BackupSolutionList = \"*\";\r\nlet ProtectionInfoList = \"*\";\r\nlet Item_search = \"*;*\";\r\nlet ItemArray = split(Item_search, \";\");\r\nlet ItemArray_length = array_length(ItemArray);\r\nlet BackupInstanceName = iff(ItemArray_length == 2, ItemArray[1], ItemArray[0] );\r\nlet DatasourceSetName = iff(ItemArray_length == 2, ItemArray[0], \"\");\r\nlet JobOperationList = todynamic( @\"{JobOperation}\"); \r\nlet JobStatusList = todynamic( @\"{JobStatus}\");\r\nlet JobFailureCodeList = \"*\";\r\nlet ExcludeLog = true; \r\nlet backupItem = '{SearchItem}';\r\n_AzureBackup_GetJobs(RangeStart, RangeEnd, DataSourceSubscriptionList, DataSourceLocationList, VaultList, VaultTypeList, ExcludeLegacyEvent, BackupSolutionList, JobOperationList, JobStatusList, JobFailureCodeList, DatasourceSetName, BackupInstanceName, ExcludeLog)\r\n| where BackupInstanceFriendlyName contains iff(isnotempty('{SearchItem}'),'{SearchItem}',BackupInstanceFriendlyName)\r\n| sort by BackupInstanceId\r\n| extend row_num = row_number()\r\n| extend page_num = tostring(((row_num-1)/toint('{RowsPerPage}') + 1))\r\n| where page_num has ('{Page}')\r\n| project BackupItem = BackupInstanceId,BackupItemFriendlyName = BackupInstanceFriendlyName ,Vault = VaultResourceId,Subscription = VaultSubscriptionId, VaultLocation = VaultLocation,JobOperation = OperationCategory,JobStartTime = StartTime,JobDuration = tostring(todouble(DurationInSecs)/60/60),JobStatus = Status,FailureCode = ErrorTitle\r\n", + "size": 3, + "title": "List of jobs in period", + "noDataMessage": "No record found for the selected time and scope.", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "crossComponentResources": [ + "{Workspaces}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "BackupItem", + "formatter": 5 + }, + { + "columnMatch": "BackupItemFriendlyName", + "formatter": 16, + "formatOptions": { + "linkColumn": "BackupItem", + "linkTarget": "Resource", + "showIcon": true, + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "Vault", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "Subscription", + "formatter": 15, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true, + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "VaultLocation", + "formatter": 17, + "formatOptions": { + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "JobOperation", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "JobStartTime", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "JobDuration", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "10%" + }, + "numberFormat": { + "unit": 0, + "options": { + "style": "decimal", + "minimumFractionDigits": 2, + "maximumFractionDigits": 2 + } + } + }, + { + "columnMatch": "JobStatus", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "contains", + "thresholdValue": "Warning", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "contains", + "thresholdValue": "Failed", + "representation": "failed", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Blank", + "text": "{0}{1}" + } + ], + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "FailureCode", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "10%" + } + } + ], + "rowLimit": 1000, + "labelSettings": [ + { + "columnId": "BackupItemFriendlyName", + "label": "Backup instance" + }, + { + "columnId": "Vault", + "label": "Vault" + }, + { + "columnId": "Subscription", + "label": "Subscription" + }, + { + "columnId": "VaultLocation", + "label": "Location" + }, + { + "columnId": "JobOperation", + "label": "Job operation" + }, + { + "columnId": "JobStartTime", + "label": "Job start time (UTC)" + }, + { + "columnId": "JobDuration", + "label": "Job duration (hours)" + }, + { + "columnId": "JobStatus", + "label": "Job status" + }, + { + "columnId": "FailureCode", + "label": "Job failure code" + } + ] + }, + "sortBy": [] + }, + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "Workspaces", + "comparison": "isNotEqualTo" + }, + "name": "Grid1", + "styleSettings": { + "margin": "5px", + "padding": "5px", + "showBorder": true + } + } + ] + }, + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "Backup" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Storage" + }, + { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + } + ], + "name": "RC_Storage" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "where type has \"microsoft.network\"\r\n| extend type = case(\r\n\ttype == 'microsoft.network/networkinterfaces', \"NICs\",\r\n\ttype == 'microsoft.network/networksecuritygroups', \"NSGs\", \r\n\ttype == \"microsoft.network/publicipaddresses\", \"Public IPs\", \r\n\ttype == 'microsoft.network/virtualnetworks', \"vNets\",\r\n\ttype == 'microsoft.network/networkwatchers/connectionmonitors', \"Connection Monitors\",\r\n\ttype == 'microsoft.network/privatednszones', \"Private DNS\",\r\n\ttype == 'microsoft.network/virtualnetworkgateways', @\"vNet Gateways\",\r\n\ttype == 'microsoft.network/connections', \"Connections\",\r\n\ttype == 'microsoft.network/networkwatchers', \"Network Watchers\",\r\n\ttype == 'microsoft.network/privateendpoints', \"Private Endpoints\",\r\n\ttype == 'microsoft.network/localnetworkgateways', \"Local Network Gateways\",\r\n\ttype == 'microsoft.network/privatednszones/virtualnetworklinks', \"vNet Links\",\r\n\ttype == 'microsoft.network/dnszones', 'DNS Zones',\r\n\ttype == 'microsoft.network/networkwatchers/flowlogs', 'Flow Logs',\r\n\ttype == 'microsoft.network/routetables', 'Route Tables',\r\n\ttype == 'microsoft.network/loadbalancers', 'Load Balancers',\r\n type =~ 'Microsoft.Network/applicationGateways', 'Application Gateways',\r\n\tstrcat(\"Not Translated: \", type))\r\n| summarize count() by type\r\n| where type !has \"Not Translated\"", + "size": 3, + "title": "Count of all network resources by resource type", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "type", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "name": "query - Network resource" + }, + { + "type": 1, + "content": { + "json": "# Network security group" + }, + "name": "Network security group title" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "7763ba7f-6187-4448-a94c-890392ed31d0", + "version": "KqlParameterItem/1.0", + "name": "OrphanNSG", + "label": "Orphaned", + "type": 10, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n { \"value\":\"and isnotnull(properties.networkInterfaces) or type =~ 'microsoft.network/networksecuritygroups' and isnotnull(properties.subnets)\", \"label\":\"No\" },\r\n { \"value\":\"and isnull(properties.networkInterfaces) and isnull(properties.subnets)\", \"label\":\"Yes\" }\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "and isnotnull(properties.networkInterfaces) or type =~ 'microsoft.network/networksecuritygroups' and isnotnull(properties.subnets)" + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "NSG" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| where type =~ 'microsoft.network/networksecuritygroups' {OrphanNSG}\r\n| project Resource=id, resourceGroup, subscriptionId, location", + "size": 0, + "title": "NSGs", + "noDataMessage": "No NSGs Found", + "noDataMessageStyle": 3, + "exportedParameters": [ + { + "fieldName": "Resource", + "parameterName": "SelectedResourceId", + "parameterType": 5 + } + ], + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "filter": true + }, + "sortBy": [] + }, + "name": "NSGs" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n | where type =~ 'microsoft.network/networksecuritygroups'\r\n | where id == \"{SelectedResourceId}\"\r\n | project id, nsgRules = parse_json(parse_json(properties).securityRules), networksecurityGroupName = name, subscriptionId, resourceGroup , location\r\n | mvexpand nsgRule = nsgRules\r\n | project id, location, access=nsgRule.properties.access,protocol=nsgRule.properties.protocol ,direction=nsgRule.properties.direction,provisioningState= nsgRule.properties.provisioningState ,priority=nsgRule.properties.priority, \r\n sourceAddressPrefix = nsgRule.properties.sourceAddressPrefix, \r\n sourceAddressPrefixes = nsgRule.properties.sourceAddressPrefixes,\r\n destinationAddressPrefix = nsgRule.properties.destinationAddressPrefix, \r\n destinationAddressPrefixes = nsgRule.properties.destinationAddressPrefixes, \r\n networksecurityGroupName, networksecurityRuleName = tostring(nsgRule.name), \r\n subscriptionId, resourceGroup,\r\n destinationPortRanges = nsgRule.properties.destinationPortRanges,\r\n destinationPortRange = nsgRule.properties.destinationPortRange,\r\n sourcePortRanges = nsgRule.properties.sourcePortRanges,\r\n sourcePortRange = nsgRule.properties.sourcePortRange\r\n| extend Details = pack_all()\r\n| project id, location, access, direction, priority, sourceAddressPrefix, sourcePortRange, destinationPortRange, subscriptionId, resourceGroup, Details", + "size": 1, + "title": "NSG rules", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 5 + }, + { + "columnMatch": "resourceGroup", + "formatter": 5 + }, + { + "columnMatch": "Details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "SelectedResourceId", + "comparison": "isNotEqualTo" + }, + "name": "NSG rules" + }, + { + "type": 1, + "content": { + "json": "# Public IPs" + }, + "name": "Public IP title" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "37cdc20d-07c3-466c-84bb-4d8050932641", + "version": "KqlParameterItem/1.0", + "name": "OrphanIPs", + "label": "Orphaned", + "type": 10, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n { \"value\":\"isnotnull\", \"label\":\"No\" },\r\n { \"value\":\"isnull\", \"label\":\"Yes\" }\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "isnotnull" + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "Public IPs" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| where type =~ 'Microsoft.Network/publicIPAddresses' and {OrphanIPs}(properties.ipAddress)\r\n| extend ipAddress = properties.ipAddress\r\n| extend sku = sku.name\r\n| extend Details = pack_all()\r\n| project Resource=id, subscriptionId, resourceGroup, name, location,sku,Details", + "size": 0, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "linkIsContextBlade": true, + "showIcon": true + } + }, + { + "columnMatch": "Resource", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + }, + { + "columnMatch": "Details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ], + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true, + "finalBy": "Resource" + } + } + }, + "name": "query - Networking Details - PiPs" + }, + { + "type": 1, + "content": { + "json": "# Application gateway" + }, + "name": "Application gateway title" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "007b8dbe-6bc6-40f9-b4bc-55f2ec14916c", + "version": "KqlParameterItem/1.0", + "name": "OrphanAppGW", + "label": "Orphaned", + "type": 10, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n { \"value\":\"//\", \"label\":\"No\" },\r\n { \"value\":\"|\", \"label\":\"Yes\" }\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "//" + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "ApplicationGateway" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type =~ 'Microsoft.Network/applicationGateways'\r\n| extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier= tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools\r\n| project id, name, SKUName, SKUTier, SKUCapacity\r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Network/applicationGateways'\r\n | mvexpand backendPools = properties.backendAddressPools\r\n | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations)\r\n | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses)\r\n | extend backendPoolName = backendPools.properties.backendAddressPools.name\r\n | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by id\r\n) on id\r\n| project-away id1\r\n{OrphanAppGW} where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount))\r\n| order by id asc", + "size": 0, + "noDataMessage": "No app gateways", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "name": "query - Application Gateways" + }, + { + "type": 1, + "content": { + "json": "# Load balancer" + }, + "name": "Load balancer title" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "8cffc283-1878-4035-a669-5d9697e9edc1", + "version": "KqlParameterItem/1.0", + "name": "OrphanLB", + "label": "Orphaned", + "type": 10, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n { \"value\":\"!=\", \"label\":\"No\" },\r\n { \"value\":\"==\", \"label\":\"Yes\" }\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "!=" + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "LoadBalancers" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.network/loadbalancers\"\r\n| where properties.backendAddressPools {OrphanLB} \"[]\"\r\n| extend Details = pack_all()\r\n| project Resource=id, subscriptionId, resourceGroup, location, tostring(sku.name), Details", + "size": 0, + "noDataMessage": "No load balancers", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "location", + "formatter": 17 + }, + { + "columnMatch": "Details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ], + "labelSettings": [ + { + "columnId": "Resource", + "label": "Resource Name" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + }, + { + "columnId": "resourceGroup", + "label": "Resource group" + }, + { + "columnId": "location", + "label": "Region" + }, + { + "columnId": "sku_name", + "label": "SKU" + } + ] + } + }, + "name": "query - Load Balancers" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Network" + }, + { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + } + ], + "name": "RC_Network" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Stay informed and act quickly on service issues\r\nAzure Service Health notifies you about Azure service incidents and planned maintenance so you can take action to mitigate downtime. Configure customisable cloud alerts and use your personalised dashboard to analyse health issues, monitor the impact to your cloud resources, get guidance and support, and share details and updates." + }, + "name": "text - 4" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "servicehealthresources\r\n| where type =~ 'Microsoft.ResourceHealth/events'\r\n| extend eventType = properties.EventType, status = properties.Status, description = properties.Title, trackingId = properties.TrackingId, summary = properties.Summary, priority = properties.Priority, impactStartTime = properties.ImpactStartTime, impactMitigationTime = properties.ImpactMitigationTime\r\n| where properties.Status == 'Active' and tolong(impactStartTime) > 1\r\n\r\n| extend Details = pack_all()\r\n| project ServiceHealthID=id, Description=description, Region=location, eventType, Status=status, Details", + "size": 1, + "title": "All active Service Health events", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "ServiceHealthID", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": false, + "customColumnWidthSetting": "20ch" + } + }, + { + "columnMatch": "Description", + "formatter": 1, + "formatOptions": { + "customColumnWidthSetting": "60ch" + } + }, + { + "columnMatch": "eventType", + "formatter": 1 + }, + { + "columnMatch": "Status", + "formatter": 1 + }, + { + "columnMatch": "Details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ] + }, + "tileSettings": { + "showBorder": false + } + }, + "name": "query - 15" + }, + { + "type": 1, + "content": { + "json": "## Activity log monitoring" + }, + "name": "text - 15" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resourcechanges\r\n| extend changeTime = todatetime(properties.changeAttributes.timestamp), targetResourceId = tostring(properties.targetResourceId),\r\nchangeType = tostring(properties.changeType), correlationId = properties.changeAttributes.correlationId, \r\nchangedProperties = properties.changes, changeCount = properties.changeAttributes.changesCount\r\n| where changeTime > ago(1d)\r\n| order by changeTime desc\r\n| project changeTime, targetResourceId, changeType, correlationId, changeCount, changedProperties", + "size": 0, + "title": "All changes in the past one day", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "changeTime", + "formatter": 6, + "formatOptions": { + "customColumnWidthSetting": "24ch" + } + }, + { + "columnMatch": "targetResourceId", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "42.7143ch" + } + }, + { + "columnMatch": "changedProperties", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ] + } + }, + "name": "query - 12" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resourcechanges\r\n| extend changeTime = todatetime(properties.changeAttributes.timestamp), targetResourceId = tostring(properties.targetResourceId),\r\nchangeType = tostring(properties.changeType), correlationId = properties.changeAttributes.correlationId\r\n| where changeType == \"Delete\"\r\n| order by changeTime desc\r\n| project changeTime, resourceGroup, targetResourceId, changeType, correlationId", + "size": 0, + "title": "Resources deleted", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "changeTime", + "formatter": 6 + } + ] + } + }, + "name": "query - 13" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Monitoring" + }, + { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + } + ], + "name": "RC_Monitoring" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Use tags to organize your Azure resources and management hierarchy\r\nTags are metadata elements that you apply to your Azure resources. They're key-value pairs that help you identify resources based on settings that are relevant to your organization. If you want to track the deployment environment for your resources, add a key named Environment. To identify the resources deployed to production, give them a value of Production. The fully formed key-value pair is Environment = Production.\r\n\r\nTo get more information about tags, see [Resource naming and tagging decision guide](https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming-and-tagging-decision-guide?toc=%2Fazure%2Fazure-resource-manager%2Fmanagement%2Ftoc.json)" + }, + "name": "text - 9" + }, + { + "type": 1, + "content": { + "json": "Tag names with spaces, hyphens, and underscores are not supported.", + "style": "info" + }, + "name": "warning tag explorer" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "bae67738-90ef-4698-9020-5e1f91d67f82", + "version": "KqlParameterItem/1.0", + "name": "TagName", + "label": "Tag name", + "type": 2, + "isRequired": true, + "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null + } + ], + "style": "formVertical", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "33", + "name": "parameters - 0" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "cb0ae78d-a49b-457b-baed-d83c97a2c934", + "version": "KqlParameterItem/1.0", + "name": "TagValue", + "label": "Tag value", + "type": 2, + "query": "Resources\r\n| extend TagValue = tostring(tags.{TagName})\r\n| project TagValue\r\n| distinct TagValue", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + } + ], + "style": "formVertical", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "33", + "conditionalVisibility": { + "parameterName": "TagName", + "comparison": "isNotEqualTo" + }, + "name": "parameters - 2" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "81756016-e942-4fa0-976e-06d8ce919f83", + "version": "KqlParameterItem/1.0", + "name": "ResourceType", + "label": "Resource type", + "type": 7, + "typeSettings": { + "additionalResourceOptions": [], + "includeAll": true, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "value": null + } + ], + "style": "formVertical", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "customWidth": "33", + "conditionalVisibility": { + "parameterName": "TagName", + "comparison": "isNotEqualTo" + }, + "name": "ResourceType" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| extend tag = tags.{TagName}\r\n| mv-expand bagexpansion=array tags\r\n| where isnotempty(tags)\r\n//| where tags[0] =~ '{TagName}' and tags[1] =~ '{TagValue}'\r\n| where tags[0] == '{TagName}' and tags[1] == '{TagValue}'\r\n| where type contains '{ResourceType}'\r\n| project id, tag", + "size": 0, + "title": "Resource with tag", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource name" + }, + { + "columnId": "tag", + "label": "Tag value" + } + ] + } + }, + "customWidth": "50", + "conditionalVisibility": { + "parameterName": "TagName", + "comparison": "isNotEqualTo" + }, + "name": "query - Resource with tag" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| extend tag = tags.{TagName}\r\n| mv-expand bagexpansion=array tags\r\n| where isnotempty(tags)\r\n| where tags[0] == '{TagName}' and tags[1] == ''\r\n| where type contains '{ResourceType}'\r\n| project id, tag", + "size": 0, + "title": "Tag with empty value", + "noDataMessage": "No tagged resources with empty value found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource name" + }, + { + "columnId": "tag", + "label": "Tag value" + } + ] + } + }, + "customWidth": "50", + "conditionalVisibility": { + "parameterName": "TagName", + "comparison": "isNotEqualTo" + }, + "name": "query - Empty value" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where tags =~ '' or tags =~ '{}'\r\n| where type contains '{ResourceType}'\r\n| project Name=id", + "size": 0, + "title": "Untagged resources", + "noDataMessage": "No untagged resources found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "rowLimit": 100, + "filter": true, + "labelSettings": [ + { + "columnId": "Name", + "label": "Resource name" + } + ] + } + }, + "name": "query - Untagged resources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resourcecontainers\r\n| where type == \"microsoft.resources/subscriptions\"\r\n| extend tag = tags.{TagName}\r\n| mv-expand bagexpansion=array tags\r\n| where isnotempty(tags)\r\n| where tags[0] == '{TagName}' and tags[1] == '{TagValue}'\r\n| project id, tag", + "size": 0, + "title": "Subscription list", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "tag", + "formatter": 1 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Subscription" + }, + { + "columnId": "tag", + "label": "Tag value" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "TagName", + "comparison": "isNotEqualTo" + }, + "name": "query - Subscription list" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resourcecontainers\r\n| where type == \"microsoft.resources/subscriptions/resourcegroups\"\r\n| extend tag = tags.{TagName}\r\n| mv-expand bagexpansion=array tags\r\n| where isnotempty(tags)\r\n| where tags[0] == '{TagName}' and tags[1] == '{TagValue}'\r\n| project id, tag", + "size": 0, + "title": "Resource groups list", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "tag", + "formatter": 1 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Subscription" + }, + { + "columnId": "tag", + "label": "Tag value" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "TagName", + "comparison": "isNotEqualTo" + }, + "name": "query - ResourceGroup list" + } + ] + }, + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "group - TagQueries" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Tag" + }, + "name": "RC_Tags" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources\r\n| where type == \"microsoft.security/securescores\"\r\n| extend subscriptionSecureScore = round(100 * bin((todouble(properties.score.current))/ todouble(properties.score.max), 0.001))\r\n| where subscriptionSecureScore > 0\r\n| project subscriptionId, subscriptionSecureScore\r\n| order by subscriptionSecureScore asc", + "size": 0, + "title": "Security Scores by Subscription", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true, + "customColumnWidthSetting": "65ch" + } + }, + { + "columnMatch": "subscriptionSecureScore", + "formatter": 8, + "formatOptions": { + "min": 0, + "max": 100, + "palette": "redGreen", + "customColumnWidthSetting": "55ch" + }, + "numberFormat": { + "unit": 1, + "options": { + "style": "decimal", + "useGrouping": false + } + } + } + ], + "labelSettings": [ + { + "columnId": "subscriptionId", + "label": "Subscription Name" + }, + { + "columnId": "subscriptionSecureScore", + "label": "Subscription Secure Score" + } + ] + } + }, + "name": "query - Monitor & Security - Security Scores" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "SecurityResources \r\n| where type == 'microsoft.security/securescores/securescorecontrols' \r\n| extend SecureControl = properties.displayName, unhealthy = properties.unhealthyResourceCount, currentscore = properties.score.current, maxscore = properties.score.max, subscriptionId, details = properties\r\n| project SecureControl , unhealthy, currentscore, maxscore, subscriptionId, details", + "size": 0, + "title": "Security Scores by Control", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true, + "customColumnWidthSetting": "65ch" + } + }, + { + "columnMatch": "Subscription", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true, + "customColumnWidthSetting": "65ch" + } + }, + { + "columnMatch": "SecureControl", + "formatter": 5, + "tooltipFormat": {} + }, + { + "columnMatch": "unhealthy", + "formatter": 8, + "formatOptions": { + "min": 0, + "palette": "greenRed", + "customColumnWidthSetting": "20ch" + } + }, + { + "columnMatch": "currentscore", + "formatter": 8, + "formatOptions": { + "palette": "redGreen", + "customColumnWidthSetting": "20ch" + }, + "numberFormat": { + "unit": 1, + "options": { + "style": "decimal" + } + } + }, + { + "columnMatch": "maxscore", + "formatter": 8, + "formatOptions": { + "palette": "blue", + "customColumnWidthSetting": "20ch" + }, + "numberFormat": { + "unit": 1, + "options": { + "style": "decimal" + } + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + }, + { + "columnMatch": "details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "Details", + "linkIsContextBlade": true + } + }, + { + "columnMatch": "subscriptionSecureScore", + "formatter": 8, + "formatOptions": { + "min": 0, + "max": 100, + "palette": "redGreen", + "customColumnWidthSetting": "20" + }, + "numberFormat": { + "unit": 1, + "options": { + "style": "decimal", + "useGrouping": false + } + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true, + "finalBy": "SecureControl" + }, + "labelSettings": [ + { + "columnId": "unhealthy", + "label": "Unhealthy" + }, + { + "columnId": "currentscore", + "label": "Current Score" + }, + { + "columnId": "maxscore", + "label": "Max Score" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + } + ] + } + }, + "name": "query - Monitor & Security - Security Scores by Control" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "4f93ebba-a9d5-4e11-8de4-b605c2b4368f", + "version": "KqlParameterItem/1.0", + "name": "ResourceIdFilter", + "type": 1, + "isGlobal": true, + "isHiddenWhenLocked": true, + "label": "Resource ID" + }, + { + "id": "e505498f-d2eb-4dd6-928f-0f0f0e9cc371", + "version": "KqlParameterItem/1.0", + "name": "AlertDisplayNameFilter", + "type": 1, + "isGlobal": true, + "isHiddenWhenLocked": true, + "label": "Alert display name" + }, + { + "id": "39e382f9-4780-40fa-8595-15eda0f08ad4", + "version": "KqlParameterItem/1.0", + "name": "NewAlertFilter", + "type": 1, + "isGlobal": true, + "isHiddenWhenLocked": true, + "label": "New alert" + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters - 15" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | where properties.Status in ('Active')\r\n | where properties.Severity in ('Low', 'Medium', 'High')\r\n | extend SeverityRank = case(\r\n properties.Severity == 'High', 3,\r\n properties.Severity == 'Medium', 2,\r\n properties.Severity == 'Low', 1,\r\n 0\r\n )\r\n | project-away SeverityRank\r\n | extend Severity = properties.Severity\r\n | project Severity = tostring(Severity)\r\n | summarize Count = count() by Severity", + "size": 0, + "title": "Severity ", + "exportedParameters": [ + { + "fieldName": "Subscription", + "parameterName": "Subscription", + "parameterType": 1 + }, + { + "fieldName": "Severity", + "parameterName": "SeverityFilter", + "parameterType": 1 + } + ], + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Severity", + "formatter": 1 + } + ] + }, + "chartSettings": { + "yAxis": [ + "Count" + ], + "seriesLabelSettings": [ + { + "seriesName": "Medium", + "color": "orange" + }, + { + "seriesName": "High", + "color": "redDark" + }, + { + "seriesName": "Low", + "color": "yellow" + } + ] + } + }, + "customWidth": "33", + "name": "Severity" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | summarize Count =count() by resourceGroup", + "size": 0, + "title": "Resource Group", + "exportFieldName": "resourceGroup", + "exportParameterName": "resourceGroupFilter", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart" + }, + "customWidth": "33", + "name": "query - 9" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n | project id = tolower(id), tags\r\n | join kind=inner (securityresources\r\n | where type =~ \"microsoft.security/locations/alerts\"\r\n | extend isAzure = tostring(properties.ResourceIdentifiers) matches regex '\"Type\"\\\\s*:\\\\s*\"AzureResource\"'\r\n | extend affectedResourceId = extract('\"AzureResourceId\"\\\\s*:\\\\s*\"([^\"]*)\"', 1, tostring(properties.ResourceIdentifiers))\r\n | extend hostName = iff(isAzure, \"\", extract('\"HostName\"\\\\s*:\\\\s*\"([^\"]*)\"', 1, tostring(properties.Entities)))\r\n | extend splitAffectedResourceId = split(affectedResourceId, \"/\")\r\n | extend resourceNameIndex = iff(array_length(splitAffectedResourceId) > 1, array_length(splitAffectedResourceId) - 1, 0)\r\n | extend affectedResourceName = iff(isAzure, splitAffectedResourceId[resourceNameIndex], iff(isempty(hostName), \"Non-Azure\", hostName))| project-away resourceNameIndex, splitAffectedResourceId, hostName, isAzure\r\n | project alertId = id, subscriptionId, alertProperties = properties, affectedResourceId = tolower(affectedResourceId)\r\n ) on $left.id == $right.affectedResourceId\r\n | extend id = alertId, subscriptionId, properties = alertProperties\r\n | where properties.Status in ('Active')\r\n | where properties.Severity in ('Low', 'Medium', 'High')\r\n | extend SeverityRank = case(\r\n properties.Severity == 'High', 3,\r\n properties.Severity == 'Medium', 2,\r\n properties.Severity == 'Low', 1,\r\n 0\r\n )\r\n | sort by SeverityRank desc, tostring(properties.SystemAlertId) asc\r\n | extend Tag = parse_json(tags)\r\n | mv-expand Tag\r\n | parse Tag with * ':\"' TagValue '\"}'\r\n | project TagValue, alertId\r\n | summarize Count = count() by TagValue", + "size": 0, + "title": "Tag", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart" + }, + "customWidth": "30", + "name": "query - 7", + "styleSettings": { + "maxWidth": "100%" + } + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "datatable(ResourceId: string) [ \"All\"] | union (securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | extend Prop = parse_json(properties)\r\n | where Prop.Severity == \"High\"\r\n | extend ResourceIdentifiers = Prop.[\"ResourceIdentifiers\"]\r\n | project ResourceIdentifiers\r\n | mv-expand ResourceIdentifiers\r\n | extend ResourceId = parse_json(ResourceIdentifiers).[\"AzureResourceId\"]\r\n | where isnotempty(ResourceId )\r\n| summarize Count=count() by tostring(ResourceId)\r\n | top 5 by Count)", + "size": 1, + "title": "Top 5 attacked resources (with High Severity)", + "noDataMessage": "There are no Top 5 attacked resources found", + "exportedParameters": [ + { + "fieldName": "ResourceId", + "parameterName": "ResourceIdFilter", + "defaultValue": "All" + }, + { + "fieldName": "ResourceId", + "parameterName": "ShowTable", + "parameterType": 1 + } + ], + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Resource ID", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "sortBy": [ + { + "itemKey": "Count", + "sortOrder": 2 + } + ], + "labelSettings": [ + { + "columnId": "ResourceId", + "label": "Resource ID" + } + ] + }, + "sortBy": [ + { + "itemKey": "Count", + "sortOrder": 2 + } + ] + }, + "customWidth": "33", + "name": "Top 5 attacked resources (with High Severity)" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " datatable(AlertDisplayName: string) [ \"All\"] | union(securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | extend Prop = parse_json(properties)\r\n | extend AlertDisplayName = Prop.[\"AlertDisplayName\"]\r\n | project tostring(AlertDisplayName)\r\n | summarize Count = count() by AlertDisplayName\r\n | top 5 by Count)", + "size": 1, + "title": "Top alert types ", + "exportedParameters": [ + { + "fieldName": "AlertDisplayName", + "parameterName": "AlertDisplayNameFilter", + "defaultValue": "All" + }, + { + "fieldName": "AlertDisplayName", + "parameterName": "ShowTable", + "parameterType": 1 + } + ], + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "sortBy": [ + { + "itemKey": "Count", + "sortOrder": 2 + } + ], + "labelSettings": [ + { + "columnId": "AlertDisplayName", + "label": "Alert display name" + } + ] + }, + "sortBy": [ + { + "itemKey": "Count", + "sortOrder": 2 + } + ] + }, + "customWidth": "33", + "name": "Top alert types" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " datatable(AlertDisplayName: string) [ \"All\"] | union(securityresources\r\n| where type =~ 'microsoft.security/locations/alerts'\r\n| extend Prop = parse_json(properties)\r\n| extend TimeGeneratedUtc = Prop.[\"TimeGeneratedUtc\"]\r\n| extend AlertDisplayName = Prop.[\"AlertDisplayName\"]\r\n| where TimeGeneratedUtc > ago(24h)\r\n| summarize Count=count() by tostring(AlertDisplayName))", + "size": 1, + "title": "New Alerts (Since last 24hrs)", + "noDataMessage": "No new alerts in Last 24 hours", + "noDataMessageStyle": 3, + "exportedParameters": [ + { + "fieldName": "AlertDisplayName", + "parameterName": "NewAlertFilter", + "defaultValue": "All" + }, + { + "fieldName": "AlertDisplayName", + "parameterName": "ShowTable", + "parameterType": 1 + } + ], + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "ClearOther", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "AlertDisplayName", + "label": "Alert display name" + } + ] + }, + "sortBy": [] + }, + "customWidth": "33", + "name": "New Alerts (Since last 24hrs)" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "", + "size": 0, + "title": "Parameters at this point", + "queryType": 2 + }, + "conditionalVisibility": { + "parameterName": "parameter1", + "comparison": "isEqualTo", + "value": "1" + }, + "name": "query - 23" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources\r\n| where type == \"microsoft.security/locations/alerts\"\r\n| project-rename P= properties\r\n| extend Details = parse_json(P)\r\n| extend IsIncident = Details.[\"IsIncident\"]\r\n| extend AlertDisplayName = Details.[\"AlertDisplayName\"]\r\n| extend SystemAlertId = Details.[\"SystemAlertId\"]\r\n| extend Severity = tostring(Details.[\"Severity\"])\r\n| where Severity == \"High\"\r\n| extend AlertUri = Details.[\"AlertUri\"]\r\n| extend Status = tostring(Details.[\"Status\"])\r\n| extend Tactics = tostring(Details.[\"Intent\"])\r\n| extend ResourceIdentifiers = Details.[\"ResourceIdentifiers\"]\r\n| mv-expand ResourceIdentifiers\r\n| extend ResourceId = parse_json(ResourceIdentifiers).[\"AzureResourceId\"]\r\n| where Status == \"Active\"\r\n| where (\"{ResourceIdFilter}\" == \"All\" or ResourceId == \"{ResourceIdFilter}\") \r\n // if either alert name or new alert are set, union those 2 together, if neither are set treat as all\r\n and ((\"{AlertDisplayNameFilter}\" == \"All\" and \"{NewAlertFilter}\" == \"All\") or AlertDisplayName == \"{AlertDisplayNameFilter}\" or AlertDisplayName == \"{NewAlertFilter}\")\r\n| extend SeverityRank = case(\r\n Severity == 'High', 3,\r\n Severity == 'Medium', 2,\r\n Severity == 'Low', 1,\r\n 0\r\n )\r\n| parse AlertUri with * '/subscriptionId/' SubscriptionId '/' *\r\n| parse AlertUri with * '/resourceGroup/' ResourceGroup '/' *\r\n| parse AlertUri with * '/location/' Location \r\n| project\r\n Severity,\r\n SystemAlertId,\r\n AlertDisplayName,\r\n IsIncident = iif(IsIncident == \"true\", \"Incident\", \"Alert\"),\r\n AlertUri,\r\n Tactics,\r\n SeverityRank,\r\n SubscriptionId,\r\n ResourceGroup,\r\n Location,\r\n ResourceId\r\n| sort by SeverityRank", + "size": 0, + "title": "{$rowCount} Active Alerts ", + "exportedParameters": [ + { + "fieldName": "ResourceId", + "parameterName": "Resource", + "parameterType": 1 + }, + { + "fieldName": "AlertUri", + "parameterName": "AlertUri", + "parameterType": 1 + }, + { + "fieldName": "SystemAlertId", + "parameterName": "SystemAlertId", + "parameterType": 1 + }, + { + "fieldName": "SubscriptionId", + "parameterName": "SubscriptionId", + "parameterType": 1 + }, + { + "fieldName": "ResourceGroup", + "parameterName": "ResourceGroup", + "parameterType": 1 + }, + { + "fieldName": "Location", + "parameterName": "Location", + "parameterType": 1 + } + ], + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Severity", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "contains", + "thresholdValue": "High", + "representation": "redBright", + "text": "{0}{1}" + }, + { + "operator": "contains", + "thresholdValue": "Medium", + "representation": "orange", + "text": "{0}{1}" + }, + { + "operator": "contains", + "thresholdValue": "Low", + "representation": "yellow", + "text": "{0}{1}" + }, + { + "operator": "contains", + "thresholdValue": "Informational ", + "representation": "gray", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": null, + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "SystemAlertId", + "formatter": 5 + }, + { + "columnMatch": "AlertDisplayName", + "formatter": 1, + "formatOptions": { + "linkTarget": "OpenBlade", + "bladeOpenContext": { + "bladeName": "AlertBlade", + "extensionName": "Microsoft_Azure_Security", + "bladeParameters": [ + { + "name": "alertId", + "source": "column", + "value": "SystemAlertId" + }, + { + "name": "subscriptionId", + "source": "column", + "value": "SubscriptionId" + }, + { + "name": "resourceGroup", + "source": "column", + "value": "ResourceGroup" + }, + { + "name": "referencedFrom", + "source": "static", + "value": "activeAlertsWorkbook" + }, + { + "name": "location", + "source": "column", + "value": "Location" + } + ] + } + } + }, + { + "columnMatch": "IsIncident", + "formatter": 1 + }, + { + "columnMatch": "AlertUri", + "formatter": 5 + }, + { + "columnMatch": "Tactics", + "formatter": 1 + }, + { + "columnMatch": "SubscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "Location", + "formatter": 17 + }, + { + "columnMatch": "ResourceId", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "TenantId", + "formatter": 5 + }, + { + "columnMatch": "AlertName", + "formatter": 5 + }, + { + "columnMatch": "Description", + "formatter": 5 + }, + { + "columnMatch": "ProviderName", + "formatter": 5 + }, + { + "columnMatch": "VendorName", + "formatter": 5 + }, + { + "columnMatch": "VendorOriginalId", + "formatter": 5 + }, + { + "columnMatch": "SourceComputerId", + "formatter": 5 + }, + { + "columnMatch": "AlertType", + "formatter": 5 + }, + { + "columnMatch": "ConfidenceLevel", + "formatter": 5 + }, + { + "columnMatch": "ConfidenceScore", + "formatter": 5 + }, + { + "columnMatch": "StartTime", + "formatter": 5 + }, + { + "columnMatch": "EndTime", + "formatter": 5 + }, + { + "columnMatch": "ProcessingEndTime", + "formatter": 5 + }, + { + "columnMatch": "RemediationSteps", + "formatter": 5 + }, + { + "columnMatch": "ExtendedProperties", + "formatter": 5 + }, + { + "columnMatch": "Entities", + "formatter": 5 + }, + { + "columnMatch": "SourceSystem", + "formatter": 5 + }, + { + "columnMatch": "WorkspaceSubscriptionId", + "formatter": 5 + }, + { + "columnMatch": "WorkspaceResourceGroup", + "formatter": 5 + }, + { + "columnMatch": "ExtendedLinks", + "formatter": 5 + }, + { + "columnMatch": "ProductName", + "formatter": 5 + }, + { + "columnMatch": "ProductComponentName", + "formatter": 5 + }, + { + "columnMatch": "AlertLink", + "formatter": 7, + "formatOptions": { + "linkTarget": "Url" + } + }, + { + "columnMatch": "SystemIncidentId", + "formatter": 5 + }, + { + "columnMatch": "SystemAlertId1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "SystemAlertId", + "label": "Alert ID" + }, + { + "columnId": "AlertDisplayName", + "label": "Alert name" + }, + { + "columnId": "IsIncident", + "label": "Incident/alert" + }, + { + "columnId": "SeverityRank", + "label": "Severity" + }, + { + "columnId": "SubscriptionId", + "label": "Subscription" + }, + { + "columnId": "ResourceGroup", + "label": "Resource group" + }, + { + "columnId": "ResourceId", + "label": "Resource" + } + ] + }, + "sortBy": [] + }, + "conditionalVisibility": { + "parameterName": "ShowTable", + "comparison": "isNotEqualTo" + }, + "showPin": true, + "name": "SecurityIncidents - FilterbyResourceId", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "list", + "links": [ + { + "id": "2f6ff56b-9afb-46f6-968d-a59cb744ea14", + "linkTarget": "OpenBlade", + "linkLabel": "Open Alert View", + "style": "primary", + "bladeOpenContext": { + "bladeName": "AlertBlade", + "extensionName": "Microsoft_Azure_Security", + "bladeParameters": [ + { + "name": "alertId", + "source": "static", + "value": "{SystemAlertId}" + }, + { + "name": "subscriptionId", + "source": "static", + "value": "{SubscriptionId}" + }, + { + "name": "resourceGroup", + "source": "static", + "value": "{ResourceGroup}" + }, + { + "name": "referencedFrom", + "source": "static", + "value": "activeAlertsWorkbook" + }, + { + "name": "location", + "source": "static", + "value": "{Location}" + } + ] + } + } + ] + }, + "conditionalVisibility": { + "parameterName": "SystemAlertId", + "comparison": "isNotEqualTo" + }, + "name": "Alerts " + }, + { + "type": 1, + "content": { + "json": "### MITRE ATT&CK tactics                                 " + }, + "customWidth": "100", + "name": "text - 17" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources\r\n| where type == \"microsoft.security/locations/alerts\"\r\n| extend Details = parse_json(properties)\r\n| extend Tactics = Details.[\"Intent\"]\r\n| project Tactics\r\n| extend Tactic = split(Tactics,\",\")\r\n| mv-expand Tactic\r\n| extend Tactic = trim(\" \",tostring(Tactic))\r\n| summarize Count = count() by Tactic\r\n| sort by Count desc\r\n", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "barchart", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "Tactics", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "Tactics", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "count_", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "mapSettings": { + "locInfo": "LatLong", + "sizeSettings": "count_", + "sizeAggregation": "Sum", + "legendMetric": "count_", + "legendAggregation": "Sum", + "itemColorSettings": { + "type": "heatmap", + "colorAggregation": "Sum", + "nodeColorField": "count_", + "heatmapPalette": "greenRed" + } + } + }, + "name": "query - 17" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "bd374a50-b240-4232-ad4a-77725f80bcf5", + "cellValue": "View", + "linkTarget": "parameter", + "linkLabel": "List View", + "subTarget": "List", + "preText": "", + "style": "link" + }, + { + "id": "588b7d9f-8ff1-4afa-8d3f-b0085ae6b148", + "cellValue": "View", + "linkTarget": "parameter", + "linkLabel": "Map View", + "subTarget": "Map", + "preText": "", + "style": "link" + } + ] + }, + "name": "links - 10" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "1ffc8fe9-a919-4c9e-8489-a92f0a7d79e1", + "version": "KqlParameterItem/1.0", + "name": "ResourceFilter", + "label": "Resource", + "type": 5, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | extend Prop = parse_json(properties)\r\n | extend ResourceIdentifiers = Prop.[\"ResourceIdentifiers\"]\r\n | project ResourceIdentifiers\r\n | mv-expand ResourceIdentifiers\r\n | extend ResourceId = parse_json(ResourceIdentifiers).[\"AzureResourceId\"]\r\n //| where isnotempty(ResourceId )\r\n | extend Resource = tolower(tostring(ResourceId))\r\n | summarize count() by Resource\r\n | project Resource\r\n //| order by Resource asc\r\n", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": [ + "value::all" + ] + }, + { + "id": "e9522d87-143f-408b-93ea-b8f07223995e", + "version": "KqlParameterItem/1.0", + "name": "SeverityFilter", + "label": "Severity", + "type": 2, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "value": [ + "value::all" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "jsonData": "[\r\n\r\n{\"value\": \"High\", \"label\":\"High\"},\r\n{\"value\": \"Medium\", \"label\":\"Medium\"},\r\n{\"value\": \"Low\", \"label\":\"Low\"},\r\n{\"value\": \"Informational\", \"label\":\"Informational\"}\r\n]\r\n \r\n ", + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all" + }, + { + "id": "664365b5-1fc4-4cfa-b99d-a72e3d35ab11", + "version": "KqlParameterItem/1.0", + "name": "ResourceGroupFilter", + "label": "Resource group", + "type": 2, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": " securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | extend resourceGroup = iif(isempty(resourceGroup),\" \",resourceGroup)\r\n| summarize Count =count() by resourceGroup\r\n | project resourceGroup", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": [ + "value::all" + ] + }, + { + "id": "48a8dd7e-43ab-413e-88f8-a433100d92ce", + "version": "KqlParameterItem/1.0", + "name": "AlertNameFilter", + "label": "Alert name", + "type": 2, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": " securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | extend Prop = parse_json(properties)\r\n | extend AlertDisplayName = Prop.[\"AlertDisplayName\"]\r\n | distinct tostring(AlertDisplayName)\r\n | order by AlertDisplayName asc\r\n ", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + { + "id": "378aeb0c-9135-43fa-b46a-86f71baa0137", + "version": "KqlParameterItem/1.0", + "name": "TagFilter", + "label": "Tag", + "type": 2, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "securityresources\r\n | where type =~ \"microsoft.security/locations/alerts\"\r\n | extend isAzure = tostring(properties.ResourceIdentifiers) matches regex '\"Type\"\\\\s*:\\\\s*\"AzureResource\"'\r\n | extend Details = parse_json(properties)\r\n| extend IsIncident = Details.[\"IsIncident\"]\r\n| extend AlertDisplayName = Details.[\"AlertDisplayName\"]\r\n| extend SystemAlertId = Details.[\"SystemAlertId\"]\r\n| extend Severity = Details.[\"Severity\"]\r\n| extend AlertUri = Details.[\"AlertUri\"]\r\n| extend Status = Details.[\"Status\"]\r\n| extend Tactics = Details.[\"Intent\"]\r\n| parse AlertUri with * '/subscriptionId/' SubscriptionId '/' *\r\n| parse AlertUri with * '/resourceGroup/' ResourceGroup '/' *\r\n| parse AlertUri with * '/location/' Location \r\n | extend affectedResourceId = extract('\"AzureResourceId\"\\\\s*:\\\\s*\"([^\"]*)\"', 1, tostring(properties.ResourceIdentifiers))\r\n | extend hostName = iff(isAzure, \"\", extract('\"HostName\"\\\\s*:\\\\s*\"([^\"]*)\"', 1, tostring(properties.Entities)))\r\n | extend splitAffectedResourceId = split(affectedResourceId, \"/\")\r\n | extend resourceNameIndex = iff(array_length(splitAffectedResourceId) > 1, array_length(splitAffectedResourceId) - 1, 0)\r\n | extend affectedResourceName = iff(isAzure, splitAffectedResourceId[resourceNameIndex], iff(isempty(hostName), \"Non-Azure\", hostName))| project-away resourceNameIndex, splitAffectedResourceId, hostName\r\n | extend ResourceIdentifiers = Details.[\"ResourceIdentifiers\"]\r\n | mv-expand ResourceIdentifiers\r\n | extend ResourceId = parse_json(ResourceIdentifiers).[\"AzureResourceId\"]\r\n | extend Resource = tolower(tostring(ResourceId))\r\n | project alertId = id, subscriptionId, alertProperties = properties, affectedResourceId = tolower(affectedResourceId),tostring(Severity), SystemAlertId, AlertDisplayName,IsIncident = iif(IsIncident==\"true\",\"Incident\",\"Alert\"),AlertUri,Status,Tactics,SubscriptionId,ResourceGroup,Location, ResourceIdentifier=Details.[\"ResourceIdentifiers\"],Resource\r\n | join kind=leftouter (\r\n resources\r\n | project id = tolower(id), tags\r\n ) on $left.affectedResourceId == $right.id\r\n | extend Tag = parse_json(tags)\r\n | mv-expand Tag\r\n | parse Tag with * ':\"' TagValue '\"}'\r\n | extend TagValue = iif(isempty(TagValue),\" \",TagValue)\r\n | project TagValue, alertId\r\n | distinct TagValue\r\n ", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "conditionalVisibility": { + "parameterName": "View", + "comparison": "isEqualTo", + "value": "List" + }, + "name": "parameters - 23" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources\r\n| where type =~ \"microsoft.security/locations/alerts\"\r\n| extend isAzure = tostring(properties.ResourceIdentifiers) matches regex '\"Type\"\\\\s*:\\\\s*\"AzureResource\"'\r\n| extend Details = parse_json(properties)\r\n| extend IsIncident = Details.[\"IsIncident\"]\r\n| extend AlertDisplayName = Details.[\"AlertDisplayName\"]\r\n| extend SystemAlertId = Details.[\"SystemAlertId\"]\r\n| extend Severity = Details.[\"Severity\"]\r\n| extend AlertUri = Details.[\"AlertUri\"]\r\n| extend Status = Details.[\"Status\"]\r\n| extend Tactics = Details.[\"Intent\"]\r\n| parse AlertUri with * '/subscriptionId/' SubscriptionId '/' *\r\n| parse AlertUri with * '/resourceGroup/' ResourceGroup '/' *\r\n| parse AlertUri with * '/location/' Location \r\n| extend affectedResourceId = extract('\"AzureResourceId\"\\\\s*:\\\\s*\"([^\"]*)\"', 1, tostring(properties.ResourceIdentifiers))\r\n| extend hostName = iff(isAzure, \"\", extract('\"HostName\"\\\\s*:\\\\s*\"([^\"]*)\"', 1, tostring(properties.Entities)))\r\n| extend splitAffectedResourceId = split(affectedResourceId, \"/\")\r\n| extend resourceNameIndex = iff(array_length(splitAffectedResourceId) > 1, array_length(splitAffectedResourceId) - 1, 0)\r\n| extend affectedResourceName = iff(isAzure, splitAffectedResourceId[resourceNameIndex], iff(isempty(hostName), \"Non-Azure\", hostName))\r\n| project-away resourceNameIndex, splitAffectedResourceId, hostName\r\n| extend ResourceIdentifiers = Details.[\"ResourceIdentifiers\"]\r\n| mv-expand ResourceIdentifiers\r\n| extend ResourceId = parse_json(ResourceIdentifiers).[\"AzureResourceId\"]\r\n| extend Resource = tolower(tostring(ResourceId))\r\n| project\r\n alertId = id,\r\n subscriptionId,\r\n alertProperties = properties,\r\n affectedResourceId = tolower(affectedResourceId),\r\n tostring(Severity),\r\n SystemAlertId,\r\n AlertDisplayName,\r\n IsIncident = iif(IsIncident == \"true\", \"Incident\", \"Alert\"),\r\n AlertUri,\r\n Status,\r\n Tactics,\r\n SubscriptionId,\r\n ResourceGroup,\r\n Location,\r\n ResourceId,\r\n ResourceIdentifier=Details.[\"ResourceIdentifiers\"],\r\n Resource\r\n| join kind=leftouter (\r\n resources\r\n | project id = tolower(id), tags\r\n )\r\n on $left.affectedResourceId == $right.id\r\n| extend id = alertId, subscriptionId, properties = alertProperties\r\n| extend ResourceFilter =\" {ResourceFilter}\"\r\n| where Resource in~ ({ResourceFilter})\r\n| where Severity in~ ({SeverityFilter})\r\n| where AlertDisplayName in~ ({AlertNameFilter})\r\n| where Status == \"Active\"\r\n| extend ResourceGroup = iif(isempty(ResourceGroup), \" \", ResourceGroup)\r\n| where ResourceGroup in~ ({ResourceGroupFilter})\r\n| extend tag = iff(isempty(tags), dynamic({\"tags\": \" \"}), parse_json(tags))\r\n| mv-expand tag\r\n| parse tag with * ':\"' TagValue '\"}'\r\n| extend TagValue = iif(isempty(TagValue), \" \", TagValue)\r\n| where TagValue in ({TagFilter})\r\n| where AlertDisplayName !startswith ('[SAMPLE ALERT]')\r\n| project\r\n (Severity),\r\n tostring(SystemAlertId),\r\n tostring(AlertDisplayName),\r\n IsIncident = iif(IsIncident == \"true\", \"Incident\", \"Alert\"),\r\n AlertURI = tostring(AlertUri),\r\n tostring(Status),\r\n tostring(Tactics),\r\n SubscriptionId,\r\n ResourceGroup,\r\n Location,\r\n TagValue,\r\n tostring(tags),\r\n tostring(ResourceId)\r\n| distinct\r\n Severity,\r\n SystemAlertId,\r\n AlertDisplayName,\r\n IsIncident,\r\n AlertURI,\r\n Status,\r\n Tactics,\r\n SubscriptionId,\r\n ResourceGroup,\r\n Location,\r\n tags,\r\n ResourceId\r\n| order by Severity asc", + "size": 0, + "title": "Active Alerts ", + "exportedParameters": [ + { + "fieldName": "Resource", + "parameterName": "Resource", + "parameterType": 1 + }, + { + "fieldName": "AlertUri", + "parameterName": "AlertUri", + "parameterType": 1 + }, + { + "fieldName": "SystemAlertId", + "parameterName": "SystemAlertId", + "parameterType": 1 + }, + { + "fieldName": "SubscriptionId", + "parameterName": "SubscriptionId", + "parameterType": 1 + }, + { + "fieldName": "ResourceGroup", + "parameterName": "ResourceGroup", + "parameterType": 1 + }, + { + "fieldName": "Location", + "parameterName": "Location", + "parameterType": 1 + } + ], + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Severity", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "contains", + "thresholdValue": "High", + "representation": "redBright", + "text": "{0}{1}" + }, + { + "operator": "contains", + "thresholdValue": "Medium", + "representation": "orange", + "text": "{0}{1}" + }, + { + "operator": "contains", + "thresholdValue": "Low", + "representation": "yellow", + "text": "{0}{1}" + }, + { + "operator": "contains", + "thresholdValue": "Informational ", + "representation": "gray", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": null, + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "SystemAlertId", + "formatter": 5 + }, + { + "columnMatch": "IsIncident", + "formatter": 1 + }, + { + "columnMatch": "AlertURI", + "formatter": 5, + "formatOptions": { + "linkTarget": "Url" + } + }, + { + "columnMatch": "Status", + "formatter": 5 + }, + { + "columnMatch": "SubscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "Location", + "formatter": 5 + }, + { + "columnMatch": "ResourceId", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "Alert ID", + "formatter": 5 + }, + { + "columnMatch": "Alert URI", + "formatter": 5, + "formatOptions": { + "linkTarget": "Url" + } + }, + { + "columnMatch": "Resource ID", + "formatter": 5 + }, + { + "columnMatch": "AlertUri", + "formatter": 5 + }, + { + "columnMatch": "ResourceIdentifier", + "formatter": 5 + }, + { + "columnMatch": "TenantId", + "formatter": 5 + }, + { + "columnMatch": "AlertName", + "formatter": 5 + }, + { + "columnMatch": "Description", + "formatter": 5 + }, + { + "columnMatch": "ProviderName", + "formatter": 5 + }, + { + "columnMatch": "VendorName", + "formatter": 5 + }, + { + "columnMatch": "VendorOriginalId", + "formatter": 5 + }, + { + "columnMatch": "SourceComputerId", + "formatter": 5 + }, + { + "columnMatch": "AlertType", + "formatter": 5 + }, + { + "columnMatch": "ConfidenceLevel", + "formatter": 5 + }, + { + "columnMatch": "ConfidenceScore", + "formatter": 5 + }, + { + "columnMatch": "StartTime", + "formatter": 5 + }, + { + "columnMatch": "EndTime", + "formatter": 5 + }, + { + "columnMatch": "ProcessingEndTime", + "formatter": 5 + }, + { + "columnMatch": "RemediationSteps", + "formatter": 5 + }, + { + "columnMatch": "ExtendedProperties", + "formatter": 5 + }, + { + "columnMatch": "Entities", + "formatter": 5 + }, + { + "columnMatch": "SourceSystem", + "formatter": 5 + }, + { + "columnMatch": "WorkspaceSubscriptionId", + "formatter": 5 + }, + { + "columnMatch": "WorkspaceResourceGroup", + "formatter": 5 + }, + { + "columnMatch": "ExtendedLinks", + "formatter": 5 + }, + { + "columnMatch": "ProductName", + "formatter": 5 + }, + { + "columnMatch": "ProductComponentName", + "formatter": 5 + }, + { + "columnMatch": "AlertLink", + "formatter": 7, + "formatOptions": { + "linkTarget": "Url" + } + }, + { + "columnMatch": "SystemIncidentId", + "formatter": 5 + }, + { + "columnMatch": "SystemAlertId1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "AlertDisplayName", + "label": "Alert name" + }, + { + "columnId": "IsIncident", + "label": "Incident/alert" + }, + { + "columnId": "AlertURI", + "label": "Alert URI" + }, + { + "columnId": "SubscriptionId", + "label": "Subscription" + }, + { + "columnId": "ResourceGroup", + "label": "Resource group" + }, + { + "columnId": "tags", + "label": "Tags" + }, + { + "columnId": "ResourceId", + "label": "Resource" + } + ] + }, + "sortBy": [] + }, + "conditionalVisibility": { + "parameterName": "View", + "comparison": "isEqualTo", + "value": "List" + }, + "showPin": true, + "name": "SecurityIncidents" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "list", + "links": [ + { + "id": "8e6f9368-ccbe-4092-b898-8a27c77a06b3", + "linkTarget": "OpenBlade", + "linkLabel": "Open Alert View", + "preText": "", + "style": "primary", + "bladeOpenContext": { + "bladeName": "AlertBlade", + "extensionName": "Microsoft_Azure_Security", + "bladeParameters": [ + { + "name": "alertId", + "source": "static", + "value": "{SystemAlertId}" + }, + { + "name": "subscriptionId", + "source": "static", + "value": "{SubscriptionId}" + }, + { + "name": "resourceGroup", + "source": "static", + "value": "{ResourceGroup}" + }, + { + "name": "referencedFrom", + "source": "static", + "value": "activeAlertsWorkbook" + }, + { + "name": "location", + "source": "static", + "value": "{Location}" + } + ] + } + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "View", + "comparison": "isEqualTo", + "value": "List" + }, + { + "parameterName": "SystemAlertId", + "comparison": "isNotEqualTo" + } + ], + "name": "links - 19" + }, + { + "type": 1, + "content": { + "json": " To see more information about the alerts in the map view:

  1. Configure continuous export to export your security alerts to a Log Analytics workspace by following the instructions described \r\n
[ here. ](https://docs.microsoft.com/azure/defender-for-cloud/continuous-export?tabs=azure-portal)\r\n
  2. In the \"Workspace\" filter below, choose the Log Analytics workspace your security alerts are exported to.\r\n\r\n" + }, + "conditionalVisibility": { + "parameterName": "View", + "comparison": "isEqualTo", + "value": "Map" + }, + "name": "text - 21" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "8724f927-b766-4814-a895-8c55565fb7f8", + "version": "KqlParameterItem/1.0", + "name": "Workspace", + "type": 5, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "resources\r\n| where type contains \"solution\"\r\n| where name contains \"security\"\r\n| project id = tostring(properties.workspaceResourceId)\r\n| distinct id", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "conditionalVisibility": { + "parameterName": "View", + "comparison": "isEqualTo", + "value": "Map" + }, + "name": "parameters - 15" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources\r\n| where type == \"microsoft.security/locations/alerts\"\r\n| project-rename P= properties\r\n| extend Details = parse_json(P)\r\n | extend ResourceIdentifiers = Details.[\"ResourceIdentifiers\"]\r\n| extend IsIncident = Details.[\"IsIncident\"]\r\n| extend AlertDisplayName = Details.[\"AlertDisplayName\"]\r\n| extend SystemAlertId = Details.[\"SystemAlertId\"]\r\n| extend Severity = Details.[\"Severity\"]\r\n| extend AlertLink = Details.[\"AlertUri\"]\r\n| extend Status = Details.[\"Status\"]\r\n| extend Tactics = Details.[\"Intent\"]\r\n| parse AlertLink with * '/subscriptionId/' SubscriptionId '/' *\r\n| parse AlertLink with * '/resourceGroup/' ResourceGroup '/' *\r\n| parse AlertLink with * '/location/' Location \r\n | mv-expand ResourceIdentifiers\r\n | extend ResourceId = parse_json(ResourceIdentifiers).[\"AzureResourceId\"]\r\n | where isnotempty(ResourceId )\r\n| project Severity, SystemAlertId, tostring(AlertDisplayName),IsIncident = iif(IsIncident==\"true\",\"Incident\",\"Alert\"),tostring(AlertLink),Status,Tactics,tostring(ResourceId),SubscriptionId,ResourceGroup,Location\r\n| distinct tostring(SystemAlertId),tostring(AlertDisplayName),tostring(AlertLink),tostring(ResourceId)\r\n| summarize count() by ResourceId, AlertLink, AlertDisplayName\r\n", + "size": 0, + "title": "AlertsMapView ", + "exportMultipleValues": true, + "exportAggregateParts": true, + "exportedParameters": [ + { + "fieldName": "ResourceId", + "parameterName": "Resource", + "parameterType": 1 + }, + { + "fieldName": "AlertLink", + "parameterName": "AlertLink", + "parameterType": 1 + }, + { + "fieldName": "AlertDisplayName", + "parameterName": "AlertDisplayName", + "parameterType": 1 + } + ], + "exportToExcelOptions": "all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "map", + "mapSettings": { + "locInfo": "AzureResource", + "locInfoColumn": "ResourceId", + "sizeSettings": "count_", + "sizeAggregation": "Sum", + "legendMetric": "count_", + "legendAggregation": "Sum", + "itemColorSettings": { + "nodeColorField": "count_", + "colorAggregation": "Sum", + "type": "heatmap", + "heatmapPalette": "coldHot" + } + } + }, + "customWidth": "50", + "conditionalVisibility": { + "parameterName": "View", + "comparison": "isEqualTo", + "value": "Map" + }, + "name": "AlertsMapView ", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "let T = datatable ( AlertLink:string)\r\n[\r\n{AlertLink}\r\n];\r\nT\r\n| parse AlertLink with * '/alertId/' AlertId '/subscriptionId/' SubscriptionId '/resourceGroup/' ResourceGroup '/' * 'location/' Location \r\n| distinct AlertLink, AlertId, ResourceGroup,Location,SubscriptionId\r\n| join kind = inner (SecurityAlert\r\n| where isempty(ResourceId) == false\r\n| where TimeGenerated > ago(90d)\r\n| project SystemAlertId,ResourceId, DisplayName,StartTime) on $left.AlertId == $right.SystemAlertId\r\n| project ResourceId,DisplayName,AlertId, SubscriptionId, ResourceGroup, Location,StartTime\r\n| order by ResourceId,DisplayName, StartTime asc\r\n\r\n\r\n\r\n", + "size": 0, + "exportedParameters": [ + { + "fieldName": "AlertId", + "parameterName": "AlertId", + "parameterType": 1 + }, + { + "fieldName": "SubscriptionId", + "parameterName": "SubscriptionId", + "parameterType": 1 + }, + { + "fieldName": "Location", + "parameterName": "Location", + "parameterType": 1 + }, + { + "fieldName": "ResourceGroup", + "parameterName": "ResourceGroup", + "parameterType": 1 + } + ], + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "crossComponentResources": [ + "{Workspace}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "ResourceId", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "23ch" + } + }, + { + "columnMatch": "DisplayName", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "45ch" + } + }, + { + "columnMatch": "AlertId", + "formatter": 5 + }, + { + "columnMatch": "Location", + "formatter": 5 + }, + { + "columnMatch": "StartTime", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "23ch" + } + }, + { + "columnMatch": "TimeGenerated", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "22ch" + } + }, + { + "columnMatch": "AlertLink", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "ResourceId", + "label": "Resource ID" + }, + { + "columnId": "DisplayName", + "label": "Alert name" + }, + { + "columnId": "AlertId", + "label": "Alert ID" + }, + { + "columnId": "SubscriptionId", + "label": "Subscription ID" + }, + { + "columnId": "ResourceGroup", + "label": "Resource group" + }, + { + "columnId": "StartTime", + "label": "Start time" + } + ] + }, + "sortBy": [], + "tileSettings": { + "showBorder": false + } + }, + "customWidth": "45", + "conditionalVisibilities": [ + { + "parameterName": "View", + "comparison": "isEqualTo", + "value": "Map" + }, + { + "parameterName": "AlertLink", + "comparison": "isNotEqualTo" + } + ], + "name": "AlertLink-Table" + } + ] + }, + "name": "Security Discipline" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Security" + }, + { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + } + ], + "name": "RC_Security" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "1e6e4cc7-5d76-48ef-8ce1-16f33f4f6dea", + "version": "KqlParameterItem/1.0", + "name": "SubscriptionAge", + "label": "Subscription name", + "type": 6, + "typeSettings": { + "additionalResourceOptions": [], + "includeAll": true, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "value": null + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters - SubscriptionAge" + }, + { + "type": 1, + "content": { + "json": "## Azure resource age\r\nAzure *resource age* is one of the metric to monitor as part of the \"resource consistency\" discipline of the Cloud Adoption Framework. This metric help you to identify old resources to be assessed and cleaned if they are not used anymore." + }, + "name": "text - ResourceAgeDescription" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadFromTemplateId": "community-Workbooks/Common/noSubscriptions", + "items": [] + }, + "conditionalVisibility": { + "parameterName": "SubscriptionAge", + "comparison": "isEqualTo", + "value": "" + }, + "name": "No Subscriptions group - Age" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":null,\"headers\":[],\"method\":\"GET\",\"path\":\"/subscriptions/{SubscriptionAge:id}/resources?\",\"urlParams\":[{\"key\":\"api-version\",\"value\":\"2021-04-01\"},{\"key\":\"$expand\",\"value\":\"createdTime,changedTime,provisioningState\"}],\"batchDisabled\":true,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.value\",\"columns\":[{\"path\":\"$..id\",\"columnid\":\"id\",\"columnType\":\"string\"},{\"path\":\"$..type\",\"columnid\":\"type\",\"columnType\":\"string\"},{\"path\":\"$..location\",\"columnid\":\"location\",\"columnType\":\"string\"},{\"path\":\"$..createdTime\",\"columnid\":\"createdTime\",\"columnType\":\"datetime\"},{\"path\":\"$..changedTime\",\"columnid\":\"changedTime\",\"columnType\":\"datetime\"},{\"path\":\"$..provisioningState\",\"columnid\":\"provisioningState\",\"columnType\":\"string\"},{\"path\":\"$..tags\",\"columnid\":\"tags\"}]}}]}", + "size": 0, + "title": "Resource age", + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "formatters": [ + { + "columnMatch": "type", + "formatter": 16, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "location", + "formatter": 17 + }, + { + "columnMatch": "createdTime", + "formatter": 6 + }, + { + "columnMatch": "changedTime", + "formatter": 6 + }, + { + "columnMatch": "provisioningState", + "formatter": 1 + }, + { + "columnMatch": "tags", + "formatter": 1 + } + ], + "filter": true, + "sortBy": [ + { + "itemKey": "changedTime", + "sortOrder": 1 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource Name" + }, + { + "columnId": "type", + "label": "Resource Type" + }, + { + "columnId": "location", + "label": "Region" + }, + { + "columnId": "createdTime", + "label": "Created Time" + }, + { + "columnId": "changedTime", + "label": "Last Change" + }, + { + "columnId": "provisioningState", + "label": "Provisioning State" + }, + { + "columnId": "tags", + "label": "Tags" + } + ] + }, + "sortBy": [ + { + "itemKey": "changedTime", + "sortOrder": 1 + } + ] + }, + "conditionalVisibility": { + "parameterName": "SubscriptionAge", + "comparison": "isNotEqualTo" + }, + "name": "query - Resource age" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Age" + }, + "name": "Resource Age" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "e15ef842-dadb-4a7b-b5f6-5d1bbe35b7af", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "description": "Cost information can only be displayed per subscription", + "typeSettings": { + "additionalResourceOptions": [], + "includeAll": true, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "value": null + }, + { + "id": "b73ef334-95b2-4ead-8dd2-51a90a90ce6f", + "version": "KqlParameterItem/1.0", + "name": "Aggregation", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n { \"value\": \"SubscriptionId\", \"label\": \"Subscription\", \"selected\":true},\r\n { \"value\": \"ResourceGroup\", \"label\": \"Resource Group\"},\r\n { \"value\": \"ResourceType\", \"label\": \"Resource Type\"}\r\n]", + "timeContext": { + "durationMs": 86400000 + } + }, + { + "id": "55ef4a45-0603-48cf-bb9b-a963e7a33be2", + "version": "KqlParameterItem/1.0", + "name": "TimeFrame", + "type": 2, + "typeSettings": { + "additionalResourceOptions": [] + }, + "jsonData": "[[\r\n { \"value\": \"BillingMonthToDate\", \"label\": \"Billing MonthToDate\"},\r\n { \"value\": \"MonthToDate\", \"label\": \"MonthToDate\", \"selected\":true },\r\n { \"value\": \"TheLastBillingMonth\", \"label\": \"Last Billing Month\"},\r\n { \"value\": \"TheLastMonth\", \"label\": \"Last Month\"},\r\n { \"value\": \"WeekToDate\", \"label\": \"WeekToDate\"}\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "label": "Timeframe" + } + ], + "style": "above", + "doNotRunWhenHidden": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "parameters - Cost Subscription" + }, + { + "type": 1, + "content": { + "json": "## Microsoft Cost Management\r\n\r\nBefore you can control and optimize your costs, you first need to understand where they originated – from the underlying resources used to support your cloud projects to the environments they're deployed in and the owners who manage them. Full visibility backed by a thorough tagging strategy is critical to accurately understand your spending patterns and enforce cost control mechanisms.\r\n\r\n[Cost Management](https://portal.azure.com/#view/Microsoft_Azure_CostManagement/Menu) is a set of FinOps tools that enable you to analyze, manage, and optimize your costs.\r\n\r\nCalculate your estimated hourly or monthly costs for using Azure with the [Azure Calculator](https://azure.microsoft.com/pricing/calculator/).\r\n\r\nFor more advanced reporting options, build custom [Power BI reports in the FinOps toolkit](https://aka.ms/ftk/pbi)." + }, + "name": "text - AzureCostManagement" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":\" {\\r\\n \\\"type\\\": \\\"Usage\\\",\\r\\n \\\"timeframe\\\": \\\"{TimeFrame}\\\",\\r\\n \\\"dataset\\\": {\\r\\n \\\"granularity\\\": \\\"None\\\",\\r\\n \\\"aggregation\\\": {\\r\\n \\\"totalCost\\\": {\\r\\n \\\"name\\\": \\\"PreTaxCost\\\",\\r\\n \\\"function\\\": \\\"Sum\\\"\\r\\n }\\r\\n },\\r\\n \\\"grouping\\\": [\\r\\n {\\r\\n \\\"type\\\": \\\"Dimension\\\",\\r\\n \\\"name\\\": \\\"{Aggregation}\\\"\\r\\n }\\r\\n ]\\r\\n }\\r\\n }\",\"headers\":[],\"method\":\"POST\",\"path\":\"/subscriptions/{Subscription:id}/providers/Microsoft.CostManagement/query?\",\"urlParams\":[{\"key\":\"api-version\",\"value\":\"2023-11-01\"}],\"batchDisabled\":true,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.properties\",\"columns\":[]}}]}", + "size": 0, + "title": "Overall cost", + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "formatters": [ + { + "columnMatch": "PreTaxCost", + "formatter": 0, + "numberFormat": { + "unit": 0, + "options": { + "style": "decimal", + "useGrouping": true, + "maximumFractionDigits": 2 + }, + "emptyValCustomText": "\"0\"" + } + }, + { + "columnMatch": "SubscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "ResourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": false + } + }, + { + "columnMatch": "ResourceType", + "formatter": 16, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": "Resource", + "linkIsContextBlade": false, + "showIcon": false + } + } + ], + "filter": true, + "sortBy": [ + { + "itemKey": "$gen_number_PreTaxCost_0", + "sortOrder": 2 + } + ], + "labelSettings": [ + { + "columnId": "PreTaxCost", + "label": "Cost" + }, + { + "columnId": "Currency", + "label": "Currency" + } + ] + }, + "sortBy": [ + { + "itemKey": "$gen_number_PreTaxCost_0", + "sortOrder": 2 + } + ], + "tileSettings": { + "showBorder": false + } + }, + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "query - Overall cost" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "6cc7fc26-1a56-41cb-ad43-301e0f9f8903", + "version": "KqlParameterItem/1.0", + "name": "TagName", + "label": "Tag name", + "type": 2, + "isRequired": true, + "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null + }, + { + "id": "2fc46f5d-ce69-42ea-8ebf-1c3d69c4e780", + "version": "KqlParameterItem/1.0", + "name": "TagValue", + "label": "Tag value", + "type": 2, + "isRequired": true, + "query": "Resources\r\n| extend TagValue = tostring(tags.{TagName})\r\n| project TagValue\r\n| distinct TagValue", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": "" + } + ], + "style": "above", + "doNotRunWhenHidden": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "parameters - TagFilter" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":\" {\\r\\n \\\"type\\\": \\\"Usage\\\",\\r\\n \\\"timeframe\\\": \\\"{TimeFrame}\\\",\\r\\n \\\"dataset\\\": {\\r\\n \\\"granularity\\\": \\\"None\\\",\\r\\n \\\"filter\\\": {\\r\\n \\\"tags\\\" : {\\r\\n \\\"name\\\" : \\\"{TagName}\\\",\\r\\n \\\"operator\\\" : \\\"In\\\",\\r\\n \\\"values\\\" : [\\r\\n \\\"{TagValue}\\\"\\r\\n ]\\r\\n }\\r\\n },\\r\\n \\\"aggregation\\\": {\\r\\n \\\"totalCost\\\": {\\r\\n \\\"name\\\": \\\"PreTaxCost\\\",\\r\\n \\\"function\\\": \\\"Sum\\\"\\r\\n }\\r\\n },\\r\\n \\\"grouping\\\": [\\r\\n {\\r\\n \\\"type\\\": \\\"Dimension\\\",\\r\\n \\\"name\\\": \\\"{Aggregation}\\\"\\r\\n }\\r\\n ]\\r\\n }\\r\\n }\",\"headers\":[],\"method\":\"POST\",\"path\":\"/subscriptions/{Subscription:id}/providers/Microsoft.CostManagement/query?\",\"urlParams\":[{\"key\":\"api-version\",\"value\":\"2023-11-01\"}],\"batchDisabled\":true,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.properties\",\"columns\":[]}}]}", + "size": 3, + "title": "Overall cost filtered by tag", + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "formatters": [ + { + "columnMatch": "PreTaxCost", + "formatter": 0, + "numberFormat": { + "unit": 0, + "options": { + "style": "decimal", + "useGrouping": true, + "maximumFractionDigits": 2 + }, + "emptyValCustomText": "\"0\"" + } + }, + { + "columnMatch": "SubscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "ResourceType", + "formatter": 16, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": "Resource", + "linkIsContextBlade": false, + "showIcon": true + } + } + ], + "labelSettings": [ + { + "columnId": "PreTaxCost", + "label": "Cost" + } + ] + }, + "tileSettings": { + "showBorder": false + } + }, + "conditionalVisibility": { + "parameterName": "TagValue", + "comparison": "isNotEqualTo", + "value": "" + }, + "name": "query - Sub cost per tag" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Cost" + }, + "name": "RC_Cost Management" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "loadType": "always", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "value::tenant" + ], + "parameters": [ + { + "id": "f967854d-c31e-46a6-b8b0-6b9fff7ce582", + "version": "KqlParameterItem/1.0", + "name": "ManagementGroup", + "label": "Management group", + "type": 2, + "query": "resourcecontainers\r\n| where type == \"microsoft.management/managementgroups\"\r\n| extend name = properties.displayName\r\n| project name", + "crossComponentResources": [ + "value::tenant" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resources/tenants", + "value": null + }, + { + "id": "476f61f4-2271-4e58-9b5e-7958d9a4ca3b", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "query": "resourcecontainers\r\n| where type == \"microsoft.resources/subscriptions\"\r\n| where properties['managementGroupAncestorsChain'] contains '{ManagementGroup:label}'", + "crossComponentResources": [ + "value::tenant" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resources/tenants", + "value": null + } + ], + "style": "above", + "doNotRunWhenHidden": true, + "queryType": 1, + "resourceType": "microsoft.resources/tenants" + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Compliance" + }, + "name": "parameters - Scope Filter For RC_Compliance" + }, + { + "type": 1, + "content": { + "json": "## Build and scale your applications quickly while maintaining control\r\nTake advantage of built-in and custom policies to set guardrails in your subscriptions. Easily deploy fully governed environments throughout your organization with Azure Blueprints. And, manage costs by gaining insights into your cloud spend so that you get the most from your cloud investments.
\r\n- Enforce and audit your policies for any Azure service
\r\n- Create compliant environments using Azure Blueprints, including resources, policies, and role-access controls
\r\n- Ensure that you’re compliant with external regulations by using built-in compliance controls
\r\n- Monitor cost and encourage accountability across your entire organization" + }, + "name": "text - 16" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "// Breakdown of compliance information for each assignment at subscription/MG/tenant scope\r\n// Gets aggregated compliance and policy definition information for each of the assignments in the selected scope as well as a few additional details, including: policySetDefinition or policyDefinition details for those assignments, the number of policies/groups within the policysetDefinitions listed, number of non-compliant policies within each policySetDefinition and the resource count breakdown per compliance state for those assignments.\r\n// Click the \"Run query\" command above to execute the query and see results.\r\npolicyResources\r\n| where type =~'Microsoft.Authorization/PolicyAssignments'\r\n| project policyAssignmentId = tolower(tostring(id)), policyAssignmentName = name, policyAssignmentDisplayName = tostring(properties.displayName), policyAssignmentScope = tostring(properties.scope), policyAssignmentDefinitionId = tolower(properties.policyDefinitionId), policyAssignmentNotScopes = tolower(properties.notScopes) \r\n| where policyAssignmentScope == \"{Subscription}\"\r\n| join kind=leftouter(\r\n policyResources\r\n | where type =~'Microsoft.Authorization/PolicySetDefinitions' or type =~'Microsoft.Authorization/PolicyDefinitions'\r\n | project definitionId = tolower(id), type, numberOfPolicies = array_length(properties.policyDefinitions), category = tostring(properties.metadata.category), numberOfGroups= array_length(properties.policyDefinitionGroups), mode = tostring(properties.mode)\r\n | extend isRegulatoryInitiative = iff(category =~ 'Regulatory Compliance', true, false)\r\n | extend definitionType = iff(type =~ 'Microsoft.Authorization/PolicysetDefinitions', 'initiative', 'policy')\r\n | extend isRPMode = iff(mode startswith 'Microsoft.', true, false)\r\n | project definitionId, numberOfPolicies, category, numberOfGroups, isRegulatoryInitiative, definitionType, isRPMode\r\n) on $left.policyAssignmentDefinitionId == $right.definitionId\r\n| join kind=leftouter(\r\n policyResources \r\n | where type =~ 'Microsoft.PolicyInsights/PolicyStates'\r\n | extend complianceState = tostring(properties.complianceState)\r\n | extend policyStateResourceId =id, resourceId = tostring(properties.resourceId), policyAssignmentId = tostring(properties.policyAssignmentId), policyDefinitionId = tostring(properties.policyDefinitionId), policySetDefinitionId = tostring(properties.policySetDefinitionId), policyDefinitionReferenceId = tostring(properties.policyDefinitionReferenceId), policyDefinitionAction = tostring(properties.policyDefinitionAction), policyDefinitionGroupNames = iff(isnotnull(properties.policyDefinitionGroupNames), properties.policyDefinitionGroupNames, dynamic([''])), stateWeight = toint(properties.stateWeight)\r\n | summarize max(stateWeight) by resourceId, policyAssignmentId, policySetDefinitionId\r\n | summarize resourceCounts = count() by policyAssignmentId, policySetDefinitionId, max_stateWeight\r\n| extend complianceState = case(\r\nmax_stateWeight == 300, 'noncompliant',\r\nmax_stateWeight == 200, 'compliant',\r\nmax_stateWeight == 100, 'conflict',\r\nmax_stateWeight == 50, 'exempt',\r\nmax_stateWeight == 10, 'unknown',\r\n'notapplicable')\r\n | extend pack = pack('complianceState', complianceState, 'resourceCounts', resourceCounts), numberOfNonCompliantResources = toint(iff(complianceState =~ 'NonCompliant', resourceCounts,0))\r\n | summarize numberOfNonCompliantResources = max(numberOfNonCompliantResources), details = makelist(pack) by policyAssignmentId, policySetDefinitionId\r\n | limit 5000\r\n) on $left.policyAssignmentId == $right.policyAssignmentId\r\n| sort by numberOfNonCompliantResources desc\r\n| project-away policyAssignmentId1", + "size": 0, + "title": "Resource compliance", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resources/tenants", + "crossComponentResources": [ + "value::tenant" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "policyAssignmentId", + "formatter": 5 + }, + { + "columnMatch": "policyAssignmentName", + "formatter": 5 + }, + { + "columnMatch": "policyAssignmentDisplayName", + "formatter": 7, + "formatOptions": { + "linkTarget": "GenericDetails", + "linkIsContextBlade": true + } + }, + { + "columnMatch": "policyAssignmentScope", + "formatter": 5 + }, + { + "columnMatch": "policyAssignmentDefinitionId", + "formatter": 5 + }, + { + "columnMatch": "policyAssignmentNotScopes", + "formatter": 5 + }, + { + "columnMatch": "definitionId", + "formatter": 5 + }, + { + "columnMatch": "numberOfPolicies", + "formatter": 5 + }, + { + "columnMatch": "numberOfGroups", + "formatter": 5 + }, + { + "columnMatch": "isRegulatoryInitiative", + "formatter": 5 + }, + { + "columnMatch": "isRPMode", + "formatter": 5 + }, + { + "columnMatch": "policySetDefinitionId", + "formatter": 5 + }, + { + "columnMatch": "details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ], + "filter": true, + "sortBy": [ + { + "itemKey": "category", + "sortOrder": 2 + } + ], + "labelSettings": [ + { + "columnId": "policyAssignmentId", + "label": "Assignment ID" + }, + { + "columnId": "policyAssignmentName", + "label": "Assignment name" + }, + { + "columnId": "policyAssignmentDisplayName", + "label": "Assignment display name" + }, + { + "columnId": "policyAssignmentScope", + "label": "Assignment scope" + }, + { + "columnId": "policyAssignmentDefinitionId", + "label": "Assignment definition ID" + }, + { + "columnId": "definitionId", + "label": "Definition ID" + }, + { + "columnId": "numberOfPolicies", + "label": "Number of policies" + }, + { + "columnId": "category", + "label": "Category" + }, + { + "columnId": "definitionType", + "label": "Type" + }, + { + "columnId": "numberOfNonCompliantResources", + "label": "Non compliant resources" + }, + { + "columnId": "details", + "label": "Details" + } + ] + }, + "sortBy": [ + { + "itemKey": "category", + "sortOrder": 2 + } + ] + }, + "name": "query - ResourceCompliance" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AzureActivity\r\n| where ActivityStatusValue has \"Failure\"\r\n| summarize AggregatedValue = count() by ResourceProviderValue\r\n| order by AggregatedValue desc", + "size": 3, + "showAnalytics": true, + "title": "Failures by resources", + "timeContext": { + "durationMs": 604800000 + }, + "queryType": 0, + "resourceType": "microsoft.resources/subscriptions", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "ResourceProviderValue", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "AggregatedValue", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "ResourceProviderValue", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "AggregatedValue", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "customWidth": "33", + "name": "query - Failures by resources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AzureActivity\r\n| where ActivityStatusValue has \"Failure\" or ActivityStatusValue has \"Failed\"\r\n| summarize AggregatedValue = count() by OperationNameValue\r\n| order by AggregatedValue desc", + "size": 3, + "showAnalytics": true, + "title": "Failures by operations", + "timeContext": { + "durationMs": 604800000 + }, + "queryType": 0, + "resourceType": "microsoft.resources/subscriptions", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "ResourceProviderValue", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "AggregatedValue", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "ResourceProviderValue", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "AggregatedValue", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "customWidth": "33", + "name": "query - Failures by operations" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AzureActivity\r\n| where ActivityStatusValue has \"Failure\" or ActivityStatusValue has \"Failed\"\r\n| summarize AggregatedValue = count() by CategoryValue\r\n| order by AggregatedValue desc", + "size": 3, + "showAnalytics": true, + "title": "Failures by category", + "timeContext": { + "durationMs": 604800000 + }, + "queryType": 0, + "resourceType": "microsoft.resources/subscriptions", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "ResourceProviderValue", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "AggregatedValue", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "ResourceProviderValue", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "AggregatedValue", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "customWidth": "33", + "name": "query - Failures by category" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AzureActivity\r\n| where ActivityStatusValue has \"Failure\" or ActivityStatusValue has \"Failed\"\r\n| order by CategoryValue\r\n", + "size": 0, + "title": "Failure by category details", + "timeContext": { + "durationMs": 604800000 + }, + "showExportToExcel": true, + "queryType": 0, + "resourceType": "microsoft.resources/subscriptions", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Authorization", + "formatter": 5 + }, + { + "columnMatch": "Authorization_d", + "formatter": 5 + }, + { + "columnMatch": "Claims", + "formatter": 5 + }, + { + "columnMatch": "Claims_d", + "formatter": 5 + }, + { + "columnMatch": "Properties_d", + "formatter": 5 + }, + { + "columnMatch": "_ResourceId", + "formatter": 5 + } + ], + "filter": true + } + }, + "name": "query - Failure by category details" + } + ] + }, + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "group - ComplianceQueries" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Compliance" + }, + "name": "RC_Compliance" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "4168a8b2-a522-4f0d-9575-893d70d9239d", + "version": "KqlParameterItem/1.0", + "name": "RulesCount", + "type": 1, + "description": "Count of the governance rule, when there is no rules, empty state will be shown", + "query": "securityresources\r\n| where type == \"microsoft.security/governancerules\"\r\n| where tostring(properties.isDisabled) == \"false\"\r\n| count", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + } + ], + "style": "above", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "name": "Tabs" + }, + { + "type": 1, + "content": { + "json": "## Security governance in Microsoft Defender for Cloud\r\n\r\n Microsoft Defender for Cloud continuously assesses your hybrid and multi-cloud workloads and provides you with recommendations to harden your assets and enhance your security posture.
Central security teams often experience challenges when driving the personnel within their organizations to implement recommendations. The organizations' security posture can suffer as a result.
\r\nWe're introducing a brand-new, built-in governance experience to set ownership and expected remediation timeframes to resolve recommendations.\r\n\r\nTo use this governance report, you need to create security governance rules.\r\n
[Learn more >](https://aka.ms/GovernanceDocumentation)\r\n" + }, + "conditionalVisibility": { + "parameterName": "RulesCount", + "comparison": "isEqualTo" + }, + "name": "text - 13" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "Select one or more governance rules from the list to see a list of affected recommendations", + "style": "info" + }, + "name": "RulesGridExplination" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources \r\n| where type == \"microsoft.security/assessments\"\r\n| where isnull(properties.resourceDetails.AwsResourceId) and isnull(properties.resourceDetails.GcpResourceId)\r\n| extend DisplayName = tostring(properties.displayName)\r\n| where isempty(DisplayName) == false\r\n| join kind=leftouter (securityresources \r\n| where type == \"microsoft.security/assessments/governanceassignments\"\r\n| extend assignedResourceId = tostring(todynamic(properties).assignedResourceId)\r\n| extend remediationDueDate = todatetime(properties.remediationDueDate)\r\n| project id = assignedResourceId, governanceassignmentsProperties = todynamic(properties), remediationDueDate) on id\r\n| extend hasAssignment = isempty( governanceassignmentsProperties) == false and isnull( governanceassignmentsProperties) == false\r\n| extend assignmentStatus = iif(tostring(properties.status.code) == \"Unhealthy\",iif(hasAssignment == true, iif(bin(remediationDueDate, 1d) < bin(now(), 1d), \"Overdue\", \"Ontime\"), \"Unassigned\") , \"Completed\")\r\n| summarize count() by assignmentStatus\r\n", + "size": 3, + "title": "Resource status", + "noDataMessage": "No unhealthy resources found", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "tileSettings": { + "titleContent": { + "columnMatch": "OsType", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal", + "useGrouping": false, + "maximumFractionDigits": 2, + "maximumSignificantDigits": 3 + } + } + }, + "showBorder": true + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "OsType", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "count_", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "chartSettings": { + "seriesLabelSettings": [ + { + "seriesName": "Ontime", + "color": "blue" + }, + { + "seriesName": "Completed", + "color": "green" + }, + { + "seriesName": "Unassigned", + "color": "orange" + }, + { + "seriesName": "Overdue", + "color": "redBright" + } + ] + } + }, + "customWidth": "20", + "name": "statusePerAssessment" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources\r\n| where type == \"microsoft.security/governancerules\"\r\n| where tostring(properties.isDisabled) == \"false\"\r\n| extend ruleName = todynamic(name)\r\n| extend ownerType = iif(tostring(properties.ownerSource.type) == \"Manually\", \"Email\", \"ByTag\")\r\n| extend description = tostring(properties.description)\r\n| extend displayName = tostring(properties.displayName)\r\n| extend governanceEmailNotification = todynamic(properties.governanceEmailNotification)\r\n| extend isGracePeriod = todynamic(properties.isGracePeriod)\r\n| extend remediationTimeframe = todynamic(properties.remediationTimeframe)\r\n| extend Days = tolong(totimespan(remediationTimeframe)/1d)\r\n| extend Days = iff(Days > 0, iff(Days == 1, \"1 day\", strcat(Days,\" days\")), \"\")\r\n| extend sourceResourceType = todynamic(properties.sourceResourceType)\r\n| extend conditionSets = todynamic(properties.conditionSets)\r\n| extend rulePriority = todynamic(properties.rulePriority)\r\n| extend ownerSource = todynamic(properties.ownerSource)\r\n| extend isDisabled = todynamic(properties.isDisabled)\r\n| extend ruleType = todynamic(properties.ruleType)\r\n| extend RuleConditionSet = tostring(properties.conditionSets), property = properties.conditionSets[0].conditions[0].property, operator = properties.conditionSets[0].conditions[0].operator\r\n| project Subscription = tostring(subscriptionId), [\"Display name\"] = tostring(properties.displayName), Priority = toint(properties.rulePriority), [\"Remediation timeframe\"] = Days, [\"Owner type\"] = ownerType, Owner = tostring(properties.ownerSource.value), [\"Grace period enabled\"] = tostring(properties.isGracePeriod), Rule = id, properties, RuleConditionSet\r\n| sort by Subscription, Priority asc", + "size": 0, + "title": "Governance rules", + "noDataMessage": "No Rules found", + "exportMultipleValues": true, + "exportedParameters": [ + { + "fieldName": "Rule", + "parameterName": "Rule", + "parameterType": 1, + "quote": "" + }, + { + "fieldName": "RuleConditionSet", + "parameterName": "RuleConditionSet", + "parameterType": 1, + "quote": "" + }, + { + "fieldName": "Owner", + "parameterName": "Owner", + "parameterType": 1, + "quote": "" + } + ], + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Subscription", + "formatter": 15, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "Display name", + "formatter": 1, + "formatOptions": { + "bladeOpenContext": { + "bladeName": "CreateGovernanceRuleContextBlade", + "extensionName": "Microsoft_Azure_Security", + "bladeParameters": [ + { + "name": "", + "source": "column", + "value": "properties" + }, + { + "name": "subscriptionId", + "source": "column", + "value": "subscriptionId" + }, + { + "name": "governanceRuleToEdit", + "source": "column", + "value": "properties" + } + ] + } + } + }, + { + "columnMatch": "Priority", + "formatter": 1 + }, + { + "columnMatch": "Remediation timeframe", + "formatter": 0, + "tooltipFormat": { + "tooltip": "DD.HH.MM.SS" + } + }, + { + "columnMatch": "Grace period enabled", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "true", + "representation": "success", + "text": "" + }, + { + "operator": "==", + "thresholdValue": "false", + "representation": "4", + "text": "" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "Rule", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + }, + { + "columnMatch": "name", + "formatter": 1 + }, + { + "columnMatch": "DisplayName", + "formatter": 1 + }, + { + "columnMatch": "ownerDetails", + "formatter": 1 + }, + { + "columnMatch": "isGracePeriod", + "formatter": 1 + }, + { + "columnMatch": "remediationTimeframe", + "formatter": 1 + } + ], + "rowLimit": 1000, + "filter": true, + "sortBy": [ + { + "itemKey": "$gen_link_Subscription_0", + "sortOrder": 2 + } + ], + "labelSettings": [ + { + "columnId": "Owner", + "label": "Owner details" + } + ] + }, + "sortBy": [ + { + "itemKey": "$gen_link_Subscription_0", + "sortOrder": 2 + } + ] + }, + "customWidth": "80", + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "Rules", + "styleSettings": { + "maxWidth": "100" + } + } + ], + "exportParameters": true + }, + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "RulesCount", + "comparison": "isNotEqualTo" + }, + "name": "subscriptionOverView" + }, + { + "type": 1, + "content": { + "json": "---" + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "resourceView" + }, + "name": "LineSeparator1" + }, + { + "type": 1, + "content": { + "json": "💡 Selected filter for **RuleConditionSet:** {RuleConditionSet}\r\n💡 Selected filter for **Rule:** {Rule}\r\n💡 Selected filter for **Owner:** {Owner}\r\n", + "style": "{selectedTab}" + }, + "conditionalVisibility": { + "parameterName": "parameter1", + "comparison": "isEqualTo", + "value": "1" + }, + "name": "ResourceFilter" + }, + { + "type": 1, + "content": { + "json": " \r\n---" + }, + "conditionalVisibility": { + "parameterName": "selectedTab", + "comparison": "isEqualTo", + "value": "resourceView" + }, + "name": "LineSeparator2" + }, + { + "type": 1, + "content": { + "json": "Select a recommendation from the list to see a list of affected resources", + "style": "info" + }, + "conditionalVisibilities": [ + { + "parameterName": "Rule", + "comparison": "isNotEqualTo" + }, + { + "parameterName": "DisplayName", + "comparison": "isEqualTo" + } + ], + "name": "assessmentsExplaination" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources \r\n| where type == \"microsoft.security/assessments\"\r\n| where isnull(properties.resourceDetails.AwsResourceId) and isnull(properties.resourceDetails.GcpResourceId)\r\n| extend DisplayName = tostring(properties.displayName)\r\n| where isempty(DisplayName) == false\r\n| extend RuleConditionSet = '{RuleConditionSet}'\r\n| where RuleConditionSet contains name or RuleConditionSet contains properties.metadata.severity\r\n| join kind=leftouter (securityresources \r\n| where type == \"microsoft.security/assessments/governanceassignments\"\r\n| extend assignedResourceId = tostring(todynamic(properties).assignedResourceId)\r\n| extend remediationDueDate = todatetime(properties.remediationDueDate)\r\n| project id = assignedResourceId, governanceassignmentsProperties = todynamic(properties), remediationDueDate) on id\r\n| extend hasAssignment = isempty( governanceassignmentsProperties) == false and isnull( governanceassignmentsProperties) == false\r\n| extend assignmentStatus = iif(tostring(properties.status.code) == \"Unhealthy\",iif(hasAssignment == true, iif(bin(remediationDueDate, 1d) < bin(now(), 1d), \"Overdue\", \"Ontime\"), \"Unassigned\") , \"Completed\")\r\n| summarize count() by assignmentStatus", + "size": 3, + "title": "Status per rule", + "noDataMessage": "No unhealthy resources found", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "gridSettings": { + "rowLimit": 10000 + }, + "tileSettings": { + "titleContent": { + "columnMatch": "OsType", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal", + "useGrouping": false, + "maximumFractionDigits": 2, + "maximumSignificantDigits": 3 + } + } + }, + "showBorder": true + }, + "graphSettings": { + "type": 0, + "topContent": { + "columnMatch": "OsType", + "formatter": 1 + }, + "centerContent": { + "columnMatch": "count_", + "formatter": 1, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "chartSettings": { + "seriesLabelSettings": [ + { + "seriesName": "Ontime", + "color": "blue" + }, + { + "seriesName": "Completed", + "color": "green" + }, + { + "seriesName": "Unassigned", + "color": "orange" + }, + { + "seriesName": "Overdue", + "color": "redBright" + } + ] + } + }, + "customWidth": "20", + "conditionalVisibility": { + "parameterName": "Rule", + "comparison": "isNotEqualTo" + }, + "name": "statusPerRule" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources \r\n| where type == \"microsoft.security/assessments\"\r\n| where isnull(properties.resourceDetails.AwsResourceId) and isnull(properties.resourceDetails.GcpResourceId)\r\n| extend DisplayName = tostring(properties.displayName)\r\n| where isempty(DisplayName) == false\r\n| extend RuleConditionSet = '{RuleConditionSet}'\r\n| where RuleConditionSet contains name or RuleConditionSet contains properties.metadata.severity\r\n| join kind=leftouter (securityresources \r\n| where type == \"microsoft.security/assessments/governanceassignments\"\r\n| extend assignedResourceId = tostring(todynamic(properties).assignedResourceId)\r\n| extend remediationDueDate = todatetime(properties.remediationDueDate)\r\n| project id = assignedResourceId, governanceassignmentsProperties = todynamic(properties), remediationDueDate) on id\r\n| extend hasAssignment = isempty( governanceassignmentsProperties) == false and isnull( governanceassignmentsProperties) == false\r\n| where hasAssignment == true\r\n| extend owner = tostring(governanceassignmentsProperties.owner)\r\n| extend owner = iif(isnull(owner) == false and isempty(owner) == false, owner, \"Unspecified\")\r\n| extend assignmentStatus = iif(bin(remediationDueDate, 1d) < bin(now(), 1d), \"Overdue\", \"Ontime\")\r\n| summarize Ontime = countif(assignmentStatus == \"Ontime\"), Overdue = countif(assignmentStatus == \"Overdue\") by selectedOwner = owner\r\n| sort by Overdue desc", + "size": 0, + "title": "Status per owner", + "exportMultipleValues": true, + "exportedParameters": [ + { + "fieldName": "selectedOwner", + "parameterName": "selectedOwner", + "quote": "" + } + ], + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Ontime", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "!=", + "thresholdValue": "0", + "representation": "info", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "Overdue", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "!=", + "thresholdValue": "0", + "representation": "3", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + } + ], + "labelSettings": [ + { + "columnId": "selectedOwner", + "label": "Owner" + } + ] + } + }, + "customWidth": "30", + "conditionalVisibilities": [ + { + "parameterName": "Owner", + "comparison": "isNotEqualTo" + }, + { + "parameterName": "RuleConditionSet", + "comparison": "isNotEqualTo" + } + ], + "name": "Owner status" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources \r\n| where type == \"microsoft.security/assessments\"\r\n| where isnull(properties.resourceDetails.AwsResourceId) and isnull(properties.resourceDetails.GcpResourceId)\r\n| extend DisplayName = tostring(properties.displayName)\r\n| where isempty(DisplayName) == false and isnull(DisplayName) == false\r\n| extend RuleConditionSet = '{RuleConditionSet}'\r\n| where RuleConditionSet contains name or RuleConditionSet contains properties.metadata.severity\r\n| join kind=leftouter (securityresources \r\n| where type == \"microsoft.security/assessments/governanceassignments\"\r\n| extend assignedResourceId = tostring(todynamic(properties).assignedResourceId)\r\n| extend remediationDueDate = todatetime(properties.remediationDueDate)\r\n| project id = assignedResourceId, governanceassignmentsProperties = todynamic(properties), remediationDueDate) on id\r\n| extend hasAssignment = isempty( governanceassignmentsProperties) == false and isnull( governanceassignmentsProperties) == false\r\n| extend assignmentStatus = iif(tostring(properties.status.code) == \"Unhealthy\",iif(hasAssignment == true, iif(bin(remediationDueDate, 1d) < bin(now(), 1d), \"Overdue\", \"Ontime\"), \"Unassigned\") , \"Completed\")\r\n| extend Status = assignmentStatus\r\n| summarize Completed = countif(Status == \"Completed\"), Ontime = countif(Status == \"Ontime\"), Overdue = countif(Status == \"Overdue\"),Unassigned = countif(Status == \"Unassigned\") by DisplayName = tostring(properties.displayName)\r\n| sort by Overdue desc", + "size": 0, + "title": "Recommendations", + "noDataMessage": "No Assessments found", + "exportedParameters": [ + { + "fieldName": "id", + "parameterName": "id", + "parameterType": 1 + }, + { + "fieldName": "DisplayName", + "parameterName": "DisplayName", + "parameterType": 1 + } + ], + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "DisplayName", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "75ch" + } + }, + { + "columnMatch": "Completed", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "!=", + "thresholdValue": "0", + "representation": "success", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Blank", + "text": "{0}{1}" + } + ], + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "Ontime", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "!=", + "thresholdValue": "0", + "representation": "1", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Blank", + "text": "{0}{1}" + }, + { + "representation": "Unknown", + "text": "{0}{1}" + } + ], + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "Overdue", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "!=", + "thresholdValue": "0", + "representation": "3", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Blank", + "text": "{0}{1}" + } + ], + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "Unassigned", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "!=", + "thresholdValue": "0", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Blank", + "text": "0" + } + ], + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "Status", + "formatter": 1 + }, + { + "columnMatch": "id", + "formatter": 1, + "formatOptions": { + "customColumnWidthSetting": "90ch" + } + }, + { + "columnMatch": "owner", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "40ch" + } + }, + { + "columnMatch": "DueDate", + "formatter": 6 + }, + { + "columnMatch": "Severity", + "formatter": 5 + }, + { + "columnMatch": "Resource", + "formatter": 13, + "formatOptions": { + "linkTarget": "OpenBlade", + "linkIsContextBlade": false, + "showIcon": true, + "bladeOpenContext": { + "bladeName": "GenericResourceHealthDetailsBlade", + "extensionName": "Microsoft_Azure_Security", + "bladeParameters": [ + { + "name": "resourceId", + "source": "cell", + "value": "%2Fsubscriptions%2F3b5bc982-20bc-4b59-b1ca-f8488bb86736%2FresourceGroups%2Fdemo%2Fproviders%2FMicrosoft.HybridCompute%2Fmachines%2FW2019" + } + ] + }, + "customColumnWidthSetting": "20ch" + } + }, + { + "columnMatch": "ResourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true, + "customColumnWidthSetting": "25ch" + } + }, + { + "columnMatch": "Source", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "OperatingSystem", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "25ch" + } + }, + { + "columnMatch": "Category", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20ch" + } + }, + { + "columnMatch": "Remediation", + "formatter": 5 + }, + { + "columnMatch": "RemediationSteps", + "formatter": 11, + "formatOptions": { + "linkColumn": "Remediation", + "linkTarget": "Url" + }, + "tooltipFormat": { + "tooltip": "Click to view remediation steps" + } + }, + { + "columnMatch": "Code", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "Healthy", + "representation": "success", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Unhealthy", + "representation": "3", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "rowLimit": 10000, + "filter": true, + "sortBy": [ + { + "itemKey": "$gen_thresholds_Ontime_2", + "sortOrder": 2 + } + ], + "labelSettings": [ + { + "columnId": "DisplayName", + "label": "Display name" + } + ] + }, + "sortBy": [ + { + "itemKey": "$gen_thresholds_Ontime_2", + "sortOrder": 2 + } + ] + }, + "customWidth": "50", + "conditionalVisibility": { + "parameterName": "Rule", + "comparison": "isNotEqualTo" + }, + "name": "Assessmetns" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "" + }, + "customWidth": "50", + "conditionalVisibility": { + "parameterName": "RuleConditionSet", + "comparison": "isNotEqualTo" + }, + "name": "empty text" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "securityresources \r\n| where type == \"microsoft.security/assessments\"\r\n| where isnull(properties.resourceDetails.AwsResourceId) and isnull(properties.resourceDetails.GcpResourceId)\r\n| extend displayNameFilter = tostring(\"{DisplayName}\")\r\n| extend selectedOwner = '{selectedOwner}'\r\n| where displayNameFilter == tostring(properties.displayName)\r\n| join kind=leftouter (securityresources \r\n| where type == \"microsoft.security/assessments/governanceassignments\"\r\n| extend assignedResourceId = tostring(todynamic(properties).assignedResourceId)\r\n| extend remediationDueDate = todatetime(properties.remediationDueDate)\r\n| project id = assignedResourceId, owner = properties.owner,governanceassignmentsProperties = todynamic(properties), remediationDueDate, isGrace = properties.isGracePeriod) on id\r\n| extend hasAssignment = isempty( governanceassignmentsProperties) == false and isnull( governanceassignmentsProperties) == false\r\n| extend assignmentStatus = iif(tostring(properties.status.code) == \"Unhealthy\",iif(hasAssignment == true, iif(bin(remediationDueDate, 1d) < bin(now(), 1d), \"Overdue\", \"Ontime\"), \"Unassigned\") , \"Completed\")\r\n| extend source = trim(' ', tolower(tostring(properties.resourceDetails.Source)))\r\n | extend resourceId = iff(source =~ \"azure\", properties.resourceDetails.Id, iff(source =~ \"aws\" and isnotempty(tostring(properties.resourceDetails.ConnectorId)), properties.resourceDetails.Id, iff(source =~ \"gcp\" and isnotempty(tostring(properties.resourceDetails.ConnectorId)), properties.resourceDetails.Id, iff(source =~ 'aws', properties.resourceDetails.AzureResourceId, iff(source =~ 'gcp', properties.resourceDetails.AzureResourceId, properties.resourceDetails.Id)))))\r\n| extend owner = tostring(governanceassignmentsProperties.owner)\r\n| extend owner = iif(isnull(owner) == false and isempty(owner) == false and hasAssignment == true , owner, iif(hasAssignment == false, owner, \"Unspecified\"))\r\n| where '{selectedOwner}' == '' or (selectedOwner contains owner and hasAssignment == true)\r\n| project [\"Resource\"] = resourceId, Subscription = subscriptionId ,Status = assignmentStatus, Owner = owner, [\"Due date\"] = remediationDueDate, [\"Grace period enabled\"] = isGrace\r\n| sort by Status desc", + "size": 0, + "title": "List of resources for: {DisplayName}", + "noDataMessage": "No Assessments found", + "exportFieldName": "id", + "exportParameterName": "id", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Resource id", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true, + "customColumnWidthSetting": "90ch" + } + }, + { + "columnMatch": "Subscription", + "formatter": 15, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "Status", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "Completed", + "representation": "success", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Unassigned", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Overdue", + "representation": "3", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Ontime", + "representation": "1", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": null, + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 0, + "options": { + "style": "decimal" + } + } + }, + { + "columnMatch": "Grace period enabled", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "true", + "representation": "success", + "text": "" + }, + { + "operator": "==", + "thresholdValue": "false", + "representation": "4", + "text": "" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Blank", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "owner", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "40ch" + } + }, + { + "columnMatch": "DueDate", + "formatter": 6 + }, + { + "columnMatch": "id", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "90ch" + } + }, + { + "columnMatch": "DisplayName", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "100ch" + } + }, + { + "columnMatch": "Completed", + "formatter": 4, + "formatOptions": { + "palette": "green", + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "Ontime", + "formatter": 4, + "formatOptions": { + "palette": "blue", + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "Overdue", + "formatter": 4, + "formatOptions": { + "palette": "redBright", + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "Unassigned", + "formatter": 4, + "formatOptions": { + "palette": "orange", + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "Severity", + "formatter": 5 + }, + { + "columnMatch": "Resource", + "formatter": 13, + "formatOptions": { + "linkTarget": "OpenBlade", + "linkIsContextBlade": false, + "showIcon": true, + "bladeOpenContext": { + "bladeName": "GenericResourceHealthDetailsBlade", + "extensionName": "Microsoft_Azure_Security", + "bladeParameters": [ + { + "name": "resourceId", + "source": "cell", + "value": "%2Fsubscriptions%2F3b5bc982-20bc-4b59-b1ca-f8488bb86736%2FresourceGroups%2Fdemo%2Fproviders%2FMicrosoft.HybridCompute%2Fmachines%2FW2019" + } + ] + }, + "customColumnWidthSetting": "20ch" + } + }, + { + "columnMatch": "ResourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true, + "customColumnWidthSetting": "25ch" + } + }, + { + "columnMatch": "Source", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "15ch" + } + }, + { + "columnMatch": "OperatingSystem", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "25ch" + } + }, + { + "columnMatch": "Category", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "20ch" + } + }, + { + "columnMatch": "Remediation", + "formatter": 5 + }, + { + "columnMatch": "RemediationSteps", + "formatter": 11, + "formatOptions": { + "linkColumn": "Remediation", + "linkTarget": "Url" + }, + "tooltipFormat": { + "tooltip": "Click to view remediation steps" + } + }, + { + "columnMatch": "Code", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "Healthy", + "representation": "success", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Unhealthy", + "representation": "3", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "rowLimit": 10000, + "filter": true + }, + "sortBy": [] + }, + "conditionalVisibility": { + "parameterName": "DisplayName", + "comparison": "isNotEqualTo" + }, + "name": "Assignments" + } + ] + }, + "name": "assessmentsWithExplaination" + }, + { + "type": 1, + "content": { + "json": "💡 Selected filter for **DisplayName:** {DisplayName}\r\n💡 Selected filter for **selectedOwner:** {selectedOwner}\r\n", + "style": "{selectedTab}" + }, + "conditionalVisibility": { + "parameterName": "parameter1", + "comparison": "isEqualTo", + "value": "1" + }, + "name": "ResourceFilter - Copy" + } + ] + }, + "name": "assessmentsGrid" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Governance" + }, + "name": "RC_Governance" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "cc98cfec-0182-4887-854e-536e9f3857da", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "typeSettings": { + "additionalResourceOptions": [], + "includeAll": true, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "value": null + }, + { + "id": "1c3411d9-e319-4d74-8e97-61e2f4c56a56", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "isRequired": true, + "query": "resources\r\n| summarize by location\r\n| where location != \"global\"", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": "westeurope" + } + ], + "style": "above", + "queryType": 0, + "resourceType": "microsoft.resourcegraph/resources" + }, + "name": "parameters - 0" + }, + { + "type": 1, + "content": { + "json": "## Azure subscription and service limits, quotas, and constraints
\r\nTo know more about Azure service limits & quotas, see [Azure subscription and service limits, quotas, and constraints](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-subscription-service-limits?toc=%2Fazure%2Fnetworking%2Ftoc.json#networking-limits)." + }, + "name": "text - Limits" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadFromTemplateId": "community-Workbooks/Common/noSubscriptions", + "items": [] + }, + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isEqualTo", + "value": "" + }, + "name": "No Subscriptions group - RC_Quota" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":null,\"headers\":[],\"method\":\"GET\",\"path\":\"/subscriptions/{Subscription:id}/providers/microsoft.compute/locations/{Location}/usages?api-version=2022-03-01\",\"urlParams\":[],\"batchDisabled\":false,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.value\",\"columns\":[{\"path\":\"currentValue\",\"columnid\":\"Used\",\"columnType\":\"long\"},{\"path\":\"limit\",\"columnid\":\"Limit\",\"columnType\":\"long\"},{\"path\":\"name.localizedValue\",\"columnid\":\"Resource\"}]}}]}", + "size": 0, + "title": "Compute resource limits", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "filter": true, + "sortBy": [ + { + "itemKey": "Limit", + "sortOrder": 1 + } + ] + }, + "sortBy": [ + { + "itemKey": "Limit", + "sortOrder": 1 + } + ] + }, + "customWidth": "50", + "conditionalVisibilities": [ + { + "parameterName": "Subscription", + "comparison": "isNotEqualTo", + "value": "" + }, + { + "parameterName": "Location", + "comparison": "isNotEqualTo" + } + ], + "name": "query - ComputeLimits" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":null,\"headers\":[],\"method\":\"GET\",\"path\":\"/subscriptions/{Subscription:id}/providers/microsoft.network/locations/{Location}/usages?api-version=2022-01-01\",\"urlParams\":[],\"batchDisabled\":false,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.value\",\"columns\":[{\"path\":\"currentValue\",\"columnid\":\"Used\",\"columnType\":\"long\"},{\"path\":\"limit\",\"columnid\":\"Limit\",\"columnType\":\"long\"},{\"path\":\"name.localizedValue\",\"columnid\":\"Resource\"}]}}]}", + "size": 0, + "title": "Network resource limits", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "filter": true, + "sortBy": [ + { + "itemKey": "Limit", + "sortOrder": 1 + } + ] + }, + "sortBy": [ + { + "itemKey": "Limit", + "sortOrder": 1 + } + ] + }, + "customWidth": "50", + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "query - NetworkLimits" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":null,\"headers\":[],\"method\":\"GET\",\"path\":\"/subscriptions/{Subscription:id}/providers/microsoft.storage/locations/{Location}/usages?api-version=2021-09-01\",\"urlParams\":[],\"batchDisabled\":false,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.value\",\"columns\":[{\"path\":\"currentValue\",\"columnid\":\"Used\",\"columnType\":\"long\"},{\"path\":\"limit\",\"columnid\":\"Limit\",\"columnType\":\"long\"},{\"path\":\"name.localizedValue\",\"columnid\":\"Resource\"}]}}]}", + "size": 4, + "title": "Storage account limits", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "filter": true, + "sortBy": [ + { + "itemKey": "Limit", + "sortOrder": 1 + } + ] + }, + "sortBy": [ + { + "itemKey": "Limit", + "sortOrder": 1 + } + ] + }, + "customWidth": "100", + "conditionalVisibility": { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + }, + "name": "query - StorageLimits" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_Quota" + }, + "name": "Usage + limits" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "title": "Automation", + "expandable": true, + "expanded": true, + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type has 'microsoft.automation'\r\n\tor type has 'microsoft.logic'\r\n\tor type has 'microsoft.web/customapis'\r\n| extend type = case(\r\n\ttype =~ 'microsoft.automation/automationaccounts', 'Automation Accounts',\r\n\ttype == 'microsoft.web/serverfarms', \"App Service Plans\",\r\n\tkind == 'functionapp', \"Azure Functions\", \r\n\tkind == \"api\", \"API Apps\", \r\n\ttype == 'microsoft.web/sites', \"App Services\",\r\n\ttype =~ 'microsoft.web/connections', 'LogicApp Connectors',\r\n\ttype =~ 'microsoft.web/customapis','LogicApp API Connectors',\r\n\ttype =~ 'microsoft.logic/workflows','LogicApps',\r\n\ttype =~ 'microsoft.automation/automationaccounts/runbooks', 'Automation Runbooks',\r\n type =~ 'microsoft.automation/automationaccounts/configurations', 'Automation Configurations',\r\nstrcat(\"Not Translated: \", type))\r\n| summarize count() by type\r\n| where type !has \"Not Translated\"", + "size": 3, + "title": "Count of all resource types", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "type", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + } + }, + "name": "Count of all resource types" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type has 'microsoft.automation'\r\n\t or type has 'microsoft.logic'\r\n\t or type has 'microsoft.web/customapis'\r\n| extend type = case(\r\n\ttype =~ 'microsoft.automation/automationaccounts', 'Automation Accounts',\r\n\ttype =~ 'microsoft.web/connections', 'LogicApp Connectors',\r\n\ttype =~ 'microsoft.web/customapis','LogicApp API Connectors',\r\n\ttype =~ 'microsoft.logic/workflows','LogicApps',\r\n\ttype =~ 'microsoft.automation/automationaccounts/runbooks', 'Automation Runbooks',\r\n\ttype =~ 'microsoft.automation/automationaccounts/configurations', 'Automation Configurations',\r\n\tstrcat(\"Not Translated: \", type))\r\n| extend RunbookType = tostring(properties.runbookType)\r\n| extend LogicAppTrigger = properties.definition.triggers\r\n| extend LogicAppTrigger = iif(type =~ 'LogicApps', case(\r\n\tLogicAppTrigger has 'manual', tostring(LogicAppTrigger.manual.type),\r\n\tLogicAppTrigger has 'Recurrence', tostring(LogicAppTrigger.Recurrence.type),\r\n\tstrcat(\"Unknown Trigger type\", LogicAppTrigger)), LogicAppTrigger)\r\n| extend State = case(\r\n\ttype =~ 'Automation Runbooks', properties.state, \r\n\ttype =~ 'LogicApps', properties.state,\r\n\ttype =~ 'Automation Accounts', properties.state,\r\n\ttype =~ 'Automation Configurations', properties.state,\r\n\t' ')\r\n| extend CreatedDate = case(\r\n\ttype =~ 'Automation Runbooks', properties.creationTime, \r\n\ttype =~ 'LogicApps', properties.createdTime,\r\n\ttype =~ 'Automation Accounts', properties.creationTime,\r\n\ttype =~ 'Automation Configurations', properties.creationTime,\r\n\t' ')\r\n| extend LastModified = case(\r\n\ttype =~ 'Automation Runbooks', properties.lastModifiedTime, \r\n\ttype =~ 'LogicApps', properties.changedTime,\r\n\ttype =~ 'Automation Accounts', properties.lastModifiedTime,\r\n\ttype =~ 'Automation Configurations', properties.lastModifiedTime,\r\n\t' ')\r\n| extend Details = pack_all()\r\n| project Resource=id, subscriptionId, type, resourceGroup, RunbookType, LogicAppTrigger, State, Details", + "size": 0, + "title": "Details", + "noDataMessage": "No resources found", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "Resource", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + }, + { + "columnMatch": "Details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true, + "finalBy": "Resource" + } + }, + "tileSettings": { + "titleContent": { + "columnMatch": "type", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "showBorder": true, + "sortCriteriaField": "type", + "sortOrderField": 1 + } + }, + "name": "query - PaaS - Automation Detailed" + } + ] + }, + "name": "Group - Automation", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "title": "App services", + "expandable": true, + "expanded": true, + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type has 'microsoft.web'\r\n\t or type =~ 'microsoft.apimanagement/service'\r\n\t or type =~ 'microsoft.network/frontdoors'\r\n\t or type =~ 'microsoft.network/applicationgateways'\r\n\t or type =~ 'microsoft.appconfiguration/configurationstores'\r\n| extend type = case(\r\n\ttype == 'microsoft.web/serverfarms', \"App Service Plans\",\r\n\tkind == 'functionapp', \"Azure Functions\", \r\n\tkind == \"api\", \"API Apps\", \r\n\ttype == 'microsoft.web/sites', \"App Services\",\r\n\ttype =~ 'microsoft.network/applicationgateways', 'App Gateways',\r\n\ttype =~ 'microsoft.network/frontdoors', 'Front Door',\r\n\ttype =~ 'microsoft.apimanagement/service', 'API Management',\r\n\ttype =~ 'microsoft.web/certificates', 'App Certificates',\r\n\ttype =~ 'microsoft.appconfiguration/configurationstores', 'App Config Stores',\r\n\tstrcat(\"Not Translated: \", type))\r\n| where type !has \"Not Translated\"\r\n| summarize count() by type", + "size": 3, + "title": "Count of all resource types", + "noDataMessage": "No resources found", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Resource", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true, + "finalBy": "Resource" + } + }, + "tileSettings": { + "titleContent": { + "columnMatch": "type", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "showBorder": false, + "sortCriteriaField": "type", + "sortOrderField": 1 + } + }, + "name": "query - PaaS - Apps Overview" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type has 'microsoft.web'\r\n\t or type =~ 'microsoft.apimanagement/service'\r\n\t or type =~ 'microsoft.network/frontdoors'\r\n\t or type =~ 'microsoft.network/applicationgateways'\r\n\t or type =~ 'microsoft.appconfiguration/configurationstores'\r\n| extend type = case(\r\n\ttype == 'microsoft.web/serverfarms', \"App Service Plans\",\r\n\tkind == 'functionapp', \"Azure Functions\", \r\n\tkind == \"api\", \"API Apps\", \r\n\ttype == 'microsoft.web/sites', \"App Services\",\r\n\ttype =~ 'microsoft.network/applicationgateways', 'App Gateways',\r\n\ttype =~ 'microsoft.network/frontdoors', 'Front Door',\r\n\ttype =~ 'microsoft.apimanagement/service', 'API Management',\r\n\ttype =~ 'microsoft.web/certificates', 'App Certificates',\r\n\ttype =~ 'microsoft.appconfiguration/configurationstores', 'App Config Stores',\r\n\tstrcat(\"Not Translated: \", type))\r\n| where type !has \"Not Translated\"\r\n| extend Sku = case(\r\n\ttype =~ 'App Gateways', properties.sku.name, \r\n\ttype =~ 'Azure Functions', properties.sku,\r\n\ttype =~ 'API Management', sku.name,\r\n\ttype =~ 'App Service Plans', sku.name,\r\n\ttype =~ 'App Services', properties.sku,\r\n\ttype =~ 'App Config Stores', sku.name,\r\n\t' ')\r\n| extend State = case(\r\n\ttype =~ 'App Config Stores', properties.provisioningState,\r\n\ttype =~ 'App Service Plans', properties.status,\r\n\ttype =~ 'Azure Functions', properties.state,\r\n\ttype =~ 'App Services', properties.state,\r\n\ttype =~ 'API Management', properties.provisioningState,\r\n\ttype =~ 'App Gateways', properties.provisioningState,\r\n\ttype =~ 'Front Door', properties.provisioningState,\r\n\t' ')\r\n| mv-expand publicIpId = properties.frontendIPConfigurations\r\n| mv-expand publicIpId = publicIpId.properties.publicIPAddress.id\r\n| extend publicIpId = tostring(publicIpId)\r\n\t| join kind=leftouter(\r\n\t \tResources\r\n \t\t| where type =~ 'microsoft.network/publicipaddresses'\r\n \t\t| project publicIpId = id, publicIpAddress = tostring(properties.ipAddress)) on publicIpId\r\n| extend PublicIP = case(\r\n\ttype =~ 'API Management', properties.publicIPAddresses,\r\n\ttype =~ 'App Gateways', publicIpAddress,\r\n type =~ 'App Services', properties.inboundIpAddress,\r\n type =~ 'Azure Functions', properties.inboundIpAddress,\r\n\t' ')\r\n| extend Instances = case(\r\n\ttype =~ 'API Management', sku.capacity,\r\n type =~ 'App Services', properties.siteConfig.numberOfWorkers,\r\n type =~ 'Azure Functions', properties.siteConfig.numberOfWorkers,\r\n type =~ 'App Service Plans', properties.currentNumberOfWorkers,\r\n\t' ')\r\n| extend ServicePlan = case(\r\n type =~ 'App Services', properties.serverFarmId,\r\n type =~ 'Azure Functions', properties.serverFarmId,\r\n\t' ')\r\n| extend Details = pack_all()\r\n| project Resource=id, type, subscriptionId, Sku, State, PublicIP, Instances, ServicePlan, Details", + "size": 0, + "noDataMessage": "No resources found", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "Resource", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + }, + { + "columnMatch": "Details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true, + "finalBy": "Resource" + } + }, + "tileSettings": { + "titleContent": { + "columnMatch": "type", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "showBorder": true, + "sortCriteriaField": "type", + "sortOrderField": 1 + } + }, + "name": "query - PaaS - Apps Detailed" + } + ] + }, + "name": "Group - App Services", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "title": "Data", + "expandable": true, + "expanded": true, + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'microsoft.documentdb/databaseaccounts'\r\n\tor type =~ 'microsoft.sql/servers/databases'\r\n\tor type =~ 'microsoft.dbformysql/servers'\r\n\tor type =~ 'microsoft.sql/servers'\r\n or type =~ 'Microsoft.DBforPostgreSQL/servers'\r\n or type =~ 'Microsoft.DBforMariaDB/servers'\r\n or type =~ 'microsoft.dbforpostgresql/flexibleservers'\r\n| extend type = case(\r\n\ttype =~ 'microsoft.documentdb/databaseaccounts', 'CosmosDB',\r\n\ttype =~ 'microsoft.sql/servers/databases', 'SQL DBs',\r\n\ttype =~ 'microsoft.dbformysql/servers', 'MySQL Servers',\r\n\ttype =~ 'microsoft.sql/servers', 'SQL Servers',\r\n type =~ 'Microsoft.DBforPostgreSQL/servers', 'PostgreSQL Servers',\r\n type =~ 'microsoft.dbforpostgresql/flexibleservers', 'PostgreSQL Flexi Servers',\r\n type =~ 'Microsoft.DBforMariaDB/servers', 'MariaDB Servers',\r\n\tstrcat(\"Not Translated: \", type))\r\n| where type !has \"Not Translated\"\r\n| summarize count() by type", + "size": 3, + "title": "Count of all resource types", + "noDataMessage": "No resources found", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "tiles", + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Resource", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true, + "finalBy": "Resource" + } + }, + "tileSettings": { + "titleContent": { + "columnMatch": "type", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "showBorder": true, + "sortCriteriaField": "type", + "sortOrderField": 1 + } + }, + "name": "query - PaaS - Data Overview" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "// data\r\n// Click the \"Run query\" command above to execute the query and see results.\r\nresources \r\n| where type =~ 'microsoft.documentdb/databaseaccounts'\r\n\tor type =~ 'microsoft.sql/servers/databases'\r\n\tor type =~ 'microsoft.dbformysql/servers'\r\n\tor type =~ 'microsoft.sql/servers'\r\n or type =~ 'Microsoft.DBforPostgreSQL/servers'\r\n or type =~ 'Microsoft.DBforMariaDB/servers'\r\n or type =~ 'microsoft.dbforpostgresql/flexibleservers'\r\n| extend type = case(\r\n\ttype =~ 'microsoft.documentdb/databaseaccounts', 'CosmosDB',\r\n\ttype =~ 'microsoft.sql/servers/databases', 'SQL DBs',\r\n\ttype =~ 'microsoft.dbformysql/servers', 'MySQL Servers',\r\n\ttype =~ 'microsoft.sql/servers', 'SQL Servers',\r\n type =~ 'Microsoft.DBforPostgreSQL/servers', 'PostgreSQL Servers',\r\n type =~ 'microsoft.dbforpostgresql/flexibleservers', 'PostgreSQL Flexi Servers',\r\n type =~ 'Microsoft.DBforMariaDB/servers', 'MariaDB Servers',\r\n\tstrcat(\"Not Translated: \", type))\r\n| extend Sku = case(\r\n\ttype =~ 'CosmosDB', properties.databaseAccountOfferType,\r\n\ttype =~ 'SQL DBs', sku.name,\r\n\ttype =~ 'MySQL Servers', sku.name,\r\n type =~ 'PostgreSQL Servers', sku.name,\r\n type =~ 'PostgreSQL Flexi Servers', sku.name,\r\n type =~ 'MariaDB Servers', sku.name,\r\n\t' ')\r\n| extend Status = case(\r\n\ttype =~ 'CosmosDB', properties.provisioningState,\r\n\ttype =~ 'SQL DBs', properties.status,\r\n type =~ 'SQL Servers', properties.state,\r\n\ttype =~ 'MySQL Servers', properties.userVisibleState,\r\n type =~ 'PostgreSQL Servers', properties.state,\r\n type =~ 'PostgreSQL Flexi Servers', properties.state,\r\n type =~ 'MariaDB Servers', properties.userVisibleState,\r\n\t' ')\r\n| extend Endpoint = case(\r\n\ttype =~ 'MySQL Servers', properties.fullyQualifiedDomainName,\r\n\ttype =~ 'SQL Servers', properties.fullyQualifiedDomainName,\r\n\ttype =~ 'CosmosDB', properties.documentEndpoint,\r\n type =~ 'PostgreSQL Servers', properties.fullyQualifiedDomainName,\r\n type =~ 'PostgreSQL Flexi Servers', properties.fullyQualifiedDomainName,\r\n type =~ 'MariaDB Servers', properties.fullyQualifiedDomainName,\r\n\t' ')\r\n| extend PublicNetworkAccess = case(\r\n\ttype =~ 'MySQL Servers', properties.publicNetworkAccess,\r\n\ttype =~ 'SQL Servers', properties.publicNetworkAccess,\r\n type =~ 'PostgreSQL Servers', properties.publicNetworkAccess,\r\n type =~ 'PostgreSQL Flexi Servers', properties.publicNetworkAccess,\r\n type =~ 'MariaDB Servers', properties.publicNetworkAccess,\r\n\t' ')\r\n| extend Version = case(\r\n\ttype =~ 'MySQL Servers', properties.version,\r\n\ttype =~ 'SQL Servers', properties.version,\r\n type =~ 'PostgreSQL Servers', properties.version,\r\n type =~ 'PostgreSQL Flexi Servers', properties.version,\r\n type =~ 'MariaDB Servers', properties.version,\r\n\t' ')\r\n| extend maxSizeGB = todouble(case(\r\n\ttype =~ 'SQL DBs', properties.maxSizeBytes,\r\n\ttype =~ 'MySQL Servers', properties.storageProfile.storageMB,\r\n type =~ 'PostgreSQL Servers', properties.storageProfile.storageMB,\r\n type =~ 'PostgreSQL Flexi Servers', properties.storageProfile.storageMB,\r\n type =~ 'MariaDB Servers', properties.storageProfile.storageMB,\r\n\t' '))\r\n| extend maxSizeGB = iif(type has 'SQL DBs', maxSizeGB /1000 /1000, maxSizeGB)\r\n| extend Details = pack_all()\r\n| project Resource=id, resourceGroup, subscriptionId, type, Sku, Status, Endpoint, Version, PublicNetworkAccess, maxSizeGB, Details\r\n\r\n", + "size": 0, + "title": "Details", + "noDataMessage": "No resources found", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "showIcon": true + } + }, + { + "columnMatch": "Resource", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 5 + }, + { + "columnMatch": "maxSizeGB", + "formatter": 0, + "numberFormat": { + "unit": 4, + "options": { + "style": "decimal", + "useGrouping": false + } + } + }, + { + "columnMatch": "Details", + "formatter": 7, + "formatOptions": { + "linkTarget": "CellDetails", + "linkLabel": "🔍 View details", + "linkIsContextBlade": true + } + } + ], + "rowLimit": 1000, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true, + "finalBy": "Resource" + } + }, + "tileSettings": { + "titleContent": { + "columnMatch": "type", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "showBorder": true, + "sortCriteriaField": "type", + "sortOrderField": 1 + } + }, + "name": "query - PaaS - Data Detailed" + } + ] + }, + "name": "Data", + "styleSettings": { + "showBorder": true + } + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_PaaS" + }, + { + "parameterName": "Subscription", + "comparison": "isNotEqualTo" + } + ], + "name": "RC_PaaS" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadFromTemplateId": "community-Workbooks/Azure Advisor/AzureServiceRetirement", + "items": [] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RC_ServicesRetirement" + }, + "name": "group - Service retirement" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadFromTemplateId": "community-Workbooks/Common/noSubscriptions", + "items": [] + }, + "conditionalVisibilities": [ + { + "parameterName": "Subscription", + "comparison": "isEqualTo", + "value": "" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "RC_Quota" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "RC_Age" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "RC_ServicesRetirement" + } + ], + "name": "No Subscriptions group" + } + ] + }, + "name": "Azure Governance Workbook" + } + ], + "fallbackResourceIds": [ + "azure monitor" + ], + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" + }, + "version": "", + "workbookJson": "[string(variables('$fxv#0'))]", + "workbookId": "907", + "telemetryId": "[format('00f120b5-2007-6120-0000-{0}30126b006', variables('workbookId'))]", + "finOpsToolkitVersion": "0.4", + "resourceTags": "[union(parameters('tags'), createObject('ftk-version', variables('finOpsToolkitVersion'), 'ftk-tool', format('{0} workbook', parameters('displayName'))))]" + }, + "resources": [ + { + "condition": "[parameters('enableDefaultTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('pid-{0}-{1}', variables('telemetryId'), uniqueString(deployment().name, parameters('location')))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "FinOps toolkit", + "version": "[variables('finOpsToolkitVersion')]" + } + }, + "resources": [] + } + } + }, + { + "type": "Microsoft.Insights/workbooks", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))]", + "location": "[parameters('location')]", + "tags": "[variables('resourceTags')]", + "kind": "shared", + "properties": { + "category": "workbook", + "description": "[parameters('description')]", + "displayName": "[parameters('displayName')]", + "serializedData": "[variables('workbookJson')]", + "sourceId": "Azure Monitor", + "version": "[variables('version')]" + } + } + ], + "outputs": { + "workbookId": { + "type": "string", + "metadata": { + "description": "The resource ID of the workbook." + }, + "value": "[resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName')))]" + }, + "workbookUrl": { + "type": "string", + "metadata": { + "description": "Link to the workbook in the Azure portal." + }, + "value": "[format('{0}/#view/AppInsightsExtension/UsageNotebookBlade/ComponentId/Azure%20Monitor/ConfigurationId/{1}/Type/{2}/WorkbookTemplateName/{3}', environment().portal, uriComponent(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName')))), reference(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))), '2022-04-01').category, uriComponent(reference(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))), '2022-04-01').displayName))]" + } + } +} \ No newline at end of file diff --git a/docs/deploy/governance-workbook-0.4.ui.json b/docs/deploy/governance-workbook-0.4.ui.json new file mode 100644 index 000000000..6646035df --- /dev/null +++ b/docs/deploy/governance-workbook-0.4.ui.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "basics": { + "description": "The Governance workbook provides an overview of the cost posture of your Azure environment. [Learn more](https://aka.ms/finops/toolkit)", + "location": { + "label": "Location", + "resourceTypes": ["Microsoft.Insights/workbooks"] + } + } + }, + "resourceTypes": ["Microsoft.Insights/workbooks"], + "basics": [ + { + "name": "displayName", + "type": "Microsoft.Common.TextBox", + "label": "Name", + "defaultValue": "Governance", + "toolTip": "Name of the workbook.", + "constraints": { + "required": true, + "regex": "^.{1,250}$", + "validationMessage": "Name cannot be longer than 250 characters." + }, + "visible": true + } + ], + "steps": [ + { + "name": "tags", + "label": "Tags", + "elements": [ + { + "name": "tagsByResource", + "label": "Tags", + "toolTip": "Tags to apply.", + "type": "Microsoft.Common.TagsByResource", + "resources": ["Microsoft.Insights/workbooks"] + } + ] + } + ], + "outputs": { + "displayName": "[basics('displayName')]", + "location": "[location()]", + "tags": "[steps('tags').tagsByResource]" + } + } +} diff --git a/docs/deploy/governance-workbook-latest.json b/docs/deploy/governance-workbook-latest.json index d80c848c7..9a1e8a2f2 100644 --- a/docs/deploy/governance-workbook-latest.json +++ b/docs/deploy/governance-workbook-latest.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.24.24.22086", - "templateHash": "12140906665358506794" + "templateHash": "11639158207496465968" } }, "parameters": { @@ -182,7 +182,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["value::tenant"], + "crossComponentResources": [ + "value::tenant" + ], "parameters": [ { "id": "5704765e-092d-41cb-b856-e5d1d5337ac5", @@ -191,7 +193,9 @@ "label": "Management group", "type": 2, "query": "resourcecontainers\r\n| where type == \"microsoft.management/managementgroups\"\r\n| extend name = properties.displayName\r\n| project name", - "crossComponentResources": ["value::tenant"], + "crossComponentResources": [ + "value::tenant" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -213,9 +217,13 @@ "quote": "'", "delimiter": ",", "query": "resourcecontainers\r\n| where type == \"microsoft.resources/subscriptions\"\r\n| where properties['managementGroupAncestorsChain'] contains '{ManagementGroup:label}'", - "crossComponentResources": ["value::tenant"], + "crossComponentResources": [ + "value::tenant" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "timeContext": { @@ -283,7 +291,7 @@ { "type": 1, "content": { - "json": "The objective of this workbook is to provide a comprehensive overview of the governance posture of your Azure environment. It offers the standard metrics aligned with the Cloud Adoption Framework for all the disciplines and has the capability to identify and apply recommendations to identify non compliance. This workbook is part of the [FinOps toolkit](https://aka.ms/finops/toolkit).\r\n\r\n## Overview of the Cloud Adoption Framework\r\n\r\n* With any cloud platform, there are common governance disciplines that help inform policies and align toolchains. These disciplines guide decisions about the proper level of automation and enforcement of corporate policy across cloud platforms.\r\n\r\n|Discipline|Description|\r\n|---|---|\r\n| [Cost Management](https://learn.microsoft.com/azure/cloud-adoption-framework/govern/cost-management/) | Cost is a primary concern for cloud users. Develop policies for cost control for all cloud platforms.\r\n| [Security Baseline](https://learn.microsoft.com/azure/cloud-adoption-framework/govern/security-baseline/) | Security is a complex subject, unique to each company. Once security requirements are established, cloud governance policies and enforcement apply those requirements across network, data, and asset configurations.\r\n| [Identity Baseline](https://learn.microsoft.com/azure/cloud-adoption-framework/govern/identity-baseline/) | Inconsistencies in the application of identity requirements can increase the risk of breach. The Identity Baseline discipline focuses on ensuring that identity is consistently applied across cloud adoption efforts.\r\n| [Resource Consistency](https://learn.microsoft.com/azure/cloud-adoption-framework/govern/resource-consistency/) | Cloud operations depend on consistent resource configuration. Through governance tooling, resources can be configured consistently to manage risks related to onboarding, drift, discoverability, and recovery.\r\n| [Deployment Acceleration](https://learn.microsoft.com/azure/cloud-adoption-framework/govern/deployment-acceleration/) | Centralization, standardization, and consistency in approaches to deployment and configuration improve governance practices. When provided through cloud-based governance tooling, they create a cloud factor that can accelerate deployment activities.\r\n\r\n* To assess your transformation journey, try the [governance benchmark tool](https://learn.microsoft.com/assessments/b1891add-7646-4d60-a875-32a4ab26327e).\r\n\r\n\r\n\r\n\r\n" + "json": "The objective of this workbook is to provide a comprehensive overview of the governance posture of your Azure environment. It offers the standard metrics aligned with the Cloud Adoption Framework and has the capability to identify and apply recommendations to identify non compliance. This workbook is part of the [FinOps toolkit](https://aka.ms/finops/toolkit).\r\n\r\n## Overview of the Cloud Adoption Framework\r\n\r\n* The CAF Govern methodology provides a structured approach for establishing and optimizing cloud governance in Azure. The guidance is relevant for organizations across any industry. It covers essential categories of cloud governance, such as regulatory compliance, security, operations, cost, data, resource management, and artificial intelligence (AI).\r\n\r\n* Cloud governance is how you control cloud use across your organization. Cloud governance sets up guardrails that regulate cloud interactions. These guardrails are a framework of policies, procedures, and tools you use to establish control. Policies define acceptable and unacceptable cloud activity, and the procedures and tools you use ensure all cloud usage aligns with those policies. Successful cloud governance prevents all unauthorized or unmanaged cloud usage.\r\n\r\n* To assess your transformation journey, try the [governance benchmark tool](https://learn.microsoft.com/assessments/b1891add-7646-4d60-a875-32a4ab26327e/).\r\n\r\n\r\n\r\n\r\n" }, "name": "text - Overview" }, @@ -309,7 +317,9 @@ "title": "Count of all resources", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "tileSettings": { "titleContent": { @@ -335,7 +345,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "gridSettings": { "formatters": [ @@ -355,7 +367,9 @@ "rowLimit": 10, "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true }, "labelSettings": [ @@ -415,7 +429,9 @@ }, "chartSettings": { "xAxis": "subscriptionId", - "yAxis": ["Count"], + "yAxis": [ + "Count" + ], "showLegend": true, "seriesLabelSettings": [ { @@ -450,7 +466,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "tileSettings": { "showBorder": false, @@ -486,7 +504,9 @@ "title": "Resource number by location", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "map", "mapSettings": { "locInfo": "AzureLoc", @@ -552,7 +572,9 @@ "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "tileSettings": { "showBorder": false, @@ -589,7 +611,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "barchart", "tileSettings": { "showBorder": false, @@ -646,7 +670,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "name": "query - virtual machine scale set capacity and size" }, @@ -661,7 +687,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "table", "gridSettings": { "formatters": [ @@ -724,7 +752,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "table", "gridSettings": { "formatters": [ @@ -783,7 +813,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "ec135d58-9c6b-4998-bd1e-75871c540d7f", @@ -797,9 +829,13 @@ "quote": "'", "delimiter": ",", "query": "where type =~ 'microsoft.operationalinsights/workspaces' | project id", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "timeContext": { @@ -828,7 +864,9 @@ }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", - "crossComponentResources": ["{laworkspace}"], + "crossComponentResources": [ + "{laworkspace}" + ], "visualization": "table", "gridSettings": { "formatters": [ @@ -914,7 +952,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "customWidth": "100", "name": "Underused assets" @@ -960,7 +1000,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -1043,7 +1085,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -1106,7 +1150,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "98d786aa-8835-493f-9fe4-fe5da150392b", @@ -1115,7 +1161,9 @@ "label": "Virtual machine state", "type": 2, "query": "resources\r\n| where type == \"microsoft.compute/virtualmachines\"\r\n| extend state = properties['extended']['instanceView']['powerState']['displayStatus']\r\n| summarize by tostring(state)", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -1155,7 +1203,9 @@ "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "tileSettings": { "titleContent": { @@ -1211,7 +1261,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -1344,7 +1396,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "gridSettings": { "formatters": [ @@ -1367,7 +1421,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true, "finalBy": "Resource" } @@ -1410,7 +1466,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -1443,7 +1501,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true, "finalBy": "Resource" } @@ -1467,17 +1527,23 @@ "quote": "'", "delimiter": ",", "query": "where type =~ 'microsoft.storage/storageaccounts'\n| order by name asc\n| extend Rank = row_number()\n| project value = id, label = id, selected = Rank <= 5", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "resourceTypeFilter": { "microsoft.storage/storageaccounts": true }, - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "value": ["value::all"] + "value": [ + "value::all" + ] }, { "id": "c4b69c01-2263-4ada-8d9c-43433b739ff3", @@ -1571,7 +1637,9 @@ "name": "Message", "type": 1, "query": "where type == 'microsoft.storage/storageaccounts' \n| summarize Selected = countif(id in ({Resources:value})), Total = count()\n| extend Selected = iff(Selected > 200, 200, Selected)\n| project Message = strcat('# ', Selected, ' / ', Total)", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "isHiddenWhenLocked": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources" @@ -1669,7 +1737,9 @@ "resourceType": "microsoft.storage/storageaccounts", "metricScope": 0, "resourceParameter": "Resources", - "resourceIds": ["{Resources}"], + "resourceIds": [ + "{Resources}" + ], "timeContextFromParameter": "TimeRange", "timeContext": { "durationMs": 172800000 @@ -1850,7 +1920,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["Subscription"], + "groupBy": [ + "Subscription" + ], "expandTopLevel": true, "finalBy": "Name" }, @@ -1932,7 +2004,9 @@ "resourceType": "microsoft.storage/storageaccounts", "metricScope": 0, "resourceParameter": "Resources", - "resourceIds": ["{Resources}"], + "resourceIds": [ + "{Resources}" + ], "timeContextFromParameter": "TimeRange", "timeContext": { "durationMs": 172800000 @@ -2060,7 +2134,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["Subscription"], + "groupBy": [ + "Subscription" + ], "expandTopLevel": true, "finalBy": "Name" }, @@ -2170,7 +2246,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "2373a24f-ad32-4909-a7f6-59b373dcde6c", @@ -2184,7 +2262,9 @@ "quote": "'", "delimiter": ",", "query": "where type =~ 'microsoft.operationalinsights/workspaces' | project id", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -2206,7 +2286,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Workspaces}"], + "crossComponentResources": [ + "{Workspaces}" + ], "parameters": [ { "id": "2965ad33-1401-47c9-8f4b-9b7126f87014", @@ -2258,9 +2340,13 @@ "quote": "", "delimiter": ",", "query": "let RangeStart = startofday({TimeRange:start});\r\nlet RangeEnd = iff(startofday({TimeRange:end}) == startofday(now()) ,startofday({TimeRange:end}) - 1d , startofday({TimeRange:end}));\r\nlet VaultSubscriptionList = \"*\";\r\nlet VaultLocationList = \"*\";\r\nlet VaultList = \"*\";\r\nlet VaultTypeList = \"*\";\r\nlet ExcludeLegacyEvent = true;\r\nlet BackupSolutionList = \"*\";\r\nlet ProtectionInfoList = \"*\";\r\nlet Item_search = \"*;*\";\r\nlet ItemArray = split(Item_search, \";\");\r\nlet ItemArray_length = array_length(ItemArray);\r\nlet BackupInstanceName = iff(ItemArray_length == 2, ItemArray[1], ItemArray[0] );\r\nlet DatasourceSetName = iff(ItemArray_length == 2, ItemArray[0], \"\");\r\nlet DisplayAllFields = false;\r\n_AzureBackup_GetBackupInstances(RangeStart, RangeEnd, VaultSubscriptionList, VaultLocationList, VaultList, VaultTypeList, ExcludeLegacyEvent, BackupSolutionList, ProtectionInfoList, DatasourceSetName, BackupInstanceName, DisplayAllFields)\r\n| distinct tostring(split(tostring(todynamic(DatasourceResourceId)),\"/\")[2])", - "crossComponentResources": ["{Workspaces}"], + "crossComponentResources": [ + "{Workspaces}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "selectAllValue": "*", "showDefault": false }, @@ -2280,15 +2366,21 @@ "quote": "", "delimiter": ",", "query": "let RangeStart = startofday({TimeRange:start});\r\nlet RangeEnd = iff(startofday({TimeRange:end}) == startofday(now()) ,startofday({TimeRange:end}) - 1d , startofday({TimeRange:end}));\r\nlet VaultSubscriptionList = todynamic( replace(\"/subscriptions/\", \"\", @\"{DataSourceSubscription}\"));\r\nlet VaultLocationList = \"*\";\r\nlet VaultList = \"*\";\r\nlet VaultTypeList = \"*\";\r\nlet ExcludeLegacyEvent = true;\r\nlet BackupSolutionList = \"*\";\r\nlet ProtectionInfoList = \"*\";\r\nlet Item_search = \"*;*\";\r\nlet ItemArray = split(Item_search, \";\");\r\nlet ItemArray_length = array_length(ItemArray);\r\nlet BackupInstanceName = iff(ItemArray_length == 2, ItemArray[1], ItemArray[0] );\r\nlet DatasourceSetName = iff(ItemArray_length == 2, ItemArray[0], \"\");\r\nlet DisplayAllFields = false;\r\n_AzureBackup_GetBackupInstances(RangeStart, RangeEnd, VaultSubscriptionList, VaultLocationList, VaultList, VaultTypeList, ExcludeLegacyEvent, BackupSolutionList, ProtectionInfoList, DatasourceSetName, BackupInstanceName, DisplayAllFields)\r\n| distinct VaultLocation", - "crossComponentResources": ["{Workspaces}"], + "crossComponentResources": [ + "{Workspaces}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "selectAllValue": "*", "showDefault": false }, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", - "value": ["value::all"] + "value": [ + "value::all" + ] }, { "id": "16ad110f-4ea3-44d6-826b-4ea3bbd68c93", @@ -2302,12 +2394,16 @@ "quote": "", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "selectAllValue": "*", "showDefault": false }, "jsonData": "\r\n[ \r\n{ \"value\": \"Backup\", \t\t\t\t\t\t\"label\": \"Backup\" },\r\n{ \"value\": \"Restore\", \t\t\t\t\t\t\"label\": \"Restore\" }\r\n]", - "value": ["value::all"] + "value": [ + "value::all" + ] }, { "id": "6a6222bf-a28a-4c98-9d74-838e74497167", @@ -2321,12 +2417,16 @@ "quote": "", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "selectAllValue": "*", "showDefault": false }, "jsonData": "\r\n[ \r\n{ \"value\": \"Completed\", \t\t\t\t\t\t\"label\": \"Completed\" },\r\n{ \"value\": \"Failed\", \t\t\t\"label\": \"Failed\" },\r\n\r\n{ \"value\": \"CompletedWithWarnings\", \t\t\t\t\t\t\"label\": \"CompletedWithWarnings\" },\r\n{ \"value\": \"Cancelled\", \"label\": \"Cancelled\" }\r\n]", - "value": ["value::all"] + "value": [ + "value::all" + ] }, { "id": "849a6401-cbaf-44b9-a733-0819f8923791", @@ -2371,7 +2471,9 @@ "showRefreshButton": true, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", - "crossComponentResources": ["{Workspaces}"], + "crossComponentResources": [ + "{Workspaces}" + ], "visualization": "piechart", "tileSettings": { "showBorder": false, @@ -2409,7 +2511,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Workspaces}"], + "crossComponentResources": [ + "{Workspaces}" + ], "parameters": [ { "id": "7a64467f-eec7-495b-9099-233fb7bceb08", @@ -2432,7 +2536,9 @@ "description": "Page number", "isRequired": true, "query": "\r\nlet RangeStart = startofday({TimeRange:start});\r\nlet RangeEnd = iff(startofday({TimeRange:end}) == startofday(now()) ,startofday({TimeRange:end}) - 1d , startofday({TimeRange:end}));\r\nlet DataSourceSubscriptionList = todynamic( replace(\"/subscriptions/\", \"\", @\"{DataSourceSubscription}\"));\r\nlet DataSourceLocationList = todynamic( @\"{DataSourceLocation}\"); \r\nlet VaultTypeList = \"*\";\r\nlet VaultList = \"*\";\r\nlet ExcludeLegacyEvent = true;\r\nlet BackupSolutionList = \"*\";\r\nlet ProtectionInfoList = \"*\";\r\nlet Item_search = \"*;*\";\r\nlet ItemArray = split(Item_search, \";\");\r\nlet ItemArray_length = array_length(ItemArray);\r\nlet BackupInstanceName = iff(ItemArray_length == 2, ItemArray[1], ItemArray[0] );\r\nlet DatasourceSetName = iff(ItemArray_length == 2, ItemArray[0], \"\");\r\nlet JobOperationList = todynamic( @\"{JobOperation}\"); \r\nlet JobStatusList = todynamic( @\"{JobStatus}\");\r\nlet JobFailureCodeList = \"*\";\r\nlet ExcludeLog = true; \r\nlet backupItem = '{SearchItem}';\r\n_AzureBackup_GetJobs(RangeStart, RangeEnd, DataSourceSubscriptionList, DataSourceLocationList, VaultList, VaultTypeList, ExcludeLegacyEvent, BackupSolutionList, JobOperationList, JobStatusList, JobFailureCodeList, DatasourceSetName, BackupInstanceName, ExcludeLog)\r\n| where BackupInstanceFriendlyName contains backupItem\r\n| summarize c=count()\r\n| project num = (c-1)/toint('{RowsPerPage}') + 1\r\n| project nums = range(1,num,1), num\r\n| mvexpand nums\r\n| project nums = tostring(nums), num = strcat(tostring(nums),\" of \",tostring(num))\r\n\r\n", - "crossComponentResources": ["{Workspaces}"], + "crossComponentResources": [ + "{Workspaces}" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -2466,7 +2572,9 @@ "showExportToExcel": true, "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", - "crossComponentResources": ["{Workspaces}"], + "crossComponentResources": [ + "{Workspaces}" + ], "visualization": "table", "gridSettings": { "formatters": [ @@ -2665,7 +2773,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "tileSettings": { "showBorder": false, @@ -2746,7 +2856,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "filter": true }, @@ -2764,7 +2876,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -2838,7 +2952,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "table", "gridSettings": { "formatters": [ @@ -2872,7 +2988,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true, "finalBy": "Resource" } @@ -2927,7 +3045,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "name": "query - Application Gateways" }, @@ -2978,7 +3098,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -3067,7 +3189,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -3128,7 +3252,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -3169,7 +3295,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -3227,7 +3355,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "bae67738-90ef-4698-9020-5e1f91d67f82", @@ -3237,7 +3367,9 @@ "type": 2, "isRequired": true, "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -3261,7 +3393,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "cb0ae78d-a49b-457b-baed-d83c97a2c934", @@ -3270,7 +3404,9 @@ "label": "Tag value", "type": 2, "query": "Resources\r\n| extend TagValue = tostring(tags.{TagName})\r\n| project TagValue\r\n| distinct TagValue", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -3336,7 +3472,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "filter": true, "labelSettings": [ @@ -3370,7 +3508,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "table", "gridSettings": { "filter": true, @@ -3405,7 +3545,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -3438,7 +3580,9 @@ "title": "Subscription list", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -3473,7 +3617,9 @@ "title": "Resource groups list", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -3531,7 +3677,9 @@ "title": "Security Scores by Subscription", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -3584,7 +3732,9 @@ "title": "Security Scores by Control", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -3680,7 +3830,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true, "finalBy": "SecureControl" }, @@ -3772,7 +3924,9 @@ ], "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart", "gridSettings": { "formatters": [ @@ -3783,7 +3937,9 @@ ] }, "chartSettings": { - "yAxis": ["Count"], + "yAxis": [ + "Count" + ], "seriesLabelSettings": [ { "seriesName": "Medium", @@ -3814,7 +3970,9 @@ "exportParameterName": "resourceGroupFilter", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart" }, "customWidth": "33", @@ -3829,7 +3987,9 @@ "title": "Tag", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart" }, "customWidth": "30", @@ -3860,7 +4020,9 @@ ], "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "table", "gridSettings": { "formatters": [ @@ -3917,7 +4079,9 @@ ], "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "table", "gridSettings": { "sortBy": [ @@ -3966,7 +4130,9 @@ ], "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -4043,7 +4209,9 @@ ], "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -4373,7 +4541,9 @@ "size": 0, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "barchart", "tileSettings": { "showBorder": false, @@ -4462,7 +4632,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "1ffc8fe9-a919-4c9e-8489-a92f0a7d79e1", @@ -4474,9 +4646,13 @@ "quote": "'", "delimiter": ",", "query": "securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | extend Prop = parse_json(properties)\r\n | extend ResourceIdentifiers = Prop.[\"ResourceIdentifiers\"]\r\n | project ResourceIdentifiers\r\n | mv-expand ResourceIdentifiers\r\n | extend ResourceId = parse_json(ResourceIdentifiers).[\"AzureResourceId\"]\r\n //| where isnotempty(ResourceId )\r\n | extend Resource = tolower(tostring(ResourceId))\r\n | summarize count() by Resource\r\n | project Resource\r\n //| order by Resource asc\r\n", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "timeContext": { @@ -4485,7 +4661,9 @@ "defaultValue": "value::all", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "value": ["value::all"] + "value": [ + "value::all" + ] }, { "id": "e9522d87-143f-408b-93ea-b8f07223995e", @@ -4496,9 +4674,13 @@ "multiSelect": true, "quote": "'", "delimiter": ",", - "value": ["value::all"], + "value": [ + "value::all" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "jsonData": "[\r\n\r\n{\"value\": \"High\", \"label\":\"High\"},\r\n{\"value\": \"Medium\", \"label\":\"Medium\"},\r\n{\"value\": \"Low\", \"label\":\"Low\"},\r\n{\"value\": \"Informational\", \"label\":\"Informational\"}\r\n]\r\n \r\n ", @@ -4517,9 +4699,13 @@ "quote": "'", "delimiter": ",", "query": " securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | extend resourceGroup = iif(isempty(resourceGroup),\" \",resourceGroup)\r\n| summarize Count =count() by resourceGroup\r\n | project resourceGroup", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "timeContext": { @@ -4528,7 +4714,9 @@ "defaultValue": "value::all", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "value": ["value::all"] + "value": [ + "value::all" + ] }, { "id": "48a8dd7e-43ab-413e-88f8-a433100d92ce", @@ -4540,9 +4728,13 @@ "quote": "'", "delimiter": ",", "query": " securityresources\r\n | where type =~ 'microsoft.security/locations/alerts'\r\n | extend Prop = parse_json(properties)\r\n | extend AlertDisplayName = Prop.[\"AlertDisplayName\"]\r\n | distinct tostring(AlertDisplayName)\r\n | order by AlertDisplayName asc\r\n ", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "defaultValue": "value::all", @@ -4559,9 +4751,13 @@ "quote": "'", "delimiter": ",", "query": "securityresources\r\n | where type =~ \"microsoft.security/locations/alerts\"\r\n | extend isAzure = tostring(properties.ResourceIdentifiers) matches regex '\"Type\"\\\\s*:\\\\s*\"AzureResource\"'\r\n | extend Details = parse_json(properties)\r\n| extend IsIncident = Details.[\"IsIncident\"]\r\n| extend AlertDisplayName = Details.[\"AlertDisplayName\"]\r\n| extend SystemAlertId = Details.[\"SystemAlertId\"]\r\n| extend Severity = Details.[\"Severity\"]\r\n| extend AlertUri = Details.[\"AlertUri\"]\r\n| extend Status = Details.[\"Status\"]\r\n| extend Tactics = Details.[\"Intent\"]\r\n| parse AlertUri with * '/subscriptionId/' SubscriptionId '/' *\r\n| parse AlertUri with * '/resourceGroup/' ResourceGroup '/' *\r\n| parse AlertUri with * '/location/' Location \r\n | extend affectedResourceId = extract('\"AzureResourceId\"\\\\s*:\\\\s*\"([^\"]*)\"', 1, tostring(properties.ResourceIdentifiers))\r\n | extend hostName = iff(isAzure, \"\", extract('\"HostName\"\\\\s*:\\\\s*\"([^\"]*)\"', 1, tostring(properties.Entities)))\r\n | extend splitAffectedResourceId = split(affectedResourceId, \"/\")\r\n | extend resourceNameIndex = iff(array_length(splitAffectedResourceId) > 1, array_length(splitAffectedResourceId) - 1, 0)\r\n | extend affectedResourceName = iff(isAzure, splitAffectedResourceId[resourceNameIndex], iff(isempty(hostName), \"Non-Azure\", hostName))| project-away resourceNameIndex, splitAffectedResourceId, hostName\r\n | extend ResourceIdentifiers = Details.[\"ResourceIdentifiers\"]\r\n | mv-expand ResourceIdentifiers\r\n | extend ResourceId = parse_json(ResourceIdentifiers).[\"AzureResourceId\"]\r\n | extend Resource = tolower(tostring(ResourceId))\r\n | project alertId = id, subscriptionId, alertProperties = properties, affectedResourceId = tolower(affectedResourceId),tostring(Severity), SystemAlertId, AlertDisplayName,IsIncident = iif(IsIncident==\"true\",\"Incident\",\"Alert\"),AlertUri,Status,Tactics,SubscriptionId,ResourceGroup,Location, ResourceIdentifier=Details.[\"ResourceIdentifiers\"],Resource\r\n | join kind=leftouter (\r\n resources\r\n | project id = tolower(id), tags\r\n ) on $left.affectedResourceId == $right.id\r\n | extend Tag = parse_json(tags)\r\n | mv-expand Tag\r\n | parse Tag with * ':\"' TagValue '\"}'\r\n | extend TagValue = iif(isempty(TagValue),\" \",TagValue)\r\n | project TagValue, alertId\r\n | distinct TagValue\r\n ", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "defaultValue": "value::all", @@ -4621,7 +4817,9 @@ ], "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -4945,7 +5143,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "8724f927-b766-4814-a895-8c55565fb7f8", @@ -4956,9 +5156,13 @@ "quote": "'", "delimiter": ",", "query": "resources\r\n| where type contains \"solution\"\r\n| where name contains \"security\"\r\n| project id = tostring(properties.workspaceResourceId)\r\n| distinct id", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "timeContext": { @@ -5008,7 +5212,9 @@ "exportToExcelOptions": "all", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "map", "mapSettings": { "locInfo": "AzureResource", @@ -5066,7 +5272,9 @@ ], "queryType": 0, "resourceType": "microsoft.operationalinsights/workspaces", - "crossComponentResources": ["{Workspace}"], + "crossComponentResources": [ + "{Workspace}" + ], "visualization": "table", "gridSettings": { "formatters": [ @@ -5497,7 +5705,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "6cc7fc26-1a56-41cb-ad43-301e0f9f8903", @@ -5507,7 +5717,9 @@ "type": 2, "isRequired": true, "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -5527,7 +5739,9 @@ "type": 2, "isRequired": true, "query": "Resources\r\n| extend TagValue = tostring(tags.{TagName})\r\n| project TagValue\r\n| distinct TagValue", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -5638,7 +5852,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["value::tenant"], + "crossComponentResources": [ + "value::tenant" + ], "parameters": [ { "id": "f967854d-c31e-46a6-b8b0-6b9fff7ce582", @@ -5647,7 +5863,9 @@ "label": "Management group", "type": 2, "query": "resourcecontainers\r\n| where type == \"microsoft.management/managementgroups\"\r\n| extend name = properties.displayName\r\n| project name", - "crossComponentResources": ["value::tenant"], + "crossComponentResources": [ + "value::tenant" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -5666,7 +5884,9 @@ "type": 6, "isRequired": true, "query": "resourcecontainers\r\n| where type == \"microsoft.resources/subscriptions\"\r\n| where properties['managementGroupAncestorsChain'] contains '{ManagementGroup:label}'", - "crossComponentResources": ["value::tenant"], + "crossComponentResources": [ + "value::tenant" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -5714,7 +5934,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resources/tenants", - "crossComponentResources": ["value::tenant"], + "crossComponentResources": [ + "value::tenant" + ], "visualization": "table", "gridSettings": { "formatters": [ @@ -5856,7 +6078,9 @@ }, "queryType": 0, "resourceType": "microsoft.resources/subscriptions", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart", "tileSettings": { "showBorder": false, @@ -5914,7 +6138,9 @@ }, "queryType": 0, "resourceType": "microsoft.resources/subscriptions", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart", "tileSettings": { "showBorder": false, @@ -5972,7 +6198,9 @@ }, "queryType": 0, "resourceType": "microsoft.resources/subscriptions", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart", "tileSettings": { "showBorder": false, @@ -6030,7 +6258,9 @@ "showExportToExcel": true, "queryType": 0, "resourceType": "microsoft.resources/subscriptions", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -6090,7 +6320,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "4168a8b2-a522-4f0d-9575-893d70d9239d", @@ -6099,7 +6331,9 @@ "type": 1, "description": "Count of the governance rule, when there is no rules, empty state will be shown", "query": "securityresources\r\n| where type == \"microsoft.security/governancerules\"\r\n| where tostring(properties.isDisabled) == \"false\"\r\n| count", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "isHiddenWhenLocked": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources" @@ -6146,7 +6380,9 @@ "noDataMessage": "No unhealthy resources found", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart", "tileSettings": { "titleContent": { @@ -6245,7 +6481,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -6457,7 +6695,9 @@ "noDataMessage": "No unhealthy resources found", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart", "gridSettings": { "rowLimit": 10000 @@ -6548,7 +6788,9 @@ ], "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -6638,7 +6880,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -6934,7 +7178,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -7263,7 +7509,9 @@ "type": 2, "isRequired": true, "query": "resources\r\n| summarize by location\r\n| where location != \"global\"", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [], "showDefault": false @@ -7444,7 +7692,9 @@ "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "tileSettings": { "showBorder": false, @@ -7482,7 +7732,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -7515,7 +7767,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true, "finalBy": "Resource" } @@ -7573,7 +7827,9 @@ "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "gridSettings": { "formatters": [ @@ -7596,7 +7852,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true, "finalBy": "Resource" } @@ -7638,7 +7896,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -7671,7 +7931,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true, "finalBy": "Resource" } @@ -7729,7 +7991,9 @@ "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "tiles", "gridSettings": { "formatters": [ @@ -7752,7 +8016,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true, "finalBy": "Resource" } @@ -7795,7 +8061,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -7838,7 +8106,9 @@ "rowLimit": 1000, "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true, "finalBy": "Resource" } @@ -7943,14 +8213,16 @@ "name": "Azure Governance Workbook" } ], - "fallbackResourceIds": ["azure monitor"], + "fallbackResourceIds": [ + "azure monitor" + ], "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" }, "version": "", "workbookJson": "[string(variables('$fxv#0'))]", "workbookId": "907", "telemetryId": "[format('00f120b5-2007-6120-0000-{0}30126b006', variables('workbookId'))]", - "finOpsToolkitVersion": "0.3", + "finOpsToolkitVersion": "0.4", "resourceTags": "[union(parameters('tags'), createObject('ftk-version', variables('finOpsToolkitVersion'), 'ftk-tool', format('{0} workbook', parameters('displayName'))))]" }, "resources": [ @@ -8007,4 +8279,4 @@ "value": "[format('{0}/#view/AppInsightsExtension/UsageNotebookBlade/ComponentId/Azure%20Monitor/ConfigurationId/{1}/Type/{2}/WorkbookTemplateName/{3}', environment().portal, uriComponent(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName')))), reference(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))), '2022-04-01').category, uriComponent(reference(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))), '2022-04-01').displayName))]" } } -} +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/azuredeploy-nested.bicep b/docs/deploy/optimization-engine/0.4/azuredeploy-nested.bicep new file mode 100644 index 000000000..cfed41bd4 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/azuredeploy-nested.bicep @@ -0,0 +1,2147 @@ +param projectLocation string +param templateLocation string + +param storageAccountName string +param automationAccountName string +param sqlServerName string +param sqlDatabaseName string +param logAnalyticsReuse bool +param logAnalyticsWorkspaceName string +param logAnalyticsWorkspaceRG string +param logAnalyticsRetentionDays int +param sqlBackupRetentionDays int +param sqlAdminLogin string + +@secure() +param sqlAdminPassword string +param cloudEnvironment string +param authenticationOption string +param baseTime string +param resourceTags object +param contributorRoleAssignmentGuid string + +param argDiskExportJobId string = newGuid() +param argVhdExportJobId string = newGuid() +param argVmExportJobId string = newGuid() +param argVmssExportJobId string = newGuid() +param argAvailSetExportJobId string = newGuid() +param advisorExportJobId string = newGuid() +param consumptionExportJobId string = newGuid() +param aadObjectsExportJobId string = newGuid() +param argLoadBalancersExportJobId string = newGuid() +param argAppGWsExportJobId string = newGuid() +param rbacExportJobId string = newGuid() +param argResContainersExportJobId string = newGuid() +param argNICExportJobId string = newGuid() +param argNSGExportJobId string = newGuid() +param argPublicIPExportJobId string = newGuid() +param argVNetExportJobId string = newGuid() +param argSqlDbExportJobId string = newGuid() +param policyStateExportJobId string = newGuid() +param monitorVmssCpuMaxExportJobId string = newGuid() +param monitorVmssCpuAvgExportJobId string = newGuid() +param monitorVmssMemoryMinExportJobId string = newGuid() +param monitorSqlDbDtuMaxExportJobId string = newGuid() +param monitorSqlDbDtuAvgExportJobId string = newGuid() +param monitorAppServiceCpuMaxExportJobId string = newGuid() +param monitorAppServiceCpuAvgExportJobId string = newGuid() +param monitorAppServiceMemoryMaxExportJobId string = newGuid() +param monitorAppServiceMemoryAvgExportJobId string = newGuid() +param monitorDiskIOPSAvgExportJobId string = newGuid() +param monitorDiskMBPsAvgExportJobId string = newGuid() +param argAppServicePlanExportJobId string = newGuid() +param pricesheetExportJobId string = newGuid() +param reservationPricesExportJobId string = newGuid() +param reservationUsageExportJobId string = newGuid() +param savingsPlansUsageExportJobId string = newGuid() +param argDiskIngestJobId string = newGuid() +param argVhdIngestJobId string = newGuid() +param argVmIngestJobId string = newGuid() +param argVmssIngestJobId string = newGuid() +param argAvailSetIngestJobId string = newGuid() +param advisorIngestJobId string = newGuid() +param remediationLogsIngestJobId string = newGuid() +param consumptionIngestJobId string = newGuid() +param aadObjectsIngestJobId string = newGuid() +param argLoadBalancersIngestJobId string = newGuid() +param argAppGWsIngestJobId string = newGuid() +param argResContainersIngestJobId string = newGuid() +param rbacIngestJobId string = newGuid() +param argNICIngestJobId string = newGuid() +param argNSGIngestJobId string = newGuid() +param argPublicIPIngestJobId string = newGuid() +param argVNetIngestJobId string = newGuid() +param argSqlDbIngestJobId string = newGuid() +param policyStateIngestJobId string = newGuid() +param monitorIngestJobId string = newGuid() +param argAppServicePlanIngestJobId string = newGuid() +param pricesheetIngestJobId string = newGuid() +param reservationPricesIngestJobId string = newGuid() +param reservationUsageIngestJobId string = newGuid() +param savingsPlansUsageIngestJobId string = newGuid() +param unattachedDisksRecommendationJobId string = newGuid() +param advisorCostAugmentedRecommendationJobId string = newGuid() +param advisorAsIsRecommendationJobId string = newGuid() +param vmsHaRecommendationJobId string = newGuid() +param vmOptimizationsRecommendationJobId string = newGuid() +param aadExpiringCredsRecommendationJobId string = newGuid() +param unusedLoadBalancersRecommendationJobId string = newGuid() +param unusedAppGWsRecommendationJobId string = newGuid() +param armOptimizationsRecommendationJobId string = newGuid() +param vnetOptimizationsRecommendationJobId string = newGuid() +param vmssOptimizationsRecommendationJobId string = newGuid() +param sqldbOptimizationsRecommendationJobId string = newGuid() +param storageOptimizationsRecommendationJobId string = newGuid() +param appServiceOptimizationsRecommendationJobId string = newGuid() +param diskOptimizationsRecommendationJobId string = newGuid() +param recommendationsIngestJobId string = newGuid() +param recommendationsLogAnalyticsIngestJobId string = newGuid() +param suppressionsLogAnalyticsIngestJobId string = newGuid() +param recommendationsCleanUpJobId string = newGuid() + +param roleContributor string = '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c' + +@description('Optional. Enable telemetry to track anonymous module usage trends, monitor for bugs, and improve future releases.') +param enableDefaultTelemetry bool = true + +var telemetryId = '00f120b5-2007-6120-0000-000000000a0e' +var finOpsToolkitVersion = loadTextContent('ftkver.txt') +var advisorExportsRunbookName = 'Export-AdvisorRecommendationsToBlobStorage' +var argVmExportsRunbookName = 'Export-ARGVirtualMachinesPropertiesToBlobStorage' +var argVmssExportsRunbookName = 'Export-ARGVMSSPropertiesToBlobStorage' +var argDisksExportsRunbookName = 'Export-ARGManagedDisksPropertiesToBlobStorage' +var argVhdExportsRunbookName = 'Export-ARGUnmanagedDisksPropertiesToBlobStorage' +var argAvailSetExportsRunbookName = 'Export-ARGAvailabilitySetPropertiesToBlobStorage' +var consumptionExportsRunbookName = 'Export-ConsumptionToBlobStorage' +var aadObjectsExportsRunbookName = 'Export-AADObjectsToBlobStorage' +var argLoadBalancersExportsRunbookName = 'Export-ARGLoadBalancerPropertiesToBlobStorage' +var argAppGWsExportsRunbookName = 'Export-ARGAppGatewayPropertiesToBlobStorage' +var argResContainersExportsRunbookName = 'Export-ARGResourceContainersPropertiesToBlobStorage' +var rbacExportsRunbookName = 'Export-RBACAssignmentsToBlobStorage' +var argNICExportsRunbookName = 'Export-ARGNICPropertiesToBlobStorage' +var argNSGExportsRunbookName = 'Export-ARGNSGPropertiesToBlobStorage' +var argVNetExportsRunbookName = 'Export-ARGVNetPropertiesToBlobStorage' +var argPublicIpExportsRunbookName = 'Export-ARGPublicIpPropertiesToBlobStorage' +var argSqlDbExportsRunbookName = 'Export-ARGSqlDatabasePropertiesToBlobStorage' +var policyStateExportsRunbookName = 'Export-PolicyComplianceToBlobStorage' +var monitorExportsRunbookName = 'Export-AzMonitorMetricsToBlobStorage' +var argAppServicePlanExportsRunbookName = 'Export-ARGAppServicePlanPropertiesToBlobStorage' +var reservationsExportsRunbookName = 'Export-ReservationsUsageToBlobStorage' +var reservationsPriceExportsRunbookName = 'Export-ReservationsPriceToBlobStorage' +var priceSheetExportsRunbookName = 'Export-PriceSheetToBlobStorage' +var savingsPlansExportsRunbookName = 'Export-SavingsPlansUsageToBlobStorage' +var advisorExportsScheduleName = 'AzureOptimization_ExportAdvisorWeekly' +var argExportsScheduleName = 'AzureOptimization_ExportARGDaily' +var consumptionExportsScheduleName = 'AzureOptimization_ExportConsumptionDaily' +var aadObjectsExportsScheduleName = 'AzureOptimization_ExportAADObjectsDaily' +var rbacExportsScheduleName = 'AzureOptimization_ExportRBACDaily' +var policyStateExportsScheduleName = 'AzureOptimization_ExportPolicyStateDaily' +var monitorVmssCpuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorVmssCpuMaxHourly' +var monitorVmssCpuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorVmssCpuAvgHourly' +var monitorVmssMemoryMinExportsScheduleName = 'AzureOptimization_ExportMonitorVmssMemoryMinHourly' +var monitorSqlDbDtuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorSqlDbDtuMaxHourly' +var monitorSqlDbDtuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorSqlDbDtuAvgHourly' +var monitorAppServiceCpuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceCpuMaxHourly' +var monitorAppServiceCpuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceCpuAvgHourly' +var monitorAppServiceMemoryMaxExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceMemoryMaxHourly' +var monitorAppServiceMemoryAvgExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceMemoryAvgHourly' +var monitorDiskIOPSAvgExportsScheduleName = 'AzureOptimization_ExportMonitorDiskIOPSHourly' +var monitorDiskMBPsAvgExportsScheduleName = 'AzureOptimization_ExportMonitorDiskMBPsHourly' +var priceExportsScheduleName = 'AzureOptimization_ExportPricesWeekly' +var reservationsUsageExportsScheduleName = 'AzureOptimization_ExportReservationsDaily' +var savingsPlansUsageExportsScheduleName = 'AzureOptimization_ExportSavingsPlansDaily' +var csvExportsSchedules = [ + { + exportSchedule: argExportsScheduleName + exportDescription: 'Daily Azure Resource Graph exports' + exportTimeOffset: 'PT1H05M' + exportFrequency: 'Day' + } + { + exportSchedule: advisorExportsScheduleName + exportDescription: 'Weekly Azure Advisor exports' + exportTimeOffset: 'PT1H15M' + exportFrequency: 'Week' + } + { + exportSchedule: consumptionExportsScheduleName + exportDescription: 'Daily Azure Consumption exports' + exportTimeOffset: 'PT1H' + exportFrequency: 'Day' + } + { + exportSchedule: aadObjectsExportsScheduleName + exportDescription: 'Daily Microsoft Entra Objects exports' + exportTimeOffset: 'PT1H' + exportFrequency: 'Day' + } + { + exportSchedule: rbacExportsScheduleName + exportDescription: 'Daily Azure RBAC exports' + exportTimeOffset: 'PT1H02M' + exportFrequency: 'Day' + } + { + exportSchedule: policyStateExportsScheduleName + exportDescription: 'Daily Azure Policy State exports' + exportTimeOffset: 'PT1H' + exportFrequency: 'Day' + } + { + exportSchedule: monitorVmssCpuAvgExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Percentage CPU (Avg.)' + exportTimeOffset: 'PT1H15M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorVmssCpuMaxExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Percentage CPU (Max.)' + exportTimeOffset: 'PT1H15M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorVmssMemoryMinExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Available Memory (Min.)' + exportTimeOffset: 'PT1H15M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorSqlDbDtuMaxExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for SQL Database Percentage DTU (Max.)' + exportTimeOffset: 'PT1H15M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorSqlDbDtuAvgExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for SQL Database Percentage DTU (Avg.)' + exportTimeOffset: 'PT1H16M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorAppServiceCpuAvgExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage CPU (Avg.)' + exportTimeOffset: 'PT1H16M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorAppServiceCpuMaxExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage CPU (Max.)' + exportTimeOffset: 'PT1H16M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorAppServiceMemoryAvgExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage RAM (Avg.)' + exportTimeOffset: 'PT1H16M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorAppServiceMemoryMaxExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage RAM (Max.)' + exportTimeOffset: 'PT1H17M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorDiskIOPSAvgExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for Disk IOPS (Avg.)' + exportTimeOffset: 'PT1H17M' + exportFrequency: 'Hour' + } + { + exportSchedule: monitorDiskMBPsAvgExportsScheduleName + exportDescription: 'Hourly Azure Monitor metrics exports for Disk MBPs (Avg.)' + exportTimeOffset: 'PT1H17M' + exportFrequency: 'Hour' + } + { + exportSchedule: priceExportsScheduleName + exportDescription: 'Weekly Pricesheet and Reservation Prices exports' + exportTimeOffset: 'PT1H35M' + exportFrequency: 'Week' + } + { + exportSchedule: reservationsUsageExportsScheduleName + exportDescription: 'Daily Reservation Usage exports' + exportTimeOffset: 'PT2H' + exportFrequency: 'Day' + } + { + exportSchedule: savingsPlansUsageExportsScheduleName + exportDescription: 'Daily Savings Plans Usage exports' + exportTimeOffset: 'PT2H05M' + exportFrequency: 'Day' + } +] +var csvExports = [ + { + runbookName: advisorExportsRunbookName + isOneToMany: false + containerName: 'advisorexports' + variableName: 'AzureOptimization_AdvisorContainer' + variableDescription: 'The Storage Account container where Azure Advisor exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestAdvisorWeekly' + ingestDescription: 'Weekly Azure Advisor recommendations ingests' + ingestTimeOffset: 'PT1H45M' + ingestFrequency: 'Week' + ingestJobId: advisorIngestJobId + exportSchedule: advisorExportsScheduleName + exportJobId: advisorExportJobId + } + { + runbookName: argVmExportsRunbookName + isOneToMany: false + containerName: 'argvmexports' + variableName: 'AzureOptimization_ARGVMContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph Virtual Machine exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGVMsDaily' + ingestDescription: 'Daily Azure Resource Graph Virtual Machines ingests' + ingestTimeOffset: 'PT1H30M' + ingestFrequency: 'Day' + ingestJobId: argVmIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argVmExportJobId + } + { + runbookName: argVmssExportsRunbookName + isOneToMany: false + containerName: 'argvmssexports' + variableName: 'AzureOptimization_ARGVMSSContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph VMSS exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGVMSSDaily' + ingestDescription: 'Daily Azure Resource Graph VMSS ingests' + ingestTimeOffset: 'PT1H30M' + ingestFrequency: 'Day' + ingestJobId: argVmssIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argVmssExportJobId + } + { + runbookName: argDisksExportsRunbookName + isOneToMany: false + containerName: 'argdiskexports' + variableName: 'AzureOptimization_ARGDiskContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph Managed Disks exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGDisksDaily' + ingestDescription: 'Daily Azure Resource Graph Managed Disks ingests' + ingestTimeOffset: 'PT1H30M' + ingestFrequency: 'Day' + ingestJobId: argDiskIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argDiskExportJobId + } + { + runbookName: argVhdExportsRunbookName + isOneToMany: false + containerName: 'argvhdexports' + variableName: 'AzureOptimization_ARGVhdContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph Unmanaged Disks exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGVHDsDaily' + ingestDescription: 'Daily Azure Resource Graph Unmanaged Disks ingests' + ingestTimeOffset: 'PT1H30M' + ingestFrequency: 'Day' + ingestJobId: argVhdIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argVhdExportJobId + } + { + runbookName: argAvailSetExportsRunbookName + isOneToMany: false + containerName: 'argavailsetexports' + variableName: 'AzureOptimization_ARGAvailabilitySetContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph Availability Set exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGAvailSetsDaily' + ingestDescription: 'Daily Azure Resource Graph Availability Sets ingests' + ingestTimeOffset: 'PT1H31M' + ingestFrequency: 'Day' + ingestJobId: argAvailSetIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argAvailSetExportJobId + } + { + runbookName: consumptionExportsRunbookName + isOneToMany: false + containerName: 'consumptionexports' + variableName: 'AzureOptimization_ConsumptionContainer' + variableDescription: 'The Storage Account container where Azure Consumption exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestConsumptionDaily' + ingestDescription: 'Daily Azure Consumption ingests' + ingestTimeOffset: 'PT2H' + ingestFrequency: 'Day' + ingestJobId: consumptionIngestJobId + exportSchedule: consumptionExportsScheduleName + exportJobId: consumptionExportJobId + } + { + runbookName: aadObjectsExportsRunbookName + isOneToMany: false + containerName: 'aadobjectsexports' + variableName: 'AzureOptimization_AADObjectsContainer' + variableDescription: 'The Storage Account container where Microsoft Entra Objects exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestAADObjectsDaily' + ingestDescription: 'Daily Microsoft Entra Objects ingests' + ingestTimeOffset: 'PT2H' + ingestFrequency: 'Day' + ingestJobId: aadObjectsIngestJobId + exportSchedule: aadObjectsExportsScheduleName + exportJobId: aadObjectsExportJobId + } + { + runbookName: argLoadBalancersExportsRunbookName + isOneToMany: false + containerName: 'arglbexports' + variableName: 'AzureOptimization_ARGLoadBalancerContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph Load Balancer exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGLoadBalancersDaily' + ingestDescription: 'Daily Azure Resource Graph Load Balancers ingests' + ingestTimeOffset: 'PT1H31M' + ingestFrequency: 'Day' + ingestJobId: argLoadBalancersIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argLoadBalancersExportJobId + } + { + runbookName: argAppGWsExportsRunbookName + isOneToMany: false + containerName: 'argappgwexports' + variableName: 'AzureOptimization_ARGAppGatewayContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph Application Gateway exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGAppGWsDaily' + ingestDescription: 'Daily Azure Resource Graph Application Gateways ingests' + ingestTimeOffset: 'PT1H31M' + ingestFrequency: 'Day' + ingestJobId: argAppGWsIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argAppGWsExportJobId + } + { + runbookName: argResContainersExportsRunbookName + isOneToMany: false + containerName: 'argrescontainersexports' + variableName: 'AzureOptimization_ARGResourceContainersContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph Resource Containers exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGResourceContainersDaily' + ingestDescription: 'Daily Azure Resource Graph Resource Containers ingests' + ingestTimeOffset: 'PT1H32M' + ingestFrequency: 'Day' + ingestJobId: argResContainersIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argResContainersExportJobId + } + { + runbookName: rbacExportsRunbookName + isOneToMany: false + containerName: 'rbacexports' + variableName: 'AzureOptimization_RBACAssignmentsContainer' + variableDescription: 'The Storage Account container where RBAC Assignments exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestRBACDaily' + ingestDescription: 'Daily Azure RBAC ingests' + ingestTimeOffset: 'PT1H32M' + ingestFrequency: 'Day' + ingestJobId: rbacIngestJobId + exportSchedule: rbacExportsScheduleName + exportJobId: rbacExportJobId + } + { + runbookName: argNICExportsRunbookName + isOneToMany: false + containerName: 'argnicexports' + variableName: 'AzureOptimization_ARGNICContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph NIC exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGNICsDaily' + ingestDescription: 'Daily Azure Resource Graph NIC ingests' + ingestTimeOffset: 'PT1H32M' + ingestFrequency: 'Day' + ingestJobId: argNICIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argNICExportJobId + } + { + runbookName: argNSGExportsRunbookName + isOneToMany: false + containerName: 'argnsgexports' + variableName: 'AzureOptimization_ARGNSGContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph NSG exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGNSGsDaily' + ingestDescription: 'Daily Azure Resource Graph NSG ingests' + ingestTimeOffset: 'PT1H32M' + ingestFrequency: 'Day' + ingestJobId: argNSGIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argNSGExportJobId + } + { + runbookName: argVNetExportsRunbookName + isOneToMany: false + containerName: 'argvnetexports' + variableName: 'AzureOptimization_ARGVNetContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph VNet exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGVNetsDaily' + ingestDescription: 'Daily Azure Resource Graph Virtual Network ingests' + ingestTimeOffset: 'PT1H33M' + ingestFrequency: 'Day' + ingestJobId: argVNetIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argVNetExportJobId + } + { + runbookName: argPublicIpExportsRunbookName + isOneToMany: false + containerName: 'argpublicipexports' + variableName: 'AzureOptimization_ARGPublicIpContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph Public IP exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGPublicIPsDaily' + ingestDescription: 'Daily Azure Resource Graph Public IP ingests' + ingestTimeOffset: 'PT1H33M' + ingestFrequency: 'Day' + ingestJobId: argPublicIPIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argPublicIPExportJobId + } + { + runbookName: argSqlDbExportsRunbookName + isOneToMany: false + containerName: 'argsqldbexports' + variableName: 'AzureOptimization_ARGSqlDatabaseContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph SQL DB exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGSqlDbDaily' + ingestDescription: 'Daily Azure Resource Graph SQL DB ingests' + ingestTimeOffset: 'PT1H33M' + ingestFrequency: 'Day' + ingestJobId: argSqlDbIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argSqlDbExportJobId + } + { + runbookName: policyStateExportsRunbookName + isOneToMany: false + containerName: 'policystateexports' + variableName: 'AzureOptimization_PolicyStatesContainer' + variableDescription: 'The Storage Account container where Azure Policy State exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestPolicyStateDaily' + ingestDescription: 'Daily Azure Policy State ingests' + ingestTimeOffset: 'PT1H33M' + ingestFrequency: 'Day' + ingestJobId: policyStateIngestJobId + exportSchedule: policyStateExportsScheduleName + exportJobId: policyStateExportJobId + } + { + runbookName: monitorExportsRunbookName + isOneToMany: true + containerName: 'azmonitorexports' + variableName: 'AzureOptimization_AzMonitorContainer' + variableDescription: 'The Storage Account container where Azure Monitor metrics exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestAzMonitorMetricsHourly' + ingestDescription: 'Hourly Azure Monitor metrics ingests' + ingestTimeOffset: 'PT2H' + ingestFrequency: 'Hour' + ingestJobId: monitorIngestJobId + exportSchedule: null + exportJobId: 'dummy' + } + { + runbookName: argAppServicePlanExportsRunbookName + isOneToMany: false + containerName: 'argappserviceplanexports' + variableName: 'AzureOptimization_ARGAppServicePlanContainer' + variableDescription: 'The Storage Account container where Azure Resource Graph App Service Plan exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestARGAppServicePlanDaily' + ingestDescription: 'Daily Azure Resource Graph App Service Plan ingests' + ingestTimeOffset: 'PT1H34M' + ingestFrequency: 'Day' + ingestJobId: argAppServicePlanIngestJobId + exportSchedule: argExportsScheduleName + exportJobId: argAppServicePlanExportJobId + } + { + runbookName: priceSheetExportsRunbookName + isOneToMany: false + containerName: 'pricesheetexports' + variableName: 'AzureOptimization_PriceSheetContainer' + variableDescription: 'The Storage Account container where Pricesheet exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestPricesheetWeekly' + ingestDescription: 'Weekly Pricesheet ingests' + ingestTimeOffset: 'PT2H' + ingestFrequency: 'Week' + ingestJobId: pricesheetIngestJobId + exportSchedule: priceExportsScheduleName + exportJobId: pricesheetExportJobId + } + { + runbookName: reservationsPriceExportsRunbookName + isOneToMany: false + containerName: 'reservationspriceexports' + variableName: 'AzureOptimization_ReservationsPriceContainer' + variableDescription: 'The Storage Account container where Reservations Prices exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestReservationsPriceWeekly' + ingestDescription: 'Weekly Reservations Prices ingests' + ingestTimeOffset: 'PT2H' + ingestFrequency: 'Week' + ingestJobId: reservationPricesIngestJobId + exportSchedule: priceExportsScheduleName + exportJobId: reservationPricesExportJobId + } + { + runbookName: reservationsExportsRunbookName + isOneToMany: false + containerName: 'reservationsexports' + variableName: 'AzureOptimization_ReservationsContainer' + variableDescription: 'The Storage Account container where Reservations Usage exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestReservationsUsageDaily' + ingestDescription: 'Daily Reservations Usage ingests' + ingestTimeOffset: 'PT2H30M' + ingestFrequency: 'Day' + ingestJobId: reservationUsageIngestJobId + exportSchedule: reservationsUsageExportsScheduleName + exportJobId: reservationUsageExportJobId + } + { + runbookName: savingsPlansExportsRunbookName + isOneToMany: false + containerName: 'savingsplansexports' + variableName: 'AzureOptimization_SavingsPlansContainer' + variableDescription: 'The Storage Account container where Savings Plans Usage exports are dumped to' + ingestSchedule: 'AzureOptimization_IngestSavingsPlansUsageDaily' + ingestDescription: 'Daily Savings Plans Usage ingests' + ingestTimeOffset: 'PT2H35M' + ingestFrequency: 'Day' + ingestJobId: savingsPlansUsageIngestJobId + exportSchedule: savingsPlansUsageExportsScheduleName + exportJobId: savingsPlansUsageExportJobId + } +] +var csvParameterizedExports = [ + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorVmssCpuMaxExportsScheduleName + exportJobId: monitorVmssCpuMaxExportJobId + parameters: { + ResourceType: 'microsoft.compute/virtualmachinescalesets' + TimeSpan: '01:00:00' + aggregationType: 'Maximum' + MetricNames: 'Percentage CPU' + TimeGrain: '01:00:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorVmssCpuAvgExportsScheduleName + exportJobId: monitorVmssCpuAvgExportJobId + parameters: { + ResourceType: 'microsoft.compute/virtualmachinescalesets' + TimeSpan: '01:00:00' + aggregationType: 'Average' + MetricNames: 'Percentage CPU' + TimeGrain: '01:00:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorVmssMemoryMinExportsScheduleName + exportJobId: monitorVmssMemoryMinExportJobId + parameters: { + ResourceType: 'microsoft.compute/virtualmachinescalesets' + TimeSpan: '01:00:00' + aggregationType: 'Minimum' + MetricNames: 'Available Memory Bytes' + TimeGrain: '01:00:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorSqlDbDtuMaxExportsScheduleName + exportJobId: monitorSqlDbDtuMaxExportJobId + parameters: { + ResourceType: 'microsoft.sql/servers/databases' + ARGFilter: 'sku.tier in (\'Standard\',\'Premium\')' + TimeSpan: '01:00:00' + aggregationType: 'Maximum' + MetricNames: 'dtu_consumption_percent' + TimeGrain: '01:00:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorSqlDbDtuAvgExportsScheduleName + exportJobId: monitorSqlDbDtuAvgExportJobId + parameters: { + ResourceType: 'microsoft.sql/servers/databases' + ARGFilter: 'sku.tier in (\'Standard\',\'Premium\')' + TimeSpan: '01:00:00' + aggregationType: 'Average' + AggregationOfType: 'Maximum' + MetricNames: 'dtu_consumption_percent' + TimeGrain: '00:01:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorAppServiceCpuMaxExportsScheduleName + exportJobId: monitorAppServiceCpuMaxExportJobId + parameters: { + ResourceType: 'microsoft.web/serverfarms' + ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\'' + TimeSpan: '01:00:00' + aggregationType: 'Maximum' + MetricNames: 'CpuPercentage' + TimeGrain: '01:00:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorAppServiceCpuAvgExportsScheduleName + exportJobId: monitorAppServiceCpuAvgExportJobId + parameters: { + ResourceType: 'microsoft.web/serverfarms' + ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\'' + TimeSpan: '01:00:00' + aggregationType: 'Average' + AggregationOfType: 'Maximum' + MetricNames: 'CpuPercentage' + TimeGrain: '00:01:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorAppServiceMemoryMaxExportsScheduleName + exportJobId: monitorAppServiceMemoryMaxExportJobId + parameters: { + ResourceType: 'microsoft.web/serverfarms' + ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\'' + TimeSpan: '01:00:00' + aggregationType: 'Maximum' + MetricNames: 'MemoryPercentage' + TimeGrain: '01:00:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorAppServiceMemoryAvgExportsScheduleName + exportJobId: monitorAppServiceMemoryAvgExportJobId + parameters: { + ResourceType: 'microsoft.web/serverfarms' + ARGFilter: 'properties.computeMode == \'Dedicated\' and sku.tier != \'Free\'' + TimeSpan: '01:00:00' + aggregationType: 'Average' + AggregationOfType: 'Maximum' + MetricNames: 'MemoryPercentage' + TimeGrain: '00:01:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorDiskIOPSAvgExportsScheduleName + exportJobId: monitorDiskIOPSAvgExportJobId + parameters: { + ResourceType: 'microsoft.compute/disks' + ARGFilter: 'sku.name =~ \'Premium_LRS\' and properties.diskState != \'Unattached\'' + TimeSpan: '01:00:00' + aggregationType: 'Average' + AggregationOfType: 'Maximum' + MetricNames: 'Composite Disk Read Operations/sec,Composite Disk Write Operations/sec' + TimeGrain: '00:01:00' + } + } + { + runbookName: monitorExportsRunbookName + exportSchedule: monitorDiskMBPsAvgExportsScheduleName + exportJobId: monitorDiskMBPsAvgExportJobId + parameters: { + ResourceType: 'microsoft.compute/disks' + ARGFilter: 'sku.name =~ \'Premium_LRS\' and properties.diskState != \'Unattached\'' + TimeSpan: '01:00:00' + aggregationType: 'Average' + AggregationOfType: 'Maximum' + MetricNames: 'Composite Disk Read Bytes/sec,Composite Disk Write Bytes/sec' + TimeGrain: '00:01:00' + } + } +] +var unattachedDisksRecommendationsRunbookName = 'Recommend-UnattachedDisksToBlobStorage' +var advisorCostAugmentedRecommendationsRunbookName = 'Recommend-AdvisorCostAugmentedToBlobStorage' +var advisorAsIsRecommendationsRunbookName = 'Recommend-AdvisorAsIsToBlobStorage' +var vmsHARecommendationsRunbookName = 'Recommend-VMsHighAvailabilityToBlobStorage' +var vmOptimizationsRecommendationsRunbookName = 'Recommend-VMOptimizationsToBlobStorage' +var aadExpiringCredsRecommendationsRunbookName = 'Recommend-AADExpiringCredentialsToBlobStorage' +var unusedLBsRecommendationsRunbookName = 'Recommend-UnusedLoadBalancersToBlobStorage' +var unusedAppGWsRecommendationsRunbookName = 'Recommend-UnusedAppGWsToBlobStorage' +var armOptimizationsRecommendationsRunbookName = 'Recommend-ARMOptimizationsToBlobStorage' +var vnetOptimizationsRecommendationsRunbookName = 'Recommend-VNetOptimizationsToBlobStorage' +var vmssOptimizationsRecommendationsRunbookName = 'Recommend-VMSSOptimizationsToBlobStorage' +var sqldbOptimizationsRecommendationsRunbookName = 'Recommend-SqlDbOptimizationsToBlobStorage' +var storageOptimizationsRecommendationsRunbookName = 'Recommend-StorageAccountOptimizationsToBlobStorage' +var appServiceOptimizationsRecommendationsRunbookName = 'Recommend-AppServiceOptimizationsToBlobStorage' +var diskOptimizationsRecommendationsRunbookName = 'Recommend-DiskOptimizationsToBlobStorage' +var cleanUpOlderRecommendationsRunbookName = 'CleanUp-OlderRecommendationsFromSqlServer' +var recommendations = [ + { + recommendationJobId: unattachedDisksRecommendationJobId + runbookName: unattachedDisksRecommendationsRunbookName + } + { + recommendationJobId: advisorCostAugmentedRecommendationJobId + runbookName: advisorCostAugmentedRecommendationsRunbookName + } + { + recommendationJobId: advisorAsIsRecommendationJobId + runbookName: advisorAsIsRecommendationsRunbookName + } + { + recommendationJobId: vmsHaRecommendationJobId + runbookName: vmsHARecommendationsRunbookName + } + { + recommendationJobId: vmOptimizationsRecommendationJobId + runbookName: vmOptimizationsRecommendationsRunbookName + } + { + recommendationJobId: aadExpiringCredsRecommendationJobId + runbookName: aadExpiringCredsRecommendationsRunbookName + } + { + recommendationJobId: unusedLoadBalancersRecommendationJobId + runbookName: unusedLBsRecommendationsRunbookName + } + { + recommendationJobId: unusedAppGWsRecommendationJobId + runbookName: unusedAppGWsRecommendationsRunbookName + } + { + recommendationJobId: armOptimizationsRecommendationJobId + runbookName: armOptimizationsRecommendationsRunbookName + } + { + recommendationJobId: vnetOptimizationsRecommendationJobId + runbookName: vnetOptimizationsRecommendationsRunbookName + } + { + recommendationJobId: vmssOptimizationsRecommendationJobId + runbookName: vmssOptimizationsRecommendationsRunbookName + } + { + recommendationJobId: sqldbOptimizationsRecommendationJobId + runbookName: sqldbOptimizationsRecommendationsRunbookName + } + { + recommendationJobId: storageOptimizationsRecommendationJobId + runbookName: storageOptimizationsRecommendationsRunbookName + } + { + recommendationJobId: appServiceOptimizationsRecommendationJobId + runbookName: appServiceOptimizationsRecommendationsRunbookName + } + { + recommendationJobId: diskOptimizationsRecommendationJobId + runbookName: diskOptimizationsRecommendationsRunbookName + } +] +var remediationLogsContainerName = 'remediationlogs' +var recommendationsContainerName = 'recommendationsexports' +var csvIngestRunbookName = 'Ingest-OptimizationCSVExportsToLogAnalytics' +var recommendationsIngestRunbookName = 'Ingest-RecommendationsToSQLServer' +var recommendationsLogAnalyticsIngestRunbookName = 'Ingest-RecommendationsToLogAnalytics' +var suppressionsLogAnalyticsIngestRunbookName = 'Ingest-SuppressionsToLogAnalytics' +var advisorRightSizeFilteredRemediationRunbookName = 'Remediate-AdvisorRightSizeFiltered' +var longDeallocatedVMsFilteredRemediationRunbookName = 'Remediate-LongDeallocatedVMsFiltered' +var unattachedDisksFilteredRemediationRunbookName = 'Remediate-UnattachedDisksFiltered' +var remediationLogsIngestScheduleName = 'AzureOptimization_IngestRemediationLogsDaily' +var recommendationsScheduleName = 'AzureOptimization_RecommendationsWeekly' +var recommendationsIngestScheduleName = 'AzureOptimization_IngestRecommendationsWeekly' +var suppressionsIngestScheduleName = 'AzureOptimization_IngestSuppressionsWeekly' +var recommendationsCleanUpScheduleName = 'AzureOptimization_CleanUpRecommendationsWeekly' +var Az_Accounts = { + name: 'Az.Accounts' + url: 'https://www.powershellgallery.com/api/v2/package/Az.Accounts/2.12.1' +} +var Microsoft_Graph_Authentication = { + name: 'Microsoft.Graph.Authentication' + url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Authentication/2.4.0' +} +var psModules = [ + { + name: 'Az.Compute' + url: 'https://www.powershellgallery.com/api/v2/package/Az.Compute/5.7.0' + } + { + name: 'Az.OperationalInsights' + url: 'https://www.powershellgallery.com/api/v2/package/Az.OperationalInsights/3.2.0' + } + { + name: 'Az.ResourceGraph' + url: 'https://www.powershellgallery.com/api/v2/package/Az.ResourceGraph/0.13.0' + } + { + name: 'Az.Storage' + url: 'https://www.powershellgallery.com/api/v2/package/Az.Storage/5.5.0' + } + { + name: 'Az.Resources' + url: 'https://www.powershellgallery.com/api/v2/package/Az.Resources/6.6.0' + } + { + name: 'Az.Monitor' + url: 'https://www.powershellgallery.com/api/v2/package/Az.Monitor/4.4.1' + } + { + name: 'Az.PolicyInsights' + url: 'https://www.powershellgallery.com/api/v2/package/Az.PolicyInsights/1.6.0' + } + { + name: 'Microsoft.Graph.Users' + url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Users/2.4.0' + } + { + name: 'Microsoft.Graph.Groups' + url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Groups/2.4.0' + } + { + name: 'Microsoft.Graph.Applications' + url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Applications/2.4.0' + } + { + name: 'Microsoft.Graph.Identity.DirectoryManagement' + url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Identity.DirectoryManagement/2.4.0' + } +] +var runbooks = [ + { + name: advisorExportsRunbookName + version: '1.4.2.1' + description: 'Exports Azure Advisor recommendations to Blob Storage using the Advisor API' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${advisorExportsRunbookName}.ps1') + } + { + name: argDisksExportsRunbookName + version: '1.3.4.1' + description: 'Exports Managed Disks properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argDisksExportsRunbookName}.ps1') + } + { + name: argVhdExportsRunbookName + version: '1.1.4.1' + description: 'Exports Unmanaged Disks (owned by a VM) properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVhdExportsRunbookName}.ps1') + } + { + name: argVmExportsRunbookName + version: '1.4.4.1' + description: 'Exports Virtual Machine properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVmExportsRunbookName}.ps1') + } + { + name: argVmssExportsRunbookName + version: '1.0.2.1' + description: 'Exports VMSS properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVmssExportsRunbookName}.ps1') + } + { + name: argAvailSetExportsRunbookName + version: '1.1.4.1' + description: 'Exports Availability Set properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAvailSetExportsRunbookName}.ps1') + } + { + name: consumptionExportsRunbookName + version: '2.0.4.1' + description: 'Exports Azure Consumption events to Blob Storage using Azure Consumption API' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${consumptionExportsRunbookName}.ps1') + } + { + name: aadObjectsExportsRunbookName + version: '1.2.2.1' + description: 'Exports Azure AAD Objects to Blob Storage using Azure ARM API' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${aadObjectsExportsRunbookName}.ps1') + } + { + name: argLoadBalancersExportsRunbookName + version: '1.1.4.1' + description: 'Exports Load Balancer properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argLoadBalancersExportsRunbookName}.ps1') + } + { + name: argAppGWsExportsRunbookName + version: '1.1.4.1' + description: 'Exports Application Gateway properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAppGWsExportsRunbookName}.ps1') + } + { + name: argResContainersExportsRunbookName + version: '1.0.5.1' + description: 'Exports Resource Containers properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argResContainersExportsRunbookName}.ps1') + } + { + name: rbacExportsRunbookName + version: '1.0.4.1' + description: 'Exports RBAC assignments to Blob Storage using ARM and Microsoft Entra' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${rbacExportsRunbookName}.ps1') + } + { + name: argNICExportsRunbookName + version: '1.0.2.1' + description: 'Exports NIC properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argNICExportsRunbookName}.ps1') + } + { + name: argNSGExportsRunbookName + version: '1.0.2.1' + description: 'Exports NSG properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argNSGExportsRunbookName}.ps1') + } + { + name: argPublicIpExportsRunbookName + version: '1.0.2.1' + description: 'Exports Public IP properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argPublicIpExportsRunbookName}.ps1') + } + { + name: argVNetExportsRunbookName + version: '1.0.2.1' + description: 'Exports VNet properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVNetExportsRunbookName}.ps1') + } + { + name: argSqlDbExportsRunbookName + version: '1.0.2.1' + description: 'Exports SQL DB properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argSqlDbExportsRunbookName}.ps1') + } + { + name: policyStateExportsRunbookName + version: '1.0.3.1' + description: 'Exports Azure Policy State to Blob Storage' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${policyStateExportsRunbookName}.ps1') + } + { + name: monitorExportsRunbookName + version: '1.0.2.1' + description: 'Exports Azure Monitor metrics to Blob Storage' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${monitorExportsRunbookName}.ps1') + } + { + name: argAppServicePlanExportsRunbookName + version: '1.0.1.1' + description: 'Exports App Service Plan properties to Blob Storage using Azure Resource Graph' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAppServicePlanExportsRunbookName}.ps1') + } + { + name: reservationsExportsRunbookName + version: '1.1.2.1' + description: 'Exports Reservations Usage to Blob Storage using the EA or MCA APIs' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${reservationsExportsRunbookName}.ps1') + } + { + name: reservationsPriceExportsRunbookName + version: '1.0.1.1' + description: 'Exports Reservations Prices to Blob Storage using the Retail Prices API' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${reservationsPriceExportsRunbookName}.ps1') + } + { + name: priceSheetExportsRunbookName + version: '1.1.1.1' + description: 'Exports Price Sheet to Blob Storage using the EA or MCA APIs' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${priceSheetExportsRunbookName}.ps1') + } + { + name: savingsPlansExportsRunbookName + version: '1.0.0.0' + description: 'Exports Savings Plans Usage to Blob Storage using the EA or MCA APIs' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${savingsPlansExportsRunbookName}.ps1') + } + { + name: csvIngestRunbookName + version: '1.5.0.0' + description: 'Ingests CSV blobs as custom logs to Log Analytics' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/data-collection/${csvIngestRunbookName}.ps1') + } + { + name: unattachedDisksRecommendationsRunbookName + version: '2.4.8.0' + description: 'Generates unattached disks recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${unattachedDisksRecommendationsRunbookName}.ps1') + } + { + name: advisorCostAugmentedRecommendationsRunbookName + version: '2.9.1.0' + description: 'Generates augmented Advisor Cost recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorCostAugmentedRecommendationsRunbookName}.ps1') + } + { + name: advisorAsIsRecommendationsRunbookName + version: '1.5.5.0' + description: 'Generates all types of Advisor recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorAsIsRecommendationsRunbookName}.ps1') + } + { + name: vmsHARecommendationsRunbookName + version: '1.0.3.0' + description: 'Generates VMs High Availability recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmsHARecommendationsRunbookName}.ps1') + } + { + name: vmOptimizationsRecommendationsRunbookName + version: '1.0.0.0' + description: 'Generates VM optimizations recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmOptimizationsRecommendationsRunbookName}.ps1') + } + { + name: aadExpiringCredsRecommendationsRunbookName + version: '1.1.10.0' + description: 'Generates AAD Objects with expiring credentials recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${aadExpiringCredsRecommendationsRunbookName}.ps1') + } + { + name: unusedLBsRecommendationsRunbookName + version: '1.2.9.0' + description: 'Generates unused Load Balancers recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedLBsRecommendationsRunbookName}.ps1') + } + { + name: unusedAppGWsRecommendationsRunbookName + version: '1.2.9.0' + description: 'Generates unused Application Gateways recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedAppGWsRecommendationsRunbookName}.ps1') + } + { + name: armOptimizationsRecommendationsRunbookName + version: '1.0.3.0' + description: 'Generates ARM optimizations recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${armOptimizationsRecommendationsRunbookName}.ps1') + } + { + name: vnetOptimizationsRecommendationsRunbookName + version: '1.0.4.0' + description: 'Generates Virtual Network optimizations recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${vnetOptimizationsRecommendationsRunbookName}.ps1') + } + { + name: vmssOptimizationsRecommendationsRunbookName + version: '1.1.1.0' + description: 'Generates VM Scale Set optimizations recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmssOptimizationsRecommendationsRunbookName}.ps1') + } + { + name: sqldbOptimizationsRecommendationsRunbookName + version: '1.1.2.0' + description: 'Generates SQL DB optimizations recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${sqldbOptimizationsRecommendationsRunbookName}.ps1') + } + { + name: storageOptimizationsRecommendationsRunbookName + version: '1.0.3.0' + description: 'Generates Storage Account optimizations recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${storageOptimizationsRecommendationsRunbookName}.ps1') + } + { + name: appServiceOptimizationsRecommendationsRunbookName + version: '1.0.3.0' + description: 'Generates App Service optimizations recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${appServiceOptimizationsRecommendationsRunbookName}.ps1') + } + { + name: diskOptimizationsRecommendationsRunbookName + version: '1.1.1.0' + description: 'Generates Disk optimizations recommendations' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${diskOptimizationsRecommendationsRunbookName}.ps1') + } + { + name: recommendationsIngestRunbookName + version: '1.6.5.0' + description: 'Ingests JSON-based recommendations into an Azure SQL Database' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${recommendationsIngestRunbookName}.ps1') + } + { + name: recommendationsLogAnalyticsIngestRunbookName + version: '1.0.2.0' + description: 'Ingests JSON-based recommendations into Log Analytics' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${recommendationsLogAnalyticsIngestRunbookName}.ps1') + } + { + name: suppressionsLogAnalyticsIngestRunbookName + version: '1.0.0.0' + description: 'Ingests suppressions into Log Analytics' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/recommendations/${suppressionsLogAnalyticsIngestRunbookName}.ps1') + } + { + name: advisorRightSizeFilteredRemediationRunbookName + version: '1.2.4.0' + description: 'Remediates Azure Advisor right-size recommendations given fit and tag filters' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/remediations/${advisorRightSizeFilteredRemediationRunbookName}.ps1') + } + { + name: longDeallocatedVMsFilteredRemediationRunbookName + version: '1.0.3.0' + description: 'Remediates long-deallocated VMs recommendations given fit and tag filters' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/remediations/${longDeallocatedVMsFilteredRemediationRunbookName}.ps1') + } + { + name: unattachedDisksFilteredRemediationRunbookName + version: '1.0.3.0' + description: 'Remediates unattached disks recommendations given fit and tag filters' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/remediations/${unattachedDisksFilteredRemediationRunbookName}.ps1') + } + { + name: cleanUpOlderRecommendationsRunbookName + version: '1.0.0.0' + description: 'Cleans up older recommendations from SQL Database' + type: 'PowerShell' + scriptUri: uri(templateLocation, 'runbooks/maintenance/${cleanUpOlderRecommendationsRunbookName}.ps1') + } +] +var automationVariables = [ + { + name: 'AzureOptimization_CloudEnvironment' + description: 'Azure Cloud environment (e.g., AzureCloud, AzureChinaCloud, etc.)' + value: '"${cloudEnvironment}"' + } + { + name: 'AzureOptimization_AuthenticationOption' + description: 'Runbook authentication type (RunAsAccount or ManagedIdentity)' + value: '"${authenticationOption}"' + } + { + name: 'AzureOptimization_StorageSink' + description: 'The Azure Storage Account where data source exports are dumped to' + value: '"${storageAccountName}"' + } + { + name: 'AzureOptimization_StorageSinkRG' + description: 'The resource group for the Azure Storage Account sink' + value: '"${resourceGroup().name}"' + } + { + name: 'AzureOptimization_StorageSinkSubId' + description: 'The subscription Id for the Azure Storage Account sink' + value: '"${subscription().subscriptionId}"' + } + { + name: 'AzureOptimization_ConsumptionOffsetDays' + description: 'The offset (in days) for querying for consumption data' + value: 3 + } + { + name: 'AzureOptimization_AdvisorFilter' + description: 'The category filter to use for Azure Advisor (non-Cost) recommendations exports' + value: '"HighAvailability,Security,Performance,OperationalExcellence"' + } + { + name: 'AzureOptimization_ReferenceRegion' + description: 'The Azure region used as a reference for getting details about Azure VM sizes available' + value: '"${projectLocation}"' + } + { + name: 'AzureOptimization_SQLServerDatabase' + description: 'The Azure SQL Database name for the ingestion control and recommendations tables' + value: '"${sqlDatabaseName}"' + } + { + name: 'AzureOptimization_LogAnalyticsChunkSize' + description: 'The size (in rows) for each chunk of Log Analytics ingestion request' + value: 6000 + } + { + name: 'AzureOptimization_StorageBlobsPageSize' + description: 'The size (in blobs count) for each page of Storage Account container blob listing' + value: 1000 + } + { + name: 'AzureOptimization_SQLServerInsertSize' + description: 'The size (in inserted lines) for each page of recommendations ingestion into the SQL Database' + value: 900 + } + { + name: 'AzureOptimization_LogAnalyticsLogPrefix' + description: 'The prefix for all Azure Optimization custom log tables in Log Analytics' + value: '"AzureOptimization"' + } + { + name: 'AzureOptimization_LogAnalyticsWorkspaceName' + description: 'The Log Analytics Workspace Name where optimization data will be ingested' + value: '"${logAnalyticsWorkspaceName}"' + } + { + name: 'AzureOptimization_LogAnalyticsWorkspaceRG' + description: 'The resource group for the Log Analytics Workspace where optimization data will be ingested' + value: '"${((!logAnalyticsReuse) ? resourceGroup().name : logAnalyticsWorkspaceRG)}"' + } + { + name: 'AzureOptimization_LogAnalyticsWorkspaceSubId' + description: 'The Azure subscription for the Log Analytics Workspace where optimization data will be ingested' + value: '"${subscription().subscriptionId}"' + } + { + name: 'AzureOptimization_LogAnalyticsWorkspaceTenantId' + description: 'The Microsoft Entra tenant for the Log Analytics Workspace where optimization data will be ingested' + value: '"${subscription().tenantId}"' + } + { + name: 'AzureOptimization_PriceSheetMeterCategories' + description: 'Comma-separated meter categories to be included in the Price Sheet (remove variable to include all categories)' + value: '"Virtual Machines,Storage"' + } + { + name: 'AzureOptimization_RetailPricesCurrencyCode' + description: 'The currency code to be used for the retail prices exports (used for Reservations prices)' + value: '"EUR"' + } + { + name: 'AzureOptimization_RecommendAdvisorPeriodInDays' + description: 'The period (in days) to look back for Advisor exported recommendations' + value: 7 + } + { + name: 'AzureOptimization_RecommendationLongDeallocatedVmsIntervalDays' + description: 'The period (in days) for considering a VM long deallocated' + value: 30 + } + { + name: 'AzureOptimization_PerfPercentileCpu' + description: 'The percentile to be used for processor metrics' + value: 99 + } + { + name: 'AzureOptimization_PerfPercentileMemory' + description: 'The percentile to be used for memory metrics' + value: 99 + } + { + name: 'AzureOptimization_PerfPercentileNetwork' + description: 'The percentile to be used for network metrics' + value: 99 + } + { + name: 'AzureOptimization_PerfPercentileDisk' + description: 'The percentile to be used for disk metrics' + value: 99 + } + { + name: 'AzureOptimization_PerfPercentileSqlDtu' + description: 'The percentile to be used for SQL DB DTU metrics' + value: 99 + } + { + name: 'AzureOptimization_PerfThresholdCpuPercentage' + description: 'The processor usage percentage threshold above which the fit score is decreased or below which the instance is considered underutilized' + value: 30 + } + { + name: 'AzureOptimization_PerfThresholdMemoryPercentage' + description: 'The memory usage percentage threshold above which the fit score is decreased or below which the instance is considered underutilized' + value: 50 + } + { + name: 'AzureOptimization_PerfThresholdCpuDegradedMaxPercentage' + description: 'The maximum processor usage percentage threshold above which the instance is considered degraded' + value: 95 + } + { + name: 'AzureOptimization_PerfThresholdCpuDegradedAvgPercentage' + description: 'The average processor usage percentage threshold above which the instance is considered degraded' + value: 75 + } + { + name: 'AzureOptimization_PerfThresholdMemoryDegradedPercentage' + description: 'The memory usage percentage threshold above which the instance is considered degraded' + value: 90 + } + { + name: 'AzureOptimization_PerfThresholdNetworkMbps' + description: 'The network usage threshold (in Mbps) above which the fit score is decreased' + value: 750 + } + { + name: 'AzureOptimization_PerfThresholdCpuShutdownPercentage' + description: 'The processor usage percentage threshold above which the fit score is decreased (shutdown scenarios)' + value: 5 + } + { + name: 'AzureOptimization_PerfThresholdMemoryShutdownPercentage' + description: 'The memory usage percentage threshold above which the fit score is decreased (shutdown scenarios)' + value: 100 + } + { + name: 'AzureOptimization_PerfThresholdNetworkShutdownMbps' + description: 'The network usage threshold (in Mbps) above which the fit score is decreased (shutdown scenarios)' + value: 10 + } + { + name: 'AzureOptimization_PerfThresholdDtuPercentage' + description: 'The DTU usage percentage threshold below which a SQL Database instance is considered underutilized' + value: 40 + } + { + name: 'AzureOptimization_PerfThresholdDtuDegradedPercentage' + description: 'The DTU usage percentage threshold above which a SQL Database instance is considered performance degraded' + value: 75 + } + { + name: 'AzureOptimization_PerfThresholdDiskIOPSPercentage' + description: 'The IOPS usage percentage threshold below which a Disk is considered underutilized' + value: 5 + } + { + name: 'AzureOptimization_PerfThresholdDiskMBsPercentage' + description: 'The throughput (MBps) usage percentage threshold below which a Disk is considered underutilized' + value: 5 + } + { + name: 'AzureOptimization_RemediateRightSizeMinFitScore' + description: 'The minimum fit score for right-size remediation' + value: '"5.0"' + } + { + name: 'AzureOptimization_RemediateRightSizeMinWeeksInARow' + description: 'The minimum number of weeks in a row required for a right-size recommendation to be remediated' + value: 4 + } + { + name: 'AzureOptimization_RecommendationAdvisorCostRightSizeId' + description: 'The Azure Advisor VM right-size recommendation ID' + value: '"e10b1381-5f0a-47ff-8c7b-37bd13d7c974"' + } + { + name: 'AzureOptimization_RemediateLongDeallocatedVMsMinFitScore' + description: 'The minimum fit score for long-deallocated VM remediation' + value: '"5.0"' + } + { + name: 'AzureOptimization_RemediateLongDeallocatedVMsMinWeeksInARow' + description: 'The minimum number of weeks in a row required for a long-deallocated VM recommendation to be remediated' + value: 4 + } + { + name: 'AzureOptimization_RecommendationLongDeallocatedVMsId' + description: 'The long deallocated VM recommendation ID' + value: '"c320b790-2e58-452a-aa63-7b62c383ad8a"' + } + { + name: 'AzureOptimization_RemediateUnattachedDisksMinFitScore' + description: 'The minimum fit score for unattached disk remediation' + value: '"5.0"' + } + { + name: 'AzureOptimization_RemediateUnattachedDisksMinWeeksInARow' + description: 'The minimum number of weeks in a row required for a unattached disk recommendation to be remediated' + value: 4 + } + { + name: 'AzureOptimization_RemediateUnattachedDisksAction' + description: 'The action for the unattached disk recommendation to be remediated (Delete or Downsize)' + value: '"Delete"' + } + { + name: 'AzureOptimization_RecommendationUnattachedDisksId' + description: 'The unattached disk recommendation ID' + value: '"c84d5e86-e2d6-4d62-be7c-cecfbd73b0db"' + } + { + name: 'AzureOptimization_RecommendationAADMinCredValidityDays' + description: 'The minimum validity of an AAD Object credential in days' + value: 30 + } + { + name: 'AzureOptimization_RecommendationAADMaxCredValidityYears' + description: 'The maximum validity of an AAD Object credential in years' + value: 2 + } + { + name: 'AzureOptimization_AADObjectsFilter' + description: 'The Microsoft Entra object types to export' + value: '"Application,ServicePrincipal,User,Group"' + } + { + name: 'AzureOptimization_RecommendationRBACAssignmentsPercentageThreshold' + description: 'The percentage threshold (used to trigger recommendations) for total RBAC assignments limits' + value: 80 + } + { + name: 'AzureOptimization_RecommendationResourceGroupsPerSubPercentageThreshold' + description: 'The percentage threshold (used to trigger recommendations) for resource group count limits' + value: 80 + } + { + name: 'AzureOptimization_RecommendationVNetSubnetMaxUsedPercentageThreshold' + description: 'The percentage threshold (used to trigger recommendations) for maximum subnet address space usage' + value: 80 + } + { + name: 'AzureOptimization_RecommendationVNetSubnetMinUsedPercentageThreshold' + description: 'The percentage threshold (used to trigger recommendations) for minimum subnet address space usage' + value: 5 + } + { + name: 'AzureOptimization_RecommendationVNetSubnetEmptyMinAgeInDays' + description: 'The minimum age (in days) for an empty subnet to trigger an NSG rule recommendation' + value: 30 + } + { + name: 'AzureOptimization_RecommendationsMaxAgeInDays' + description: 'The maximum age (in days) for a recommendation to be kept in the SQL database' + value: 365 + } + { + name: 'AzureOptimization_RecommendationStorageAcountGrowthThresholdPercentage' + description: 'The minimum Storage Account growth percentage required to flag Storage as not having a retention policy in place' + value: 5 + } + { + name: 'AzureOptimization_RecommendationStorageAcountGrowthMonthlyCostThreshold' + description: 'The minimum monthly cost (in your EA/MCA currency) required to flag Storage as not having a retention policy in place' + value: 50 + } + { + name: 'AzureOptimization_RecommendationStorageAcountGrowthLookbackDays' + description: 'The lookback period (in days) for analyzing Storage Account growth' + value: 30 + } +] + +//------------------------------------------------------------------------------ +// Telemetry +// Used to anonymously count the number of times the template has been deployed +// and to track and fix deployment bugs to ensure the highest quality. +// No information about you or your cost data is collected. +//------------------------------------------------------------------------------ + +resource defaultTelemetry 'Microsoft.Resources/deployments@2022-09-01' = if (enableDefaultTelemetry) { + name: 'pid-${telemetryId}-${uniqueString(deployment().name, projectLocation)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + metadata: { + _generator: { + name: 'FinOps toolkit' + version: finOpsToolkitVersion + } + } + resources: [] + } + } +} + +resource logAnalyticsWorkspace 'microsoft.operationalinsights/workspaces@2020-08-01' = if (!logAnalyticsReuse) { + name: logAnalyticsWorkspaceName + location: projectLocation + tags: resourceTags + properties: { + sku: { + name: 'pergb2018' + } + retentionInDays: logAnalyticsRetentionDays + } +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: storageAccountName + location: projectLocation + tags: resourceTags + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + allowBlobPublicAccess: false + networkAcls: { + bypass: 'AzureServices' + virtualNetworkRules: [] + ipRules: [] + defaultAction: 'Allow' + } + supportsHttpsTrafficOnly: true + encryption: { + services: { + file: { + enabled: true + } + blob: { + enabled: true + } + } + keySource: 'Microsoft.Storage' + } + minimumTlsVersion: 'TLS1_2' + accessTier: 'Cool' + } +} + +resource storageBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = { + parent: storageAccount + name: 'default' + properties: { + cors: { + corsRules: [] + } + deleteRetentionPolicy: { + enabled: false + } + } +} + +resource storageCsvExportsContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = [for item in csvExports: { + name: '${storageAccountName}/default/${item.containerName}' + properties: { + publicAccess: 'None' + } + dependsOn: [ + storageBlobServices + storageAccount + ] +}] + +resource storageRecommendationsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = { + name: '${storageAccountName}/default/${recommendationsContainerName}' + properties: { + publicAccess: 'None' + } + dependsOn: [ + storageBlobServices + storageAccount + ] +} + +resource storageRemediationLogsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = { + name: '${storageAccountName}/default/${remediationLogsContainerName}' + properties: { + publicAccess: 'None' + } + dependsOn: [ + storageBlobServices + storageAccount + ] +} + +resource storageLifecycleManagementPolicy 'Microsoft.Storage/storageAccounts/managementPolicies@2021-02-01' = { + parent: storageAccount + name: 'default' + properties: { + policy: { + rules: [ + { + enabled: true + name: 'Clean6MonthsOldBlobs' + type: 'Lifecycle' + definition: { + actions: { + baseBlob: { + delete: { + daysAfterModificationGreaterThan: 180 + } + } + snapshot: { + delete: { + daysAfterCreationGreaterThan: 180 + } + } + version: { + delete: { + daysAfterCreationGreaterThan: 180 + } + } + } + filters: { + blobTypes: [ + 'blockBlob' + ] + } + } + } + ] + } + } + dependsOn: [ + storageBlobServices + ] +} + +resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { + name: sqlServerName + location: projectLocation + tags: resourceTags + properties: { + administratorLogin: sqlAdminLogin + administratorLoginPassword: sqlAdminPassword + version: '12.0' + publicNetworkAccess: 'Enabled' + minimalTlsVersion: '1.2' + } +} + +resource sqlServerFirewall 'Microsoft.Sql/servers/firewallRules@2022-05-01-preview' = { + parent: sqlServer + name: 'AllowAllWindowsAzureIps' + properties: { + endIpAddress: '0.0.0.0' + startIpAddress: '0.0.0.0' + } +} + +resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-05-01-preview' = { + parent: sqlServer + name: sqlDatabaseName + location: projectLocation + tags: resourceTags + sku: { + name: 'Basic' + tier: 'Basic' + capacity: 5 + } + properties: { + collation: 'SQL_Latin1_General_CP1_CI_AS' + maxSizeBytes: 2147483648 + catalogCollation: 'SQL_Latin1_General_CP1_CI_AS' + zoneRedundant: false + readScale: 'Disabled' + autoPauseDelay: 60 + requestedBackupStorageRedundancy: 'Geo' + } +} + +resource sqlServerName_sqlDatabaseName_default 'Microsoft.Sql/servers/databases/backupShortTermRetentionPolicies@2022-05-01-preview' = { + name: '${sqlServerName}/${sqlDatabaseName}/default' + properties: { + retentionDays: sqlBackupRetentionDays + } + dependsOn: [ + sqlDatabase + sqlServer + ] +} + +resource automationAccount 'Microsoft.Automation/automationAccounts@2020-01-13-preview' = { + name: automationAccountName + location: projectLocation + tags: resourceTags + identity: { + type: 'SystemAssigned' + } + properties: { + sku: { + name: 'Basic' + } + } +} + +resource automationModule_Az_Accounts 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = { + parent: automationAccount + name: Az_Accounts.name + tags: resourceTags + properties: { + contentLink: { + uri: Az_Accounts.url + } + } +} + +resource automationModule_Microsoft_Graph_Authentication 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = { + parent: automationAccount + name: Microsoft_Graph_Authentication.name + tags: resourceTags + properties: { + contentLink: { + uri: Microsoft_Graph_Authentication.url + } + } +} + +resource automationModule_All 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = [for item in psModules: { + parent: automationAccount + name: item.name + tags: resourceTags + properties: { + contentLink: { + uri: item.url + } + } + dependsOn: [ + automationModule_Az_Accounts + automationModule_Microsoft_Graph_Authentication + ] +}] + +resource automationRunbooks 'Microsoft.Automation/automationAccounts/runbooks@2020-01-13-preview' = [for item in runbooks: { + parent: automationAccount + name: item.name + tags: resourceTags + location: projectLocation + properties: { + runbookType: item.type + logProgress: false + logVerbose: false + description: item.description + publishContentLink: { + uri: item.scriptUri + version: item.version + } + } + dependsOn: [ + automationModule_All + ] +}] + +resource automationVariablesAll 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = [for item in automationVariables: { + parent: automationAccount + name: item.name + properties: { + description: item.description + value: item.value + } +}] + +resource automationVariables_csvExports 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = [for item in csvExports: { + parent: automationAccount + name: item.variableName + properties: { + description: item.variableDescription + value: '"${item.containerName}"' + } +}] + +resource automationVariables_SQLServerHostname 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = { + parent: automationAccount + name: 'AzureOptimization_SQLServerHostname' + properties: { + description: 'The Azure SQL Server hostname for the ingestion control and recommendations tables' + value: '"${sqlServer.properties.fullyQualifiedDomainName}"' + } +} + +resource automationVariables_LogAnalyticsWorkspaceId 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = { + parent: automationAccount + name: 'AzureOptimization_LogAnalyticsWorkspaceId' + properties: { + description: 'The Log Analytics Workspace ID where optimization data will be ingested' + value: '"${reference(((!logAnalyticsReuse) ? logAnalyticsWorkspace.id : resourceId(logAnalyticsWorkspaceRG, 'microsoft.operationalinsights/workspaces', logAnalyticsWorkspaceName)), '2020-08-01').customerId}"' + } +} + +resource automationVariables_LogAnalyticsWorkspaceKey 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = { + parent: automationAccount + name: 'AzureOptimization_LogAnalyticsWorkspaceKey' + properties: { + description: 'The shared key for the Log Analytics Workspace where optimization data will be ingested' + value: '"${listKeys(((!logAnalyticsReuse) ? logAnalyticsWorkspace.id : resourceId(logAnalyticsWorkspaceRG, 'microsoft.operationalinsights/workspaces', logAnalyticsWorkspaceName)), '2020-08-01').primarySharedKey}"' + isEncrypted: true + } +} + +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 + properties: { + description: item.exportDescription + expiryTime: '9999-12-31T17:59:00-06:00' + startTime: dateTimeAdd(baseTime, item.exportTimeOffset) + interval: 1 + frequency: item.exportFrequency + } +}] + +resource automationSchedules_csvIngests 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = [for item in csvExports: { + parent: automationAccount + name: item.ingestSchedule + properties: { + description: item.ingestDescription + expiryTime: '9999-12-31T17:59:00-06:00' + startTime: dateTimeAdd(baseTime, item.ingestTimeOffset) + interval: 1 + frequency: item.ingestFrequency + } +}] + +resource automationSchedules_remediationCsvIngest 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = { + parent: automationAccount + name: remediationLogsIngestScheduleName + properties: { + description: 'Starts the daily Remediation Logs ingests' + expiryTime: '9999-12-31T17:59:00-06:00' + startTime: dateTimeAdd(baseTime, 'PT1H30M') + interval: 1 + frequency: 'Day' + } +} + +resource automationSchedules_recommendationsExport 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = { + parent: automationAccount + name: recommendationsScheduleName + properties: { + description: 'Starts the weekly Recommendations generation' + expiryTime: '9999-12-31T17:59:00-06:00' + startTime: dateTimeAdd(baseTime, 'PT2H30M') + interval: 1 + frequency: 'Week' + } +} + +resource automationSchedules_recommendationsIngest 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = { + parent: automationAccount + name: recommendationsIngestScheduleName + properties: { + description: 'Starts the weekly Recommendations ingests' + expiryTime: '9999-12-31T17:59:00-06:00' + startTime: dateTimeAdd(baseTime, 'PT3H30M') + interval: 1 + frequency: 'Week' + } +} + +resource automationSchedules_suppressionsIngest 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = { + parent: automationAccount + name: suppressionsIngestScheduleName + properties: { + description: 'Starts the weekly Suppressions ingests' + expiryTime: '9999-12-31T17:59:00-06:00' + startTime: dateTimeAdd(baseTime, 'PT3H00M') + interval: 1 + frequency: 'Week' + } +} + +resource automationSchedules_recommendationsCleanUp 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = { + parent: automationAccount + name: recommendationsCleanUpScheduleName + properties: { + description: 'Starts the weekly Recommendations cleanup' + expiryTime: '9999-12-31T17:59:00-06:00' + startTime: dateTimeAdd(baseTime, 'P6D') + interval: 1 + frequency: 'Week' + } +} + +resource automationJobSchedules_csvExports 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = [for item in csvExports: if (!item.isOneToMany) { + parent: automationAccount + name: item.exportJobId + properties: { + schedule: { + name: item.exportSchedule + } + runbook: { + name: item.runbookName + } + } + dependsOn: [ + automationSchedules_csvExports + automationModule_All + automationRunbooks + ] +}] + +resource automationJobSchedules_csvParameterizedExports 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = [for item in csvParameterizedExports: { + parent: automationAccount + name: item.exportJobId + properties: { + schedule: { + name: item.exportSchedule + } + runbook: { + name: item.runbookName + } + parameters: item.parameters + } + dependsOn: [ + automationSchedules_csvExports + automationModule_All + automationRunbooks + ] +}] + +resource automationJobSchedules_csvIngests 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = [for item in csvExports: { + parent: automationAccount + name: item.ingestJobId + properties: { + schedule: { + name: item.ingestSchedule + } + runbook: { + name: csvIngestRunbookName + } + parameters: { + StorageSinkContainer: item.containerName + } + } + dependsOn: [ + automationSchedules_csvIngests + automationModule_All + automationRunbooks + ] +}] + +resource automationJobSchedules_remediationLogsIngests 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = { + parent: automationAccount + name: remediationLogsIngestJobId + properties: { + schedule: { + name: remediationLogsIngestScheduleName + } + runbook: { + name: csvIngestRunbookName + } + parameters: { + StorageSinkContainer: remediationLogsContainerName + } + } + dependsOn: [ + automationSchedules_remediationCsvIngest + automationModule_All + automationRunbooks + ] +} + +resource automationJobSchedules_recommendationsExports 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = [for item in recommendations: { + parent: automationAccount + name: item.recommendationJobId + properties: { + schedule: { + name: recommendationsScheduleName + } + runbook: { + name: item.runbookName + } + } + dependsOn: [ + automationSchedules_recommendationsExport + automationModule_All + automationRunbooks + ] +}] + +resource automationJobSchedules_recommendationsIngests 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = { + parent: automationAccount + name: recommendationsIngestJobId + properties: { + schedule: { + name: recommendationsIngestScheduleName + } + runbook: { + name: recommendationsIngestRunbookName + } + } + dependsOn: [ + automationSchedules_recommendationsIngest + automationModule_All + automationRunbooks + ] +} + +resource automationJobSchedules_recommendationsLogAnalyticsIngest 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = { + parent: automationAccount + name: recommendationsLogAnalyticsIngestJobId + properties: { + schedule: { + name: recommendationsIngestScheduleName + } + runbook: { + name: recommendationsLogAnalyticsIngestRunbookName + } + } + dependsOn: [ + automationSchedules_recommendationsIngest + automationModule_All + automationRunbooks + ] +} + +resource automationJobSchedules_suppressionsLogAnalyticsIngest 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = { + parent: automationAccount + name: suppressionsLogAnalyticsIngestJobId + properties: { + schedule: { + name: suppressionsIngestScheduleName + } + runbook: { + name: suppressionsLogAnalyticsIngestRunbookName + } + } + dependsOn: [ + automationSchedules_suppressionsIngest + automationModule_All + automationRunbooks + ] +} + +resource automationJobSchedules_recommendationsCleanUp 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = { + parent: automationAccount + name: recommendationsCleanUpJobId + properties: { + schedule: { + name: recommendationsCleanUpScheduleName + } + runbook: { + name: cleanUpOlderRecommendationsRunbookName + } + } + dependsOn: [ + automationSchedules_recommendationsCleanUp + automationModule_All + automationRunbooks + ] +} + +resource contributorRoleAssignmentGuid_resource 'Microsoft.Authorization/roleAssignments@2018-09-01-preview' = { + name: contributorRoleAssignmentGuid + properties: { + roleDefinitionId: roleContributor + principalId: reference(automationAccount.id, '2019-06-01', 'Full').identity.principalId + principalType: 'ServicePrincipal' + } +} + +output automationPrincipalId string = reference(automationAccount.id, '2019-06-01', 'Full').identity.principalId diff --git a/docs/deploy/optimization-engine/0.4/azuredeploy.bicep b/docs/deploy/optimization-engine/0.4/azuredeploy.bicep new file mode 100644 index 000000000..ec8982de5 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/azuredeploy.bicep @@ -0,0 +1,80 @@ +targetScope = 'subscription' +param rgName string +param readerRoleAssignmentGuid string = guid(subscription().subscriptionId, rgName) +param contributorRoleAssignmentGuid string = guid(rgName) +param projectLocation string + +@description('The base URI where artifacts required by this template are located') +param templateLocation string + +param storageAccountName string +param automationAccountName string +param sqlServerName string +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 cloudEnvironment string = 'AzureCloud' +param authenticationOption string = 'ManagedIdentity' + +@description('Base time for all automation runbook schedules.') +param baseTime string = utcNow('u') +param resourceTags object + +param roleReader string = '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7' + +@description('Optional. Enable telemetry to track anonymous module usage trends, monitor for bugs, and improve future releases.') +param enableDefaultTelemetry bool = true + +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: rgName + location: projectLocation + tags: resourceTags + dependsOn: [] +} + +module resourcesDeployment './azuredeploy-nested.bicep' = { + name: 'resourcesDeployment' + scope: resourceGroup(rgName) + params: { + projectLocation: projectLocation + templateLocation: templateLocation + storageAccountName: storageAccountName + automationAccountName: automationAccountName + sqlServerName: sqlServerName + 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 + enableDefaultTelemetry: enableDefaultTelemetry + } + dependsOn: [ + rg + ] +} + +resource readerRoleAssignmentGuid_resource 'Microsoft.Authorization/roleAssignments@2018-09-01-preview' = { + name: readerRoleAssignmentGuid + properties: { + roleDefinitionId: roleReader + principalId: resourcesDeployment.outputs.automationPrincipalId + principalType: 'ServicePrincipal' + } +} + +output automationPrincipalId string = resourcesDeployment.outputs.automationPrincipalId diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1 new file mode 100644 index 000000000..8bde11357 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1 @@ -0,0 +1,509 @@ +param( + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName, + + [Parameter(Mandatory = $false)] + [string] $groupFilter, + + [Parameter(Mandatory = $false)] + [string] $userFilter +) + +$ErrorActionPreference = "Stop" + +function Build-CredObjectWithDates { + param ( + [object] $appObject + ) + + $credObjects = @() + + foreach ($obj in $appObject.KeyCredentials) + { + $credObject = New-Object PSObject -Property @{ + DisplayName = $obj.DisplayName + KeyId = $obj.KeyId + KeyType = $obj.Type + StartDate = (Get-Date($obj.StartDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + EndDate = (Get-Date($obj.EndDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + $credObjects += $credObject + } + + foreach ($obj in $appObject.PasswordCredentials) + { + $credObject = New-Object PSObject -Property @{ + DisplayName = $obj.DisplayName + KeyId = $obj.KeyId + KeyType = "Password" + StartDate = (Get-Date($obj.StartDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + EndDate = (Get-Date($obj.EndDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + $credObjects += $credObject + } + + return $credObjects +} + +function Build-PrincipalNames { + param ( + [object] $appObject + ) + + $principalNames = @() + + if ($appObject.Web.HomePageUrl) + { + $principalNames += $appObject.Web.HomePageUrl + } + + foreach ($obj in $appObject.IdentifierUris) + { + $principalNames += $obj + } + + foreach ($obj in $appObject.ServicePrincipalNames) + { + $principalNames += $obj + } + + foreach ($obj in $appObject.AlternativeNames) + { + $principalNames += $obj + } + + return $principalNames +} + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_AADObjectsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "aadobjectsexports" +} + +# Application,ServicePrincipal,User,Group +$aadObjectsFilter = Get-AutomationVariable -Name "AzureOptimization_AADObjectsFilter" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($aadObjectsFilter)) +{ + $aadObjectsFilter = "Application,ServicePrincipal" +} + +$groupFilterVariable = Get-AutomationVariable -Name "AzureOptimization_AADObjectsGroupFilter" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($groupFilter) -and -not([string]::IsNullOrEmpty($groupFilterVariable))) +{ + $groupFilter = $groupFilterVariable +} + +$userFilterVariable = Get-AutomationVariable -Name "AzureOptimization_AADObjectsUserFilter" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($userFilter) -and -not([string]::IsNullOrEmpty($userFilterVariable))) +{ + $userFilter = $userFilterVariable +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +#workaround for https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/888 +$localPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile) +if (-not(get-item "$localPath\.graph\" -ErrorAction SilentlyContinue)) +{ + New-Item -Type Directory "$localPath\.graph" +} + +Import-Module Microsoft.Graph.Authentication +Import-Module Microsoft.Graph.Users +Import-Module Microsoft.Graph.Applications +Import-Module Microsoft.Graph.Groups + +switch ($cloudEnvironment) { + "AzureUSGovernment" { + $graphEnvironment = "USGov" + break + } + "AzureChinaCloud" { + $graphEnvironment = "China" + break + } + "AzureGermanCloud" { + $graphEnvironment = "Germany" + break + } + Default { + $graphEnvironment = "Global" + } +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Microsoft Graph with $externalCredentialName external credential..." + Connect-MgGraph -TenantId $externalTenantId -ClientSecretCredential $externalCredential -Environment $graphEnvironment -NoWelcome +} +else +{ + "Logging in to Microsoft Graph..." + Connect-MgGraph -Identity -Environment $graphEnvironment -NoWelcome +} + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +$aadObjectsTypes = $aadObjectsFilter.Split(",") + +$fileDate = $datetime.ToString("yyyyMMdd") + +if ("Application" -in $aadObjectsTypes) +{ + $aadObjects = @() + + "Getting AAD applications..." + $apps = Get-MgApplication -All -ExpandProperty Owners -Property Id,AppId,CreatedDateTime,DeletedDateTime,DisplayName,KeyCredentials,PasswordCredentials,Owners,PublisherDomain,Web,IdentifierUris + "Found $($apps.Count) AAD applications" + + foreach ($app in $apps) + { + $owners = $null + if ($app.Owners.Count -gt 0) + { + $owners = ($app.Owners | Where-Object { [string]::IsNullOrEmpty($_.DeletedDateTime) }).Id | ConvertTo-Json -Compress + } + $createdDate = $null + if ($app.CreatedDateTime) + { + $createdDate = (Get-Date($app.CreatedDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + $deletedDate = $null + if ($app.DeletedDateTime) + { + $deletedDate = (Get-Date($app.DeletedDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + $aadObject = New-Object PSObject -Property @{ + Timestamp = $timestamp + TenantGuid = $tenantId + Cloud = $cloudEnvironment + ObjectId = $app.Id + ObjectType = "Application" + ObjectSubType = "N/A" + DisplayName = $app.DisplayName + SecurityEnabled = "N/A" + ApplicationId = $app.AppId + Keys = (Build-CredObjectWithDates -appObject $app) | ConvertTo-Json -Compress + PrincipalNames = (Build-PrincipalNames -appObject $app) | ConvertTo-Json -Compress + Owners = $owners + CreatedDate = $createdDate + DeletedDate = $deletedDate + } + $aadObjects += $aadObject + } + + $jsonExportPath = "$fileDate-$tenantId-aadobjects-apps.json" + $csvExportPath = "$fileDate-$tenantId-aadobjects-apps.csv" + + $aadObjects | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath + "Exported to JSON: $($aadObjects.Count) lines" + $aadObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json + "JSON Import: $($aadObjectsJson.Count) lines" + $aadObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath + "Export to $csvExportPath" + + $csvBlobName = $csvExportPath + $csvProperties = @{"ContentType" = "text/csv"}; + + Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Uploaded $csvBlobName to Blob Storage..." + + Remove-Item -Path $csvExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Removed $csvExportPath from local disk..." + + Remove-Item -Path $jsonExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Removed $jsonExportPath from local disk..." +} + +if ("ServicePrincipal" -in $aadObjectsTypes) +{ + $aadObjects = @() + + "Getting AAD service principals..." + $spns = Get-MgServicePrincipal -All -ExpandProperty Owners -Property Id,AppId,DeletedDateTime,DisplayName,KeyCredentials,PasswordCredentials,Owners,ServicePrincipalNames,ServicePrincipalType,AccountEnabled,AlternativeNames + "Found $($spns.Count) AAD service principals" + + foreach ($spn in $spns) + { + $owners = $null + if ($spn.Owners.Count -gt 0) + { + $owners = ($spn.Owners | Where-Object { [string]::IsNullOrEmpty($_.DeletedDateTime) }).Id | ConvertTo-Json -Compress + } + $deletedDate = $null + if ($spn.DeletedDateTime) + { + $deletedDate = (Get-Date($spn.DeletedDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + $aadObject = New-Object PSObject -Property @{ + Timestamp = $timestamp + TenantGuid = $tenantId + Cloud = $cloudEnvironment + ObjectId = $spn.Id + ObjectType = "ServicePrincipal" + ObjectSubType = $spn.ServicePrincipalType + DisplayName = $spn.DisplayName + SecurityEnabled = $spn.AccountEnabled + ApplicationId = $spn.AppId + Keys = (Build-CredObjectWithDates -appObject $spn) | ConvertTo-Json -Compress + PrincipalNames = (Build-PrincipalNames -appObject $spn) | ConvertTo-Json -Compress + Owners = $owners + DeletedDate = $deletedDate + } + $aadObjects += $aadObject + } + + $jsonExportPath = "$fileDate-$tenantId-aadobjects-spns.json" + $csvExportPath = "$fileDate-$tenantId-aadobjects-spns.csv" + + $aadObjects | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath + "Exported to JSON: $($aadObjects.Count) lines" + $aadObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json + "JSON Import: $($aadObjectsJson.Count) lines" + $aadObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath + "Export to $csvExportPath" + + $csvBlobName = $csvExportPath + $csvProperties = @{"ContentType" = "text/csv"}; + + Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Uploaded $csvBlobName to Blob Storage..." + + Remove-Item -Path $csvExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Removed $csvExportPath from local disk..." + + Remove-Item -Path $jsonExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Removed $jsonExportPath from local disk..." +} + +if ("User" -in $aadObjectsTypes) +{ + $aadObjects = @() + + if ([string]::IsNullOrEmpty($userFilter)) + { + "Getting AAD users..." + $users = Get-MgUser -All -Property Id,AccountEnabled,DisplayName,UserPrincipalName,UserType,CreatedDateTime,DeletedDateTime + } + else + { + "Getting AAD users with filter $userFilter..." + $users = Get-MgUser -Filter $userFilter -All -Property Id,AccountEnabled,DisplayName,UserPrincipalName,UserType,CreatedDateTime,DeletedDateTime + } + "Found $($users.Count) AAD users" + + foreach ($user in $users) + { + $createdDate = $null + if ($user.CreatedDateTime) + { + $createdDate = (Get-Date($user.CreatedDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + $deletedDate = $null + if ($user.DeletedDateTime) + { + $deletedDate = (Get-Date($user.DeletedDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + $aadObject = New-Object PSObject -Property @{ + Timestamp = $timestamp + TenantGuid = $tenantId + Cloud = $cloudEnvironment + ObjectId = $user.Id + ObjectType = "User" + ObjectSubType = $user.UserType + DisplayName = $user.DisplayName + SecurityEnabled = $user.AccountEnabled + PrincipalNames = $user.UserPrincipalName + CreatedDate = $createdDate + DeletedDate = $deletedDate + } + $aadObjects += $aadObject + } + + $jsonExportPath = "$fileDate-$tenantId-aadobjects-users.json" + $csvExportPath = "$fileDate-$tenantId-aadobjects-users.csv" + + $aadObjects | Export-Csv -NoTypeInformation -Path $csvExportPath + "Export to $csvExportPath" + + $csvBlobName = $csvExportPath + $csvProperties = @{"ContentType" = "text/csv"}; + + Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Uploaded $csvBlobName to Blob Storage..." + + Remove-Item -Path $csvExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Removed $csvExportPath from local disk..." +} + +if ("Group" -in $aadObjectsTypes) +{ + $aadObjects = @() + + if ([string]::IsNullOrEmpty($groupFilter)) + { + "Getting AAD groups..." + $groups = Get-MgGroup -All -ExpandProperty Members -Property Id,SecurityEnabled,DisplayName,Members,CreatedDateTime,DeletedDateTime,GroupTypes + } + else + { + "Getting AAD groups with filter $groupFilter..." + $groups = Get-MgGroup -Filter $groupFilter -All -ExpandProperty Members -Property Id,SecurityEnabled,DisplayName,Members,CreatedDateTime,DeletedDateTime,GroupTypes + } + "Found $($groups.Count) AAD groups" + + foreach ($group in $groups) + { + $groupMembers = $null + if ($group.Members.Count -gt 0) + { + $groupMembers = $group.Members.Id | ConvertTo-Json -Compress + } + $createdDate = $null + if ($group.CreatedDateTime) + { + $createdDate = (Get-Date($group.CreatedDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + $deletedDate = $null + if ($group.DeletedDateTime) + { + $deletedDate = (Get-Date($group.DeletedDateTime)).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + $aadObject = New-Object PSObject -Property @{ + Timestamp = $timestamp + TenantGuid = $tenantId + Cloud = $cloudEnvironment + ObjectId = $group.Id + ObjectType = "Group" + ObjectSubType = $group.GroupTypes | ConvertTo-Json -Compress + DisplayName = $group.DisplayName + SecurityEnabled = $group.SecurityEnabled + PrincipalNames = $groupMembers + CreatedDate = $createdDate + DeletedDate = $deletedDate + } + $aadObjects += $aadObject + } + + $jsonExportPath = "$fileDate-$tenantId-aadobjects-groups.json" + $csvExportPath = "$fileDate-$tenantId-aadobjects-groups.csv" + + $aadObjects | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath + "Exported to JSON: $($aadObjects.Count) lines" + $aadObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json + "JSON Import: $($aadObjectsJson.Count) lines" + $aadObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath + "Export to $csvExportPath" + + $csvBlobName = $csvExportPath + $csvProperties = @{"ContentType" = "text/csv"}; + + Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Uploaded $csvBlobName to Blob Storage..." + + Remove-Item -Path $csvExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Removed $csvExportPath from local disk..." + + Remove-Item -Path $jsonExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Removed $jsonExportPath from local disk..." +} + +"DONE!" \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGAppGatewayPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGAppGatewayPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..226208aa8 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGAppGatewayPropertiesToBlobStorage.ps1 @@ -0,0 +1,231 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGAppGatewayContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argappgwexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allAppGWs = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$appGWsTotal = @() +$resultsSoFar = 0 + +Write-Output "Querying for Application Gateways properties" + +$argQuery = @" +resources +| where type =~ 'Microsoft.Network/applicationGateways' +| extend gatewayIPsCount = array_length(properties.gatewayIPConfigurations) +| extend frontendIPsCount = array_length(properties.frontendIPConfigurations) +| extend frontendPortsCount = array_length(properties.frontendPorts) +| extend backendPoolsCount = array_length(properties.backendAddressPools) +| extend httpSettingsCount = array_length(properties.backendHttpSettingsCollection) +| extend httpListenersCount = array_length(properties.httpListeners) +| extend urlPathMapsCount = array_length(properties.urlPathMaps) +| extend requestRoutingRulesCount = array_length(properties.requestRoutingRules) +| extend probesCount = array_length(properties.probes) +| extend rewriteRulesCount = array_length(properties.rewriteRuleSets) +| extend redirectConfsCount = array_length(properties.redirectConfigurations) +| project id, name, resourceGroup, subscriptionId, tenantId, location, zones, skuName = properties.sku.name, skuTier = properties.sku.tier, skuCapacity = properties.sku.capacity, enableHttp2 = properties.enableHttp2, gatewayIPsCount, frontendIPsCount, frontendPortsCount, httpSettingsCount, httpListenersCount, backendPoolsCount, urlPathMapsCount, requestRoutingRulesCount, probesCount, rewriteRulesCount, redirectConfsCount, tags +| join kind=leftouter ( + resources + | where type =~ 'Microsoft.Network/applicationGateways' + | mvexpand backendPools = properties.backendAddressPools + | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations) + | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses) + | summarize backendIPCount = sum(backendIPCount), backendAddressesCount = sum(backendAddressesCount) by id +) on id +| project-away id1 +| order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $appGWs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $appGWs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($appGWs -and $appGWs.GetType().Name -eq "PSResourceGraphResponse") + { + $appGWs = $appGWs.Data + } + $resultsCount = $appGWs.Count + $resultsSoFar += $resultsCount + $appGWsTotal += $appGWs + +} while ($resultsCount -eq $ARGPageSize) + +Write-Output "Found $($appGWsTotal.Count) Application Gateway entries" + +<# + Building CSV entries +#> + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +foreach ($appGW in $appGWsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $appGW.tenantId + SubscriptionGuid = $appGW.subscriptionId + ResourceGroupName = $appGW.resourceGroup.ToLower() + InstanceName = $appGW.name.ToLower() + InstanceId = $appGW.id.ToLower() + SkuName = $appGW.skuName + SkuTier = $appGW.skuTier + SkuCapacity = $appGW.skuCapacity + Location = $appGW.location + Zones = $appGW.zones + EnableHttp2 = $appGW.enableHttp2 + GatewayIPsCount = $appGW.gatewayIPsCount + FrontendIPsCount = $appGW.frontendIPsCount + FrontendPortsCount = $appGW.frontendPortsCount + BackendIPCount = $appGW.backendIPCount + BackendAddressesCount = $appGW.backendAddressesCount + HttpSettingsCount = $appGW.httpSettingsCount + HttpListenersCount = $appGW.httpListenersCount + BackendPoolsCount = $appGW.backendPoolsCount + ProbesCount = $appGW.probesCount + UrlPathMapsCount = $appGW.urlPathMapsCount + RequestRoutingRulesCount = $appGW.requestRoutingRulesCount + RewriteRulesCount = $appGW.rewriteRulesCount + RedirectConfsCount = $appGW.redirectConfsCount + StatusDate = $statusDate + Tags = $appGW.tags + } + + $allAppGWs += $logentry +} + +<# + Actually exporting CSV to Azure Storage +#> + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-appgws-$subscriptionSuffix.csv" + +$allAppGWs | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGAppServicePlanPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGAppServicePlanPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..b81e05bbd --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGAppServicePlanPropertiesToBlobStorage.ps1 @@ -0,0 +1,209 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGAppServicePlanContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argappserviceplanexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allasp = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$aspTotal = @() + +$resultsSoFar = 0 + +Write-Output "Querying for App Service Plan properties" + +$argQuery = @" + resources + | where type =~ 'microsoft.web/serverfarms' + | extend skuName = sku.name, skuTier = sku.tier, skuCapacity = sku.capacity, skuFamily = sku.family, skuSize = sku.size + | extend computeMode = properties.computeMode, zoneRedundant = properties.zoneRedundant + | extend numberOfWorkers = properties.numberOfWorkers, currentNumberOfWorkers = properties.currentNumberOfWorkers, maximumNumberOfWorkers = properties.maximumNumberOfWorkers + | extend numberOfSites = properties.numberOfSites, planName = properties.planName + | order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $asp = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $asp = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($asp -and $asp.GetType().Name -eq "PSResourceGraphResponse") + { + $asp = $asp.Data + } + $resultsCount = $asp.Count + $resultsSoFar += $resultsCount + $aspTotal += $asp + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($aspTotal.Count) App Service Plan entries" + +foreach ($asplan in $aspTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $asplan.tenantId + SubscriptionGuid = $asplan.subscriptionId + ResourceGroupName = $asplan.resourceGroup.ToLower() + ZoneRedundant = $asplan.zoneRedundant + Location = $asplan.location + AppServicePlanName = $asplan.name.ToLower() + InstanceId = $asplan.id.ToLower() + Kind = $asplan.kind + SkuName = $asplan.skuName + SkuTier = $asplan.skuTier + SkuCapacity = $asplan.skuCapacity + SkuFamily = $asplan.skuFamily + SkuSize = $asplan.skuSize + ComputeMode = $asplan.computeMode + NumberOfWorkers = $asplan.numberOfWorkers + CurrentNumberOfWorkers = $asplan.currentNumberOfWorkers + MaximumNumberOfWorkers = $asplan.maximumNumberOfWorkers + NumberOfSites = $asplan.numberOfSites + PlanName = $asplan.planName + Tags = $asplan.tags + StatusDate = $statusDate + } + + $allasp += $logentry +} + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-asp-$subscriptionSuffix.csv" + +$allasp | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..d13e82b75 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1 @@ -0,0 +1,198 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGAvailabilitySetContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argavailsetexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allAvSets = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$avSetsTotal = @() +$resultsSoFar = 0 + +Write-Output "Querying for Availability Set properties" + +$argQuery = @" +resources +| where type =~ 'Microsoft.Compute/availabilitySets' +| project id, name, location, resourceGroup, subscriptionId, tenantId, skuName = tostring(sku.name), faultDomains = tostring(properties.platformFaultDomainCount), updateDomains = tostring(properties.platformUpdateDomainCount), vmCount = array_length(properties.virtualMachines), tags, zones +| order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $avSets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $avSets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($avSets -and $avSets.GetType().Name -eq "PSResourceGraphResponse") + { + $avSets = $avSets.Data + } + $resultsCount = $avSets.Count + $resultsSoFar += $resultsCount + $avSetsTotal += $avSets + +} while ($resultsCount -eq $ARGPageSize) + +Write-Output "Found $($avSetsTotal.Count) Availability Set entries" + +<# + Building CSV entries +#> + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +foreach ($avSet in $avSetsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $avSet.tenantId + SubscriptionGuid = $avSet.subscriptionId + ResourceGroupName = $avSet.resourceGroup.ToLower() + InstanceName = $avSet.name.ToLower() + InstanceId = $avSet.id.ToLower() + SkuName = $avSet.skuName + Location = $avSet.location + FaultDomains = $avSet.faultDomains + UpdateDomains = $avSet.updateDomains + VmCount = $avSet.vmCount + StatusDate = $statusDate + Tags = $avSet.tags + Zones = $avSet.zones + } + + $allAvSets += $logentry +} + +<# + Actually exporting CSV to Azure Storage +#> + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-availsets-$subscriptionSuffix.csv" + +$allAvSets | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGLoadBalancerPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGLoadBalancerPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..103ba7cf6 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGLoadBalancerPropertiesToBlobStorage.ps1 @@ -0,0 +1,222 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGLoadBalancerContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "arglbexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allLBs = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$LBsTotal = @() +$resultsSoFar = 0 + +Write-Output "Querying for Load Balancer properties" + +$argQuery = @" +resources +| where type =~ 'Microsoft.Network/loadBalancers' +| extend lbType = iif(properties.frontendIPConfigurations contains 'publicIPAddress', 'Public', iif(properties.frontendIPConfigurations contains 'privateIPAddress', 'Internal', 'Unknown')) +| extend lbRulesCount = array_length(properties.loadBalancingRules) +| extend frontendIPsCount = array_length(properties.frontendIPConfigurations) +| extend inboundNatRulesCount = array_length(properties.inboundNatRules) +| extend outboundRulesCount = array_length(properties.outboundRules) +| extend inboundNatPoolsCount = array_length(properties.inboundNatPools) +| extend backendPoolsCount = array_length(properties.backendAddressPools) +| extend probesCount = array_length(properties.probes) +| project id, name, resourceGroup, subscriptionId, tenantId, location, skuName = sku.name, skuTier = sku.tier, lbType, lbRulesCount, frontendIPsCount, inboundNatRulesCount, outboundRulesCount, inboundNatPoolsCount, backendPoolsCount, probesCount, tags +| join kind=leftouter ( + resources + | where type =~ 'Microsoft.Network/loadBalancers' + | mvexpand backendPools = properties.backendAddressPools + | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations) + | extend backendAddressesCount = array_length(backendPools.properties.loadBalancerBackendAddresses) + | summarize backendIPCount = sum(backendIPCount), backendAddressesCount = sum(backendAddressesCount) by id +) on id +| project-away id1 +| order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $LBs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $LBs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($LBs -and $LBs.GetType().Name -eq "PSResourceGraphResponse") + { + $LBs = $LBs.Data + } + $resultsCount = $LBs.Count + $resultsSoFar += $resultsCount + $LBsTotal += $LBs + +} while ($resultsCount -eq $ARGPageSize) + +Write-Output "Found $($LBsTotal.Count) Load Balancer entries" + +<# + Building CSV entries +#> + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +foreach ($lb in $LBsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $lb.tenantId + SubscriptionGuid = $lb.subscriptionId + ResourceGroupName = $lb.resourceGroup.ToLower() + InstanceName = $lb.name.ToLower() + InstanceId = $lb.id.ToLower() + SkuName = $lb.skuName + SkuTier = $lb.skuTier + Location = $lb.location + LbType = $lb.lbType + LbRulesCount = $lb.lbRulesCount + InboundNatRulesCount = $lb.inboundNatRulesCount + OutboundRulesCount = $lb.outboundRulesCount + FrontendIPsCount = $lb.frontendIPsCount + BackendIPCount = $lb.backendIPCount + BackendAddressesCount = $lb.backendAddressesCount + InboundNatPoolsCount = $lb.inboundNatPoolsCount + BackendPoolsCount = $lb.backendPoolsCount + ProbesCount = $lb.probesCount + StatusDate = $statusDate + Tags = $lb.tags + } + + $allLBs += $logentry +} + +<# + Actually exporting CSV to Azure Storage +#> + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-lbs-$subscriptionSuffix.csv" + +$allLBs | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGManagedDisksPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGManagedDisksPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..3ba66de18 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGManagedDisksPropertiesToBlobStorage.ps1 @@ -0,0 +1,232 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGDiskContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argdiskexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$alldisks = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$mdisksTotal = @() +$resultsSoFar = 0 + +<# + Getting all Managed Disks properties with Azure Resource Graph query +#> + +Write-Output "Querying for ARM Managed Disks properties" + +$argQuery = @" + resources + | where type =~ 'Microsoft.Compute/disks' + | extend DiskId = tolower(id), OwnerVmId = tolower(managedBy) + | join kind=leftouter ( + resources + | where type =~ 'Microsoft.Compute/virtualMachines' and array_length(properties.storageProfile.dataDisks) > 0 + | extend OwnerVmId = tolower(id) + | mv-expand DataDisks = properties.storageProfile.dataDisks + | extend DiskId = tolower(DataDisks.managedDisk.id), diskCaching = tostring(DataDisks.caching), diskType = 'Data' + | project DiskId, OwnerVmId, diskCaching, diskType + | union ( + resources + | where type =~ 'Microsoft.Compute/virtualMachines' + | extend OwnerVmId = tolower(id) + | extend DiskId = tolower(properties.storageProfile.osDisk.managedDisk.id), diskCaching = tostring(properties.storageProfile.osDisk.caching), diskType = 'OS' + | project DiskId, OwnerVmId, diskCaching, diskType + ) + ) on OwnerVmId, DiskId + | project-away OwnerVmId, DiskId, OwnerVmId1, DiskId1 + | order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($mdisks -and $mdisks.GetType().Name -eq "PSResourceGraphResponse") + { + $mdisks = $mdisks.Data + } + $resultsCount = $mdisks.Count + $resultsSoFar += $resultsCount + $mdisksTotal += $mdisks + +} while ($resultsCount -eq $ARGPageSize) + +Write-Output "Found $($mdisksTotal.Count) Managed Disk entries" + +<# + Building CSV entries +#> + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +foreach ($disk in $mdisksTotal) +{ + $ownerVmId = $null + if ($null -ne $disk.managedBy) + { + $ownerVmId = $disk.managedBy.ToLower() + } + + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $disk.tenantId + SubscriptionGuid = $disk.subscriptionId + ResourceGroupName = $disk.resourceGroup.ToLower() + DiskName = $disk.name.ToLower() + InstanceId = $disk.id.ToLower() + Location = $disk.location + OwnerVMId = $ownerVmId + DeploymentModel = "Managed" + DiskType = $disk.diskType + TimeCreated = $disk.properties.timeCreated + DiskIOPS = $disk.properties.diskIOPSReadWrite + DiskThroughput = $disk.properties.diskMBpsReadWrite + DiskTier = $disk.properties.tier + DiskState = $disk.properties.diskState + EncryptionType = $disk.properties.encryption.type + Zones = $disk.zones + Caching = $disk.diskCaching + DiskSizeGB = $disk.properties.diskSizeGB + SKU = $disk.sku.name + StatusDate = $statusDate + Tags = $disk.tags + } + + $alldisks += $logentry +} + +<# + Actually exporting CSV to Azure Storage +#> + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-disks-$subscriptionSuffix.csv" + +$alldisks | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGNICPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGNICPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..6ae3d1946 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGNICPropertiesToBlobStorage.ps1 @@ -0,0 +1,235 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGNICContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argnicexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allnics = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$nicsTotal = @() + +$resultsSoFar = 0 + +Write-Output "Querying for NIC properties" + +$argQuery = @" + resources + | where type =~ 'microsoft.network/networkinterfaces' + | extend isPrimary = properties.primary + | extend enableAcceleratedNetworking = properties.enableAcceleratedNetworking + | extend enableIPForwarding = properties.enableIPForwarding + | extend tapConfigurationsCount = array_length(properties.tapConfigurations) + | extend hostedWorkloadsCount = array_length(properties.hostedWorkloads) + | extend internalDomainNameSuffix = properties.dnsSettings.internalDomainNameSuffix + | extend appliedDnsServers = properties.dnsSettings.appliedDnsServers + | extend dnsServers = properties.dnsSettings.dnsServers + | extend ownerVMId = tolower(properties.virtualMachine.id) + | extend ownerPEId = tolower(properties.privateEndpoint.id) + | extend macAddress = properties.macAddress + | extend nicType = properties.nicType + | extend nicNsgId = tolower(properties.networkSecurityGroup.id) + | mv-expand ipconfigs = properties.ipConfigurations + | project-away properties + | extend privateIPAddressVersion = tostring(ipconfigs.properties.privateIPAddressVersion) + | extend privateIPAllocationMethod = tostring(ipconfigs.properties.privateIPAllocationMethod) + | extend isIPConfigPrimary = tostring(ipconfigs.properties.primary) + | extend privateIPAddress = tostring(ipconfigs.properties.privateIPAddress) + | extend publicIPId = tolower(ipconfigs.properties.publicIPAddress.id) + | extend IPConfigName = tostring(ipconfigs.name) + | extend subnetId = tolower(ipconfigs.properties.subnet.id) + | project-away ipconfigs + | order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $nics = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $nics = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($nics -and $nics.GetType().Name -eq "PSResourceGraphResponse") + { + $nics = $nics.Data + } + $resultsCount = $nics.Count + $resultsSoFar += $resultsCount + $nicsTotal += $nics + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($nicsTotal.Count) ARM VNet nic entries" + +foreach ($nic in $nicsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $nic.tenantId + SubscriptionGuid = $nic.subscriptionId + ResourceGroupName = $nic.resourceGroup.ToLower() + Location = $nic.location + Name = $nic.name.ToLower() + InstanceId = $nic.id.ToLower() + IsPrimary = $nic.isPrimary + EnableAcceleratedNetworking = $nic.enableAcceleratedNetworking + EnableIPForwarding = $nic.enableIPForwarding + TapConfigurationsCount = $nic.tapConfigurationsCount + HostedWorkloadsCount = $nic.hostedWorkloadsCount + InternalDomainNameSuffix = $nic.internalDomainNameSuffix + AppliedDnsServers = $nic.appliedDnsServers + DnsServers = $nic.dnsServers + OwnerVMId = $nic.ownerVMId + OwnerPEId = $nic.ownerPEId + MacAddress = $nic.macAddress + NicType = $nic.nicType + NicNSGId = $nic.nicNsgId + PrivateIPAddressVersion = $nic.privateIPAddressVersion + PrivateIPAllocationMethod = $nic.privateIPAllocationMethod + IsIPConfigPrimary = $nic.isIPConfigPrimary + PrivateIPAddress = $nic.privateIPAddress + PublicIPId = $nic.publicIPId + IPConfigName = $nic.IPConfigName + SubnetId = $nic.subnetId + Tags = $nic.tags + StatusDate = $statusDate + } + + $allnics += $logentry +} + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-nics-$subscriptionSuffix.csv" + +$allnics | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGNSGPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGNSGPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..763dbe32d --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGNSGPropertiesToBlobStorage.ps1 @@ -0,0 +1,217 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGNSGContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argnsgexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allnsgRules = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$nsgRulesTotal = @() + +$resultsSoFar = 0 + +Write-Output "Querying for NSG properties" + +$argQuery = @" +resources +| where type =~ 'Microsoft.Network/networkSecurityGroups' +| extend nicCount = iif(isnotempty(properties.networkInterfaces),array_length(properties.networkInterfaces),0) +| extend subnetCount = iif(isnotempty(properties.subnets),array_length(properties.subnets),0) +| mvexpand securityRules = properties.securityRules +| extend ruleName = tolower(securityRules.name) +| extend ruleProtocol = tolower(securityRules.properties.protocol) +| extend ruleDirection = tolower(securityRules.properties.direction) +| extend rulePriority = toint(securityRules.properties.priority) +| extend ruleAccess = tolower(securityRules.properties.access) +| extend ruleDestinationAddresses = tolower(iif(array_length(securityRules.properties.destinationAddressPrefixes) > 0,strcat_array(securityRules.properties.destinationAddressPrefixes, ','),securityRules.properties.destinationAddressPrefix)) +| extend ruleSourceAddresses = tolower(iif(array_length(securityRules.properties.sourceAddressPrefixes) > 0,strcat_array(securityRules.properties.sourceAddressPrefixes, ','),securityRules.properties.sourceAddressPrefix)) +| extend ruleDestinationPorts = iif(array_length(securityRules.properties.destinationPortRanges) > 0,strcat_array(securityRules.properties.destinationPortRanges, ','),securityRules.properties.destinationPortRange) +| extend ruleSourcePorts = iif(array_length(securityRules.properties.sourcePortRanges) > 0,strcat_array(securityRules.properties.sourcePortRanges, ','),securityRules.properties.sourcePortRange) +| extend ruleId = tolower(securityRules.id) +| project-away securityRules, properties +| order by ruleId asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $nsgRules = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $nsgRules = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($nsgRules -and $nsgRules.GetType().Name -eq "PSResourceGraphResponse") + { + $nsgRules = $nsgRules.Data + } + $resultsCount = $nsgRules.Count + $resultsSoFar += $resultsCount + $nsgRulesTotal += $nsgRules + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($nsgRulesTotal.Count) ARM NSG entries" + +foreach ($nsgRule in $nsgRulesTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $nsgRule.tenantId + SubscriptionGuid = $nsgRule.subscriptionId + ResourceGroupName = $nsgRule.resourceGroup.ToLower() + Location = $nsgRule.location + NSGName = $nsgRule.name.ToLower() + InstanceId = $nsgRule.id.ToLower() + NicCount = $nsgRule.nicCount + SubnetCount = $nsgRule.subnetCount + RuleName = $nsgRule.ruleName + RuleProtocol = $nsgRule.ruleProtocol + RuleDirection = $nsgRule.ruleDirection + RulePriority = $nsgRule.rulePriority + RuleAccess = $nsgRule.ruleAccess + RuleDestinationAddresses = $nsgRule.ruleDestinationAddresses + RuleSourceAddresses = $nsgRule.ruleSourceAddresses + RuleDestinationPorts = $nsgRule.ruleDestinationPorts + RuleSourcePorts = $nsgRule.ruleSourcePorts + Tags = $nsgRule.tags + StatusDate = $statusDate + } + + $allnsgRules += $logentry +} + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-nsgrules-$subscriptionSuffix.csv" + +$allnsgRules | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGPublicIpPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGPublicIpPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..d33a8330f --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGPublicIpPropertiesToBlobStorage.ps1 @@ -0,0 +1,275 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGPublicIpContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argpublicipexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allpips = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$pipsTotal = @() + +$resultsSoFar = 0 + +Write-Output "Querying for ARM Public IP properties" + +$argQuery = @" +resources +| where type =~ 'microsoft.network/publicipaddresses' +| extend skuName = tolower(sku.name) +| extend skuTier = tolower(sku.tier) +| extend allocationMethod = tolower(properties.publicIPAllocationMethod) +| extend addressVersion = tolower(properties.publicIPAddressVersion) +| extend associatedResourceId = iif(isnotempty(properties.ipConfiguration.id),tolower(properties.ipConfiguration.id),tolower(properties.natGateway.id)) +| extend ipAddress = tostring(properties.ipAddress) +| extend fqdn = tolower(properties.dnsSettings.fqdn) +| extend publicIpPrefixId = tostring(properties.publicIPPrefix.id) +| order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $pips = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $pips = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($pips -and $pips.GetType().Name -eq "PSResourceGraphResponse") + { + $pips = $pips.Data + } + $resultsCount = $pips.Count + $resultsSoFar += $resultsCount + $pipsTotal += $pips + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($pipsTotal.Count) ARM Public IP entries" + +foreach ($pip in $pipsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $pip.tenantId + SubscriptionGuid = $pip.subscriptionId + ResourceGroupName = $pip.resourceGroup.ToLower() + Location = $pip.location + Name = $pip.name.ToLower() + InstanceId = $pip.id.ToLower() + Model = "ARM" + SkuName = $pip.skuName + SkuTier = $pip.skuTier + AllocationMethod = $pip.allocationMethod + AddressVersion = $pip.addressVersion + AssociatedResourceId = $pip.associatedResourceId + PublicIpPrefixId = $pip.publicIpPrefixId + IPAddress = $pip.ipAddress + FQDN = $pip.fqdn + Zones = $pip.zones + Tags = $pip.tags + StatusDate = $statusDate + } + + $allpips += $logentry +} + +$pipsTotal = @() + +$resultsSoFar = 0 + +Write-Output "Querying for Classic Reserved IP properties" + +$argQuery = @" +resources +| where type =~ 'microsoft.classicnetwork/reservedips' +| extend ipAddress = tostring(properties.ipAddress) +| extend allocationMethod = 'static' +| extend addressVersion = 'ipv4' +| extend associatedResourceId = tolower(properties.attachedTo.id) +| extend ipAddress = tostring(properties.ipAddress) +| order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $pips = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $pips = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($pips -and $pips.GetType().Name -eq "PSResourceGraphResponse") + { + $pips = $pips.Data + } + $resultsCount = $pips.Count + $resultsSoFar += $resultsCount + $pipsTotal += $pips + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($pipsTotal.Count) Classic Reserved IP entries" + +foreach ($pip in $pipsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $pip.tenantId + SubscriptionGuid = $pip.subscriptionId + ResourceGroupName = $pip.resourceGroup.ToLower() + Location = $pip.location + Name = $pip.name.ToLower() + InstanceId = $pip.id.ToLower() + Model = "Classic" + AllocationMethod = $pip.allocationMethod + AddressVersion = $pip.addressVersion + AssociatedResourceId = $pip.associatedResourceId + IPAddress = $pip.ipAddress + StatusDate = $statusDate + } + + $allpips += $logentry +} + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-publicips-$subscriptionSuffix.csv" + +$allpips | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGResourceContainersPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGResourceContainersPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..b7609cda6 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGResourceContainersPropertiesToBlobStorage.ps1 @@ -0,0 +1,272 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGResourceContainersContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argrescontainersexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allResourceContainers = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$rgsTotal = @() +$subsTotal = @() + +$resultsSoFar = 0 + +Write-Output "Querying for resource groups..." + +$argQuery = @" + resourcecontainers + | where type == "microsoft.resources/subscriptions/resourcegroups" + | join kind=leftouter ( + resources + | summarize ResourceCount= count() by subscriptionId, resourceGroup + ) on subscriptionId, resourceGroup + | extend ResourceCount = iif(isempty(ResourceCount), 0, ResourceCount) + | project id, name, type, tenantId, location, subscriptionId, managedBy, tags, properties, ResourceCount + | order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $rgs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $rgs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($rgs -and $rgs.GetType().Name -eq "PSResourceGraphResponse") + { + $rgs = $rgs.Data + } + $resultsCount = $rgs.Count + $resultsSoFar += $resultsCount + $rgsTotal += $rgs + +} while ($resultsCount -eq $ARGPageSize) + +$resultsSoFar = 0 + +Write-Output "Querying for subscriptions" + +$argQuery = @" + resourcecontainers + | where type == "microsoft.resources/subscriptions" + | join kind=leftouter ( + resources + | summarize ResourceCount= count() by subscriptionId + ) on subscriptionId + | extend ResourceCount = iif(isempty(ResourceCount), 0, ResourceCount) + | project id, name, type, tenantId, subscriptionId, managedBy, tags, properties, ResourceCount + | order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $subs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $subs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($subs -and $subs.GetType().Name -eq "PSResourceGraphResponse") + { + $subs = $subs.Data + } + $resultsCount = $subs.Count + $resultsSoFar += $resultsCount + $subsTotal += $subs + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($rgsTotal.Count) RG entries" + +foreach ($rg in $rgsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $rg.tenantId + SubscriptionGuid = $rg.subscriptionId + Location = $rg.location + ContainerType = $rg.type + ContainerName = $rg.name.ToLower() + InstanceId = $rg.id.ToLower() + ResourceCount = $rg.ResourceCount + ManagedBy = $rg.managedBy + ContainerProperties = $rg.properties | ConvertTo-Json -Compress + Tags = $rg.tags + StatusDate = $statusDate + } + + $allResourceContainers += $logentry +} + +Write-Output "Building $($subsTotal.Count) subscription entries" + +foreach ($sub in $subsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $sub.tenantId + SubscriptionGuid = $sub.subscriptionId + Location = $sub.location + ContainerType = $sub.type + ContainerName = $sub.name.ToLower() + InstanceId = $sub.id.ToLower() + ResourceCount = $sub.ResourceCount + ManagedBy = $sub.managedBy + ContainerProperties = $sub.properties | ConvertTo-Json -Compress + Tags = $sub.tags + StatusDate = $statusDate + } + + $allResourceContainers += $logentry +} + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$jsonExportPath = "$today-rescontainers-$subscriptionSuffix.json" +$csvExportPath = "$today-rescontainers-$subscriptionSuffix.csv" + +$allResourceContainers | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath +Write-Output "Exported to JSON: $($allResourceContainers.Count) lines" +$allResourceContainersJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json +Write-Output "JSON Import: $($allResourceContainersJson.Count) lines" +$allResourceContainersJson | Export-Csv -NoTypeInformation -Path $csvExportPath +Write-Output "Export to $csvExportPath" + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGSqlDatabasePropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGSqlDatabasePropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..adf0979eb --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGSqlDatabasePropertiesToBlobStorage.ps1 @@ -0,0 +1,204 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGSqlDatabaseContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argsqldbexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$alldbs = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$dbsTotal = @() + +$resultsSoFar = 0 + +Write-Output "Querying for SQL Databases properties" + +$argQuery = @" + resources + | where type =~ 'microsoft.sql/servers/databases' and name != 'master' + | extend skuName = sku.name, skuTier = sku.tier, skuCapacity = sku.capacity + | extend storageAccountType = properties.storageAccountType, licenseType = properties.licenseType, serviceObjectiveName = properties.currentServiceObjectiveName + | extend zoneRedundant = properties.zoneRedundant, maxSizeBytes = properties.maxSizeBytes, maxLogSizeBytes = properties.maxLogSizeBytes + | order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $dbs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $dbs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($dbs -and $dbs.GetType().Name -eq "PSResourceGraphResponse") + { + $dbs = $dbs.Data + } + $resultsCount = $dbs.Count + $resultsSoFar += $resultsCount + $dbsTotal += $dbs + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($dbsTotal.Count) SQL Database entries" + +foreach ($db in $dbsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $db.tenantId + SubscriptionGuid = $db.subscriptionId + ResourceGroupName = $db.resourceGroup.ToLower() + ZoneRedundant = $db.zoneRedundant + Location = $db.location + DBName = $db.name.ToLower() + InstanceId = $db.id.ToLower() + SkuName = $db.skuName + SkuTier = $db.skuTier + SkuCapacity = $db.skuCapacity + ServiceObjectiveName = $db.serviceObjectiveName + StorageAccountType = $db.storageAccountType + LicenseType = $db.licenseType + MaxSizeBytes = $db.maxSizeBytes + MaxLogSizeBytes = $db.maxLogSizeBytes + Tags = $db.tags + StatusDate = $statusDate + } + + $alldbs += $logentry +} + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-sqldbs-$subscriptionSuffix.csv" + +$alldbs | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..e48c45406 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1 @@ -0,0 +1,236 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGVhdContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argvhdexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$alldisks = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$mdisksTotal = @() +$resultsSoFar = 0 + +Write-Output "Querying for ARM Unmanaged OS Disks properties" + +$argQuery = @" +resources +| where type =~ 'Microsoft.Compute/virtualMachines' and isnull(properties.storageProfile.osDisk.managedDisk) +| extend diskType = 'OS', diskCaching = tostring(properties.storageProfile.osDisk.caching), diskSize = tostring(properties.storageProfile.osDisk.diskSizeGB) +| extend vhdUriParts = split(tostring(properties.storageProfile.osDisk.vhd.uri),'/') +| extend diskStorageAccountName = tostring(split(vhdUriParts[2],'.')[0]), diskContainerName = tostring(vhdUriParts[3]), diskVhdName = tostring(vhdUriParts[4]) +| order by id, diskStorageAccountName, diskContainerName, diskVhdName +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($mdisks -and $mdisks.GetType().Name -eq "PSResourceGraphResponse") + { + $mdisks = $mdisks.Data + } + $resultsCount = $mdisks.Count + $resultsSoFar += $resultsCount + $mdisksTotal += $mdisks + +} while ($resultsCount -eq $ARGPageSize) + +$resultsSoFar = 0 + +Write-Output "Found $($mdisksTotal.Count) Unmanaged OS Disk entries" + +Write-Output "Querying for ARM Unmanaged Data Disks properties" + +$argQuery = @" +resources +| where type =~ 'Microsoft.Compute/virtualMachines' and isnull(properties.storageProfile.osDisk.managedDisk) +| mvexpand dataDisks = properties.storageProfile.dataDisks +| extend diskType = 'Data', diskCaching = tostring(dataDisks.caching), diskSize = tostring(dataDisks.diskSizeGB) +| extend vhdUriParts = split(tostring(dataDisks.vhd.uri),'/') +| extend diskStorageAccountName = tostring(split(vhdUriParts[2],'.')[0]), diskContainerName = tostring(vhdUriParts[3]), diskVhdName = tostring(vhdUriParts[4]) +| order by id, diskStorageAccountName, diskContainerName, diskVhdName +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($mdisks -and $mdisks.GetType().Name -eq "PSResourceGraphResponse") + { + $mdisks = $mdisks.Data + } + $resultsCount = $mdisks.Count + $resultsSoFar += $resultsCount + $mdisksTotal += $mdisks + +} while ($resultsCount -eq $ARGPageSize) + +Write-Output "Found overall $($mdisksTotal.Count) Unmanaged Disk entries" + +<# + Building CSV entries +#> + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +foreach ($disk in $mdisksTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $disk.tenantId + SubscriptionGuid = $disk.subscriptionId + ResourceGroupName = $disk.resourceGroup.ToLower() + DiskName = $disk.diskVhdName.ToLower() + InstanceId = ($disk.diskStorageAccountName + "/" + $disk.diskContainerName + "/" + $disk.diskVhdName).ToLower() + OwnerVMId = $disk.id.ToLower() + Location = $disk.location + DeploymentModel = "Unmanaged" + DiskType = $disk.diskType + Caching = $disk.diskCaching + DiskSizeGB = $disk.diskSize + StatusDate = $statusDate + Tags = $disk.tags + } + + $alldisks += $logentry +} + +<# + Actually exporting CSV to Azure Storage +#> + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-vhds-$subscriptionSuffix.csv" + +$alldisks | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGVMSSPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGVMSSPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..81f39771a --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGVMSSPropertiesToBlobStorage.ps1 @@ -0,0 +1,239 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGVMSSContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argvmssexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +Write-Output "Getting VM sizes details for $referenceRegion" +$sizes = Get-AzVMSize -Location $referenceRegion + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allvmss = @() + +if ($TargetSubscription) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = "-" + $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$armVmssTotal = @() + +$resultsSoFar = 0 + +$argQuery = @" +resources +| where type =~ 'microsoft.compute/virtualmachinescalesets' +| project id, tenantId, name, location, resourceGroup, subscriptionId, skUName = tostring(sku.name), + computerNamePrefix = tostring(properties.virtualMachineProfile.osProfile.computerNamePrefix), + usesManagedDisks = iif(isnull(properties.virtualMachineProfile.storageProfile.osDisk.managedDisk), 'false', 'true'), + capacity = tostring(sku.capacity), priority = tostring(properties.virtualMachineProfile.priority), tags, zones, + osType = iif(isnotnull(properties.virtualMachineProfile.osProfile.linuxConfiguration), "Linux", "Windows"), + osDiskSize = tostring(properties.virtualMachineProfile.storageProfile.osDisk.diskSizeGB), + osDiskCaching = tostring(properties.virtualMachineProfile.storageProfile.osDisk.caching), + osDiskSKU = tostring(properties.virtualMachineProfile.storageProfile.osDisk.managedDisk.storageAccountType), + dataDiskCount = iif(isnotnull(properties.virtualMachineProfile.storageProfile.dataDisks), array_length(properties.virtualMachineProfile.storageProfile.dataDisks), 0), + nicCount = array_length(properties.virtualMachineProfile.networkProfile.networkInterfaceConfigurations), + imagePublisher = iif(isnotempty(properties.virtualMachineProfile.storageProfile.imageReference.publisher),tostring(properties.virtualMachineProfile.storageProfile.imageReference.publisher),'Custom'), + imageOffer = iif(isnotempty(properties.virtualMachineProfile.storageProfile.imageReference.offer),tostring(properties.virtualMachineProfile.storageProfile.imageReference.offer),tostring(properties.virtualMachineProfile.storageProfile.imageReference.id)), + imageSku = tostring(properties.virtualMachineProfile.storageProfile.imageReference.sku), + imageVersion = tostring(properties.virtualMachineProfile.storageProfile.imageReference.version), + imageExactVersion = tostring(properties.virtualMachineProfile.storageProfile.imageReference.exactVersion), + singlePlacementGroup = tostring(properties.singlePlacementGroup), + upgradePolicy = tostring(properties.upgradePolicy.mode), + overProvision = tostring(properties.overprovision), + platformFaultDomainCount = tostring(properties.platformFaultDomainCount), + zoneBalance = tostring(properties.zoneBalance) +| order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $armVmss = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $armVmss = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + + if ($armVmss -and $armVmss.GetType().Name -eq "PSResourceGraphResponse") + { + $armVmss = $armVmss.Data + } + $resultsCount = $armVmss.Count + $resultsSoFar += $resultsCount + $armVmssTotal += $armVmss + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($armVmssTotal.Count) VMSS entries" + +foreach ($vmss in $armVmssTotal) +{ + $vmSize = $sizes | Where-Object {$_.name -eq $vmss.skUName} + + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $vmss.tenantId + SubscriptionGuid = $vmss.subscriptionId + ResourceGroupName = $vmss.resourceGroup.ToLower() + Zones = $vmss.zones + Location = $vmss.location + VMSSName = $vmss.name.ToLower() + ComputerNamePrefix = $vmss.computerNamePrefix.ToLower() + InstanceId = $vmss.id.ToLower() + VMSSSize = $vmSize.name.ToLower() + CoresCount = $vmSize.NumberOfCores + MemoryMB = $vmSize.MemoryInMB + OSType = $vmss.osType + DataDiskCount = $vmss.dataDiskCount + NicCount = $vmss.nicCount + StatusDate = $statusDate + Tags = $vmss.tags + Capacity = $vmss.capacity + Priority = $vmss.priority + OSDiskSize = $vmss.osDiskSize + OSDiskCaching = $vmss.osDiskCaching + OSDiskSKU = $vmss.osDiskSKU + SinglePlacementGroup = $vmss.singlePlacementGroup + UpgradePolicy = $vmss.upgradePolicy + OverProvision = $vmss.overProvision + PlatformFaultDomainCount = $vmss.platformFaultDomainCount + ZoneBalance = $vmss.zoneBalance + UsesManagedDisks = $vmss.usesManagedDisks + ImagePublisher = $vmss.imagePublisher + ImageOffer = $vmss.imageOffer + ImageSku = $vmss.imageSku + ImageVersion = $vmss.imageVersion + ImageExactVersion = $vmss.imageExactVersion + } + + $allvmss += $logentry +} + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-vmss-$subscriptionSuffix.csv" + +$allvmss | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGVNetPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGVNetPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..94301bfad --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGVNetPropertiesToBlobStorage.ps1 @@ -0,0 +1,308 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGVNetContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argvnetexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allsubnets = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$subnetsTotal = @() + +$resultsSoFar = 0 + +Write-Output "Querying for ARM VNet properties" + +$argQuery = @" + resources + | where type =~ 'microsoft.network/virtualnetworks' + | mv-expand subnets = properties.subnets limit 400 + | extend peeringsCount = array_length(properties.virtualNetworkPeerings) + | extend vnetPrefixes = properties.addressSpace.addressPrefixes + | extend dnsServers = properties.dhcpOptions.dnsServers + | extend enableDdosProtection = properties.enableDdosProtection + | project-away properties + | extend subnetPrefix = tostring(subnets.properties.addressPrefix) + | extend subnetDelegationsCount = array_length(subnets.properties.delegations) + | extend subnetUsedIPs = iif(isnotempty(subnets.properties.ipConfigurations), array_length(subnets.properties.ipConfigurations), 0) + | extend subnetTotalPrefixIPs = pow(2, 32 - toint(split(subnetPrefix,'/')[1])) - 5 + | extend subnetNsgId = tolower(subnets.properties.networkSecurityGroup.id) + | project id, vnetName = name, resourceGroup, subscriptionId, tenantId, location, vnetPrefixes, dnsServers, subnetName = tolower(tostring(subnets.name)), subnetPrefix, subnetDelegationsCount, subnetTotalPrefixIPs, subnetUsedIPs, subnetNsgId, peeringsCount, enableDdosProtection, tags + | order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $subnets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $subnets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($subnets -and $subnets.GetType().Name -eq "PSResourceGraphResponse") + { + $subnets = $subnets.Data + } + $resultsCount = $subnets.Count + $resultsSoFar += $resultsCount + $subnetsTotal += $subnets + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($subnetsTotal.Count) ARM VNet subnet entries" + +foreach ($subnet in $subnetsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $subnet.tenantId + SubscriptionGuid = $subnet.subscriptionId + ResourceGroupName = $subnet.resourceGroup.ToLower() + Location = $subnet.location + VNetName = $subnet.vnetName.ToLower() + InstanceId = $subnet.id.ToLower() + Model = "ARM" + VNetPrefixes = $subnet.vnetPrefixes + DNSServers = $subnet.dnsServers + PeeringsCount = $subnet.peeringsCount + EnableDdosProtection = $subnet.enableDdosProtection + SubnetName = $subnet.subnetName + SubnetPrefix = $subnet.subnetPrefix + SubnetDelegationsCount = $subnet.subnetDelegationsCount + SubnetTotalPrefixIPs = $subnet.subnetTotalPrefixIPs + SubnetUsedIPs = $subnet.subnetUsedIPs + SubnetNSGId = $subnet.subnetNsgId + Tags = $subnet.tags + StatusDate = $statusDate + } + + $allsubnets += $logentry +} + +$subnetsTotal = @() + +$resultsSoFar = 0 + +Write-Output "Querying for Classic VNet properties" + +$argQuery = @" + resources + | where type =~ 'microsoft.classicnetwork/virtualnetworks' + | extend vNetId = tolower(id) + | mv-expand subnets = properties.subnets limit 400 + | extend subnetName = tolower(tostring(subnets.name)) + | join kind=leftouter ( + resources + | where type =~ 'microsoft.network/virtualnetworks' + | mvexpand peerings = properties.virtualNetworkPeerings limit 400 + | extend vNetId = tolower(tostring(peerings.properties.remoteVirtualNetwork.id)) + | where vNetId has "microsoft.classicnetwork" + | summarize vNetPeerings=count() by vNetId + ) on vNetId + | extend peeringsCount = iif(isnotempty(vNetPeerings), vNetPeerings, 0) + | extend vnetPrefixes = properties.addressSpace.addressPrefixes + | extend dnsServers = properties.dhcpOptions.dnsServers + | project-away properties + | extend subnetPrefix = tostring(subnets.addressPrefix) + | join kind=leftouter ( + resources + | where type =~ 'microsoft.classiccompute/virtualmachines' + | extend networkProfile = properties.networkProfile + | mvexpand subnets = networkProfile.virtualNetwork.subnetNames limit 400 + | extend subnetName = tolower(tostring(subnets)) + | project id, vNetId = tolower(tostring(networkProfile.virtualNetwork.id)), subnetName + | summarize subnetUsedIPs = count() by vNetId, subnetName + ) on vNetId and subnetName + | extend subnetUsedIPs = iif(isnotempty(subnetUsedIPs), subnetUsedIPs, 0) + | extend subnetTotalPrefixIPs = pow(2, 32 - toint(split(subnetPrefix,'/')[1])) - 5 + | extend enableDdosProtection = 'false' + | project vNetId, vnetName = name, resourceGroup, subscriptionId, tenantId, location, vnetPrefixes, dnsServers, subnetName, subnetPrefix, subnetTotalPrefixIPs, subnetUsedIPs, peeringsCount, enableDdosProtection + | order by vNetId asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $subnets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $subnets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($subnets -and $subnets.GetType().Name -eq "PSResourceGraphResponse") + { + $subnets = $subnets.Data + } + $resultsCount = $subnets.Count + $resultsSoFar += $resultsCount + $subnetsTotal += $subnets + +} while ($resultsCount -eq $ARGPageSize) + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($subnetsTotal.Count) Classic VNet subnet entries" + +foreach ($subnet in $subnetsTotal) +{ + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $subnet.tenantId + SubscriptionGuid = $subnet.subscriptionId + ResourceGroupName = $subnet.resourceGroup.ToLower() + Location = $subnet.location + VNetName = $subnet.vnetName.ToLower() + InstanceId = $subnet.vNetId.ToLower() + Model = "Classic" + VNetPrefixes = $subnet.vnetPrefixes + DNSServers = $subnet.dnsServers + PeeringsCount = $subnet.peeringsCount + EnableDdosProtection = $subnet.enableDdosProtection + SubnetName = $subnet.subnetName + SubnetPrefix = $subnet.subnetPrefix + SubnetTotalPrefixIPs = $subnet.subnetTotalPrefixIPs + SubnetUsedIPs = $subnet.subnetUsedIPs + StatusDate = $statusDate + } + + $allsubnets += $logentry +} + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-vnetsubnets-$subscriptionSuffix.csv" + +$allsubnets | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1 new file mode 100644 index 000000000..a30511aad --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1 @@ -0,0 +1,340 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ARGVMContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "argvmexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +# get list of all VM sizes +Write-Output "Getting VM sizes details for $referenceRegion" +$sizes = Get-AzVMSize -Location $referenceRegion + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allvms = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +$armVmsTotal = @() +$classicVmsTotal = @() + +$resultsSoFar = 0 + +<# + Getting all ARM VMs properties with Azure Resource Graph query +#> + +Write-Output "Querying for ARM VM properties" + +$argQuery = @" + resources + | where type =~ 'Microsoft.Compute/virtualMachines' + | extend dataDiskCount = array_length(properties.storageProfile.dataDisks), nicCount = array_length(properties.networkProfile.networkInterfaces) + | extend usesManagedDisks = iif(isnull(properties.storageProfile.osDisk.managedDisk), 'false', 'true') + | extend availabilitySetId = tostring(properties.availabilitySet.id) + | extend bootDiagnosticsEnabled = tostring(properties.diagnosticsProfile.bootDiagnostics.enabled) + | extend bootDiagnosticsStorageAccount = split(split(properties.diagnosticsProfile.bootDiagnostics.storageUri, '/')[2],'.')[0] + | extend powerState = tostring(properties.extended.instanceView.powerState.code) + | extend imagePublisher = iif(isnotempty(properties.storageProfile.imageReference.publisher),tostring(properties.storageProfile.imageReference.publisher),'Custom') + | extend imageOffer = iif(isnotempty(properties.storageProfile.imageReference.offer),tostring(properties.storageProfile.imageReference.offer),tostring(properties.storageProfile.imageReference.id)) + | extend imageSku = tostring(properties.storageProfile.imageReference.sku) + | extend imageVersion = tostring(properties.storageProfile.imageReference.version) + | extend imageExactVersion = tostring(properties.storageProfile.imageReference.exactVersion) + | extend osName = tostring(properties.extended.instanceView.osName) + | extend osVersion = tostring(properties.extended.instanceView.osVersion) + | order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $armVms = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $armVms = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($armVms -and $armVms.GetType().Name -eq "PSResourceGraphResponse") + { + $armVms = $armVms.Data + } + $resultsCount = $armVms.Count + $resultsSoFar += $resultsCount + $armVmsTotal += $armVms + +} while ($resultsCount -eq $ARGPageSize) + +$resultsSoFar = 0 + +<# + Getting all Classic VMs properties with Azure Resource Graph query +#> + +Write-Output "Querying for Classic VM properties" + +$argQuery = @" + resources + | where type =~ 'Microsoft.ClassicCompute/virtualMachines' + | extend dataDiskCount = iif(isnotnull(properties.storageProfile.dataDisks), array_length(properties.storageProfile.dataDisks), 0), nicCount = iif(isnotnull(properties.networkProfile.virtualNetwork.networkInterfaces), array_length(properties.networkProfile.virtualNetwork.networkInterfaces) + 1, 1) + | extend usesManagedDisks = 'false' + | extend availabilitySetId = tostring(properties.hardwareProfile.availabilitySet) + | extend bootDiagnosticsEnabled = tostring(properties.debugProfile.bootDiagnosticsEnabled) + | extend bootDiagnosticsStorageAccount = split(split(properties.debugProfile.serialOutputBlobUri, '/')[2],'.')[0] + | extend powerState = tostring(properties.instanceView.status) + | extend imageOffer = tostring(properties.storageProfile.operatingSystemDisk.sourceImageName) + | order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $classicVms = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $classicVms = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($classicVms -and $classicVms.GetType().Name -eq "PSResourceGraphResponse") + { + $classicVms = $classicVms.Data + } + $resultsCount = $classicVms.Count + $resultsSoFar += $resultsCount + $classicVmsTotal += $classicVms + +} while ($resultsCount -eq $ARGPageSize) + +<# + Merging ARM + Classic VMs, enriching VM size details and building CSV entries +#> + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +Write-Output "Building $($armVmsTotal.Count) ARM VM entries" + +foreach ($vm in $armVmsTotal) +{ + $vmSize = $sizes | Where-Object {$_.name -eq $vm.properties.hardwareProfile.vmSize} + + $avSetId = $null + if ($vm.availabilitySetId) + { + $avSetId = $vm.availabilitySetId.ToLower() + } + + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $vm.tenantId + SubscriptionGuid = $vm.subscriptionId + ResourceGroupName = $vm.resourceGroup.ToLower() + Zones = $vm.zones + Location = $vm.location + VMName = $vm.name.ToLower() + DeploymentModel = 'ARM' + InstanceId = $vm.id.ToLower() + VMSize = $vm.properties.hardwareProfile.vmSize + CoresCount = $vmSize.NumberOfCores + MemoryMB = $vmSize.MemoryInMB + OSType = $vm.properties.storageProfile.osDisk.osType + LicenseType = $vm.properties.licenseType + DataDiskCount = $vm.dataDiskCount + NicCount = $vm.nicCount + UsesManagedDisks = $vm.usesManagedDisks + AvailabilitySetId = $avSetId + BootDiagnosticsEnabled = $vm.bootDiagnosticsEnabled + BootDiagnosticsStorageAccount = $vm.bootDiagnosticsStorageAccount + StatusDate = $statusDate + PowerState = $vm.powerState + ImagePublisher = $vm.imagePublisher + ImageOffer = $vm.imageOffer + ImageSku = $vm.imageSku + ImageVersion = $vm.imageVersion + ImageExactVersion = $vm.imageExactVersion + OSName = $vm.osName + OSVersion = $vm.osVersion + Tags = $vm.tags + } + + $allvms += $logentry +} + +Write-Output "Building $($classicVmsTotal.Count) Classic VM entries" + +foreach ($vm in $classicVmsTotal) +{ + $vmSize = $sizes | Where-Object {$_.name -eq $vm.properties.hardwareProfile.size} + + $avSetId = $null + if ($vm.availabilitySetId) + { + $avSetId = $vm.availabilitySetId.ToLower() + } + + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $vm.tenantId + SubscriptionGuid = $vm.subscriptionId + ResourceGroupName = $vm.resourceGroup.ToLower() + VMName = $vm.name.ToLower() + DeploymentModel = 'Classic' + Location = $vm.location + InstanceId = $vm.id.ToLower() + VMSize = $vm.properties.hardwareProfile.size + CoresCount = $vmSize.NumberOfCores + MemoryMB = $vmSize.MemoryInMB + OSType = $vm.properties.storageProfile.operatingSystemDisk.operatingSystem + LicenseType = "N/A" + DataDiskCount = $vm.dataDiskCount + NicCount = $vm.nicCount + UsesManagedDisks = $vm.usesManagedDisks + AvailabilitySetId = $avSetId + BootDiagnosticsEnabled = $vm.bootDiagnosticsEnabled + BootDiagnosticsStorageAccount = $vm.bootDiagnosticsStorageAccount + PowerState = $vm.powerState + StatusDate = $statusDate + ImagePublisher = $vm.imagePublisher + ImageOffer = $vm.imageOffer + ImageSku = $vm.imageSku + ImageVersion = $vm.imageVersion + ImageExactVersion = $vm.imageExactVersion + OSName = $vm.osName + OSVersion = $vm.osVersion + Tags = $null + } + + $allvms += $logentry +} + +<# + Actually exporting CSV to Azure Storage +#> + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-vms-$subscriptionSuffix.csv" + +$allvms | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-AdvisorRecommendationsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-AdvisorRecommendationsToBlobStorage.ps1 new file mode 100644 index 000000000..cc82c3e90 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-AdvisorRecommendationsToBlobStorage.ps1 @@ -0,0 +1,247 @@ +param( + [Parameter(Mandatory = $false)] + [string] $targetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_AdvisorContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "advisorexports" +} + +$CategoryFilter = Get-AutomationVariable -Name "AzureOptimization_AdvisorFilter" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($CategoryFilter)) +{ + $CategoryFilter = "HighAvailability,Security,Performance,OperationalExcellence" # comma-separated list of categories +} +$CategoryFilter += ",Cost" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudEnvironment = $externalCloudEnvironment +} + +Write-Output "Getting subscriptions target $TargetSubscription" + +$tenantId = (Get-AzContext).Tenant.Id + +$ARGPageSize = 1000 + +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $scope = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" -and $_.SubscriptionPolicies.QuotaId -notlike "AAD*" } | ForEach-Object { "$($_.Id)"} + $scope = $tenantId +} + + +<# + Getting Advisor recommendations for each subscription and building CSV entries +#> + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +$recommendationsARG = @() + +$resultsSoFar = 0 + +$FinalCategoryFilter = "" + +if (-not([string]::IsNullOrEmpty($CategoryFilter))) +{ + $categories = $CategoryFilter.Split(',') + for ($i = 0; $i -lt $categories.Count; $i++) + { + $categories[$i] = "'" + $categories[$i] + "'" + } + $FinalCategoryFilter = " and properties.category in (" + ($categories -join ",") + ")" +} + +$argQuery = @" +advisorresources +| where type == 'microsoft.advisor/recommendations' +| where isnull(properties.suppressionIds)$FinalCategoryFilter +| extend resourceId = tostring(split(tolower(id),'/providers/microsoft.advisor')[0]) +| join kind=leftouter (resources | project resourceId=tolower(id), resourceTags=tags) on resourceId +| project id, category = properties.category, impact = properties.impact, impactedArea = properties.impactedField, + description = properties.shortDescription.problem, recommendationText = properties.shortDescription.solution, + recommendationTypeId = properties.recommendationTypeId, instanceName = properties.impactedValue, + additionalInfo = properties.extendedProperties, tags=resourceTags +| order by id asc +"@ + +do +{ + if ($resultsSoFar -eq 0) + { + $recs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $recs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($recs -and $recs.GetType().Name -eq "PSResourceGraphResponse") + { + $recs = $recs.Data + } + $resultsCount = $recs.Count + $resultsSoFar += $resultsCount + $recommendationsARG += $recs + +} while ($resultsCount -eq $ARGPageSize) + +Write-Output "Building $($recommendationsARG.Count) recommendations entries" + +$recommendations = @() + +foreach ($advisorRecommendation in $recommendationsARG) +{ + $resourceIdParts = $advisorRecommendation.id.Split('/') + if ($resourceIdParts.Count -ge 9) + { + # if the Resource ID is made of 9 parts, then the recommendation is relative to a specific Azure resource + $realResourceIdParts = $resourceIdParts[0..8] + $instanceId = ($realResourceIdParts -join "/").ToLower() + $resourceGroup = $realResourceIdParts[4].ToLower() + $subscriptionId = $realResourceIdParts[2] + } + else + { + # otherwise it is not a resource-specific recommendation (e.g., reservations) + $resourceGroup = "notavailable" + $instanceId = $advisorRecommendation.id.ToLower() + $subscriptionId = $resourceIdParts[2] + } + + if (-not([string]::IsNullOrEmpty($advisorRecommendation.additionalInfo))) + { + $additionalInfo = $advisorRecommendation.additionalInfo | ConvertTo-Json -Compress + } + else + { + $additionalInfo = $null + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + Category = $advisorRecommendation.category + Impact = $advisorRecommendation.impact + ImpactedArea = $advisorRecommendation.impactedArea + Description = $advisorRecommendation.description + RecommendationText = $advisorRecommendation.recommendationText + RecommendationTypeId = $advisorRecommendation.recommendationTypeId + InstanceId = $instanceId + InstanceName = $advisorRecommendation.instanceName + Tags = $advisorRecommendation.tags + AdditionalInfo = $additionalInfo + ResourceGroup = $resourceGroup + SubscriptionGuid = $subscriptionId + TenantGuid = $tenantId + } + + $recommendations += $recommendation +} + +Write-Output "Found $($recommendations.Count) ($CategoryFilter) recommendations..." + +$fileDate = $datetime.ToString("yyyyMMdd") +$advisorFilter = $CategoryFilter.Replace(',','').ToLower() +$csvExportPath = "$fileDate-$advisorFilter-$scope.csv" + +$recommendations | Export-Csv -NoTypeInformation -Path $csvExportPath +Write-Output "Export to $csvExportPath" + +$csvBlobName = $csvExportPath +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." + +Write-Output "DONE!" \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1 new file mode 100644 index 000000000..e31dd034f --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1 @@ -0,0 +1,296 @@ +Param ( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $true)] + [string] $ResourceType, # ARM resource type + + [Parameter(Mandatory = $false)] + [string] $ARGFilter, # e.g., name != 'master' and sku.tier in ('Basic','Standard','Premium') + + [Parameter(Mandatory = $true)] + [string] $MetricNames, # comma-separated metrics names (use Get-AzMetricDefinition for a list of supported metric names for a given resource) + + [Parameter(Mandatory = $true)] + [ValidateSet("Maximum", "Minimum", "Average", "Total")] + [string] $AggregationType, + + [Parameter(Mandatory = $false)] + [ValidateSet("Default", "Maximum", "Minimum", "Average", "Total")] + [string] $AggregationOfType = "Default", + + [Parameter(Mandatory = $true)] + [string] $TimeSpan, # [d.]hh:mm:ss + + [Parameter(Mandatory = $true)] + [string] $TimeGrain, # [d.]hh:mm:ss (00:01:00, 00:05:00, 00:15:00, 00:30:00, 01:00:00, 06:00:00, 12:00:00, 1.00:00:00, 7.00:00:00, 30.00:00:00) + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_AzMonitorContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "azmonitorexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +if (-not([string]::IsNullOrEmpty($TargetSubscription))) { + $subscriptions = $TargetSubscription + $subscriptionSuffix = "-" + $TargetSubscription +} +else { + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +[TimeSpan]::Parse($TimeGrain) | Out-Null +$TimeSpanObj = [TimeSpan]::Parse("-$TimeSpan") + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Querying for $ResourceType with page size $ARGPageSize and target subscription $TargetSubscription..." + +$allResources = @() + +$resultsSoFar = 0 + +$argWhere = "" +if (-not([string]::IsNullOrEmpty($ARGFilter))) +{ + $argWhere = " and $ARGFilter" +} + +$argQuery = @" +resources +| where type =~ '$ResourceType'$argWhere +| project id, name, subscriptionId, resourceGroup, tenantId +| order by id asc +"@ + +do { + if ($resultsSoFar -eq 0) { + $resources = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else { + $resources = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($resources -and $resources.GetType().Name -eq "PSResourceGraphResponse") + { + $resources = $resources.Data + } + $resultsCount = $resources.Count + $resultsSoFar += $resultsCount + $allResources += $resources + +} while ($resultsCount -eq $ARGPageSize) + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Found $($allResources.Count) resources." + +$metrics = $MetricNames.Split(',') + +$queryDate = Get-Date +$utcNow = $queryDate.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +$utcAgo = $queryDate.Add($TimeSpanObj).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + +$customMetrics = @() + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Analyzing resources for $MetricNames metrics ($AggregationType with $TimeGrain time grain) since $utcAgo..." + +foreach ($resource in $allResources) { + $valuesAggregation = @() + $foundResource = $true + foreach ($metric in $metrics) { + $metricValues = Get-AzMetric -ResourceId $resource.id -MetricName $metric -TimeGrain $TimeGrain -AggregationType $AggregationType ` + -StartTime $utcAgo -EndTime $utcNow -WarningAction SilentlyContinue -ErrorAction Continue + if ($metricValues.Data) { + if ($valuesAggregation.Count -eq 0) { + $valuesAggregation = $metricValues.Data."$AggregationType" + } + else { + for ($i = 0; $i -lt $valuesAggregation.Count; $i++) { + if ($metricValues.Data.Count -gt 1) + { + $valuesAggregation[$i] += $metricValues.Data[$i]."$AggregationType" + } + else + { + $valuesAggregation += $metricValues.Data."$AggregationType" + } + } + } + } + + if (-not($metricValues.Id)) + { + $foundResource = $false + } + } + + if ($foundResource) + { + $aggregatedValue = $null + $finalAggregationType = $AggregationType + if ($AggregationOfType -ne "Default") + { + $finalAggregationType = $AggregationOfType + } + if ($valuesAggregation.Count -gt 0) { + switch ($finalAggregationType) { + "Maximum" { + $aggregatedValue = ($valuesAggregation | Measure-Object -Maximum).Maximum + } + "Minimum" { + $aggregatedValue = ($valuesAggregation | Measure-Object -Minimum).Minimum + } + "Average" { + $aggregatedValue = ($valuesAggregation | Measure-Object -Average).Average + } + "Total" { + $aggregatedValue = ($valuesAggregation | Measure-Object -Sum).Sum + } + } + } + + $customMetric = New-Object PSObject -Property @{ + Timestamp = $utcNow + Cloud = $cloudEnvironment + TenantGuid = $resource.tenantId + SubscriptionGuid = $resource.subscriptionId + ResourceGroupName = $resource.resourceGroup.ToLower() + ResourceName = $resource.name.ToLower() + ResourceId = $resource.id.ToLower() + MetricNames = $MetricNames + AggregationType = $AggregationType + AggregationOfType = $AggregationOfType + MetricValue = $aggregatedValue + TimeGrain = $TimeGrain + TimeSpan = $TimeSpan + } + + $customMetrics += $customMetric + } +} + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Found $($customMetrics.Count) resources to collect metrics from..." + +$metricMoment = $queryDate.Add($TimeSpanObj).ToUniversalTime().ToString("yyyyMMddHHmmss") +$ResourceTypeName = $ResourceType.Split('/')[1].ToLower() +$MetricName = $MetricNames.Replace(',','').Replace(' ','').Replace('/','').ToLower() +$AggregationOfTypeName = "" +if ($AggregationOfType -ne "Default") +{ + $AggregationOfTypeName = ("-$AggregationOfType").ToLower() +} +$AggregationTypeName = "$($AggregationType.ToLower())$AggregationOfTypeName" +$csvExportPath = "$metricMoment-metrics-$ResourceTypeName-$MetricName-$AggregationTypeName-$subscriptionSuffix.csv" + +$ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name) +if ($ci.NumberFormat.NumberDecimalSeparator -ne '.') +{ + Write-Output "Current culture ($($ci.Name)) does not use . as decimal separator" + $ci.NumberFormat.NumberDecimalSeparator = '.' + [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci +} + +$customMetrics | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." + diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1 new file mode 100644 index 000000000..eac393878 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1 @@ -0,0 +1,790 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName, + + [Parameter(Mandatory = $false)] + [string] $targetStartDate, # YYYY-MM-DD format + + [Parameter(Mandatory = $false)] + [string] $targetEndDate # YYYY-MM-DD format +) + +$ErrorActionPreference = "Stop" +$global:hadErrors = $false +$global:scopesWithErrors = @() + +function Authenticate-AzureWithOption { + param ( + [string] $authOption = "ManagedIdentity", + [string] $cloudEnv = "AzureCloud", + [string] $clientID + ) + + switch ($authOption) { + "UserAssignedManagedIdentity" { + Connect-AzAccount -Identity -EnvironmentName $cloudEnv -AccountId $clientID + break + } + Default { #ManagedIdentity + Connect-AzAccount -Identity -EnvironmentName $cloudEnv + break + } + } +} + +function Generate-CostDetails { + param ( + [string] $ScopeId, + [string] $ScopeName + ) + + $MaxTries = 20 # The typical Retry-After is set to 20 seconds. We'll give ~6 minutes overall to download the cost details report + $hadErrors = $false + + $CostDetailsApiPath = "$ScopeId/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2022-05-01" + $body = "{ `"metric`": `"$consumptionMetric`", `"timePeriod`": { `"start`": `"$targetStartDate`", `"end`": `"$targetEndDate`" } }" + $result = Invoke-AzRestMethod -Path $CostDetailsApiPath -Method POST -Payload $body + $requestResultPath = $result.Headers.Location.PathAndQuery + if ($result.StatusCode -in (200,202)) + { + $tries = 0 + $requestSuccess = $false + + Write-Output "Obtained cost detail results endpoint: $requestResultPath..." + + Write-Output "Was told to wait $($result.Headers.RetryAfter.Delta.TotalSeconds) seconds." + + $sleepSeconds = 60 + if ($result.Headers.RetryAfter.Delta.TotalSeconds -gt 0) + { + $sleepSeconds = $result.Headers.RetryAfter.Delta.TotalSeconds + } + + do + { + $tries++ + Write-Output "Checking whether export is ready (try $tries)..." + + Start-Sleep -Seconds $sleepSeconds + $downloadResult = Invoke-AzRestMethod -Method GET -Path $requestResultPath + + if ($downloadResult.StatusCode -eq 200) + { + + Write-Output "Export is ready. Proceeding with CSV download..." + + $downloadBlobJson = $downloadResult.Content | ConvertFrom-Json + + $blobCounter = 0 + foreach ($blob in $downloadBlobJson.manifest.blobs) + { + $blobCounter++ + + Write-Output "Downloading blob $blobCounter..." + + $csvExportPath = "$env:TEMP\$targetStartDate-$ScopeName-$consumptionMetric-$blobCounter.csv" + $finalCsvExportPath = "$env:TEMP\$targetStartDate-$ScopeName-$consumptionMetric-$blobCounter-final.csv" + + Invoke-WebRequest -Uri $blob.blobLink -OutFile $csvExportPath + + Write-Output "Blob downloaded to $csvExportPath successfully." + + $r = [IO.File]::OpenText($csvExportPath) + $w = [System.IO.StreamWriter]::new($finalCsvExportPath) + + # header normalization between MCA and EA + $headerConversion = @{ + additionalInfo = "AdditionalInfo"; + billingAccountId = "BillingAccountId"; + billingAccountName = "BillingAccountName"; + billingCurrency = "BillingCurrencyCode"; + billingPeriodEndDate = "BillingPeriodEndDate"; + billingPeriodStartDate = "BillingPeriodStartDate"; + billingProfileId = "BillingProfileId"; + billingProfileName = "BillingProfileName"; + chargeType = "ChargeType"; + consumedService = "ConsumedService"; + costAllocationRuleName = "CostAllocationRuleName"; + costCenter = "CostCenter"; + costInBillingCurrency = "CostInBillingCurrency"; + date = "Date"; + effectivePrice = "EffectivePrice"; + frequency = "Frequency"; + invoiceSectionId = "InvoiceSectionId"; + invoiceSectionName = "InvoiceSectionName"; + isAzureCreditEligible = "IsAzureCreditEligible"; + meterCategory = "MeterCategory"; + meterId = "MeterId"; + meterName = "MeterName"; + meterRegion = "MeterRegion"; + meterSubCategory = "MeterSubCategory"; + offerId = "OfferId"; + pricingModel = "PricingModel"; + productOrderId = "ProductOrderId"; + productOrderName = "ProductOrderName"; + publisherName = "PublisherName"; + publisherType = "PublisherType"; + quantity = "Quantity"; + reservationId = "ReservationId"; + reservationName = "ReservationName"; + resourceGroupName = "ResourceGroup"; + resourceLocation = "ResourceLocation"; + serviceFamily = "ServiceFamily"; + serviceInfo1 = "ServiceInfo1"; + serviceInfo2 = "ServiceInfo2"; + subscriptionName = "SubscriptionName"; + tags = "Tags"; + term = "Term"; + unitOfMeasure = "UnitOfMeasure"; + unitPrice = "UnitPrice" + } + + $lineCounter = 0 + while ($r.Peek() -ge 0) { + $line = $r.ReadLine() + $lineCounter++ + if ($lineCounter -eq 1) + { + $headers = $line.Split(",") + + for ($i = 0; $i -lt $headers.Length; $i++) + { + $header = $headers[$i] + if ($headerConversion.ContainsKey($header)) + { + $headers[$i] = $headerConversion[$header] + } + } + + $line = $headers -join "," + + $w.WriteLine($line) + } + else + { + $w.WriteLine($line) + } + } + $r.Dispose() + $w.Close() + + $csvBlobName = [System.IO.Path]::GetFileName($finalCsvExportPath) + $csvProperties = @{"ContentType" = "text/csv"}; + Set-AzStorageBlobContent -File $finalCsvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + + Remove-Item -Path $csvExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "[$now] Removed $csvExportPath from local disk..." + + Remove-Item -Path $finalCsvExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "[$now] Removed $finalCsvExportPath from local disk..." + } + + $requestSuccess = $true + } + elseif ($downloadResult.StatusCode -eq 202) + { + Write-Output "Was told to wait a bit more... $($downloadResult.Headers.RetryAfter.Delta.TotalSeconds) seconds." + + $sleepSeconds = 60 + if ($downloadResult.Headers.RetryAfter.Delta.TotalSeconds -gt 0) + { + $sleepSeconds = $downloadResult.Headers.RetryAfter.Delta.TotalSeconds + } + } + elseif ($downloadResult.StatusCode -eq 401) + { + Write-Output "Had an authentication issue. Will login again and sleep just a couple of seconds." + + if ($authenticationOption -eq "UserAssignedManagedIdentity") + { + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID + } + else + { + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment + } + + $sleepSeconds = 2 + } + else + { + $global:hadErrors = $true + $global:scopesWithErrors += $ScopeName + Write-Warning "Got an unexpected response code: $($downloadResult.StatusCode)" + } + } + while (-not($requestSuccess) -and $tries -lt $MaxTries) + + if (-not($requestSuccess)) + { + $global:hadErrors = $true + $global:scopesWithErrors += $ScopeName + if ($tries -eq $MaxTries) + { + Write-Warning "Reached maximum number of tries. Aborting..." + } + else + { + Write-Warning "Error returned by the Download Cost Details API. Status Code: $($downloadResult.StatusCode). Message: $($downloadResult.Content)" + } + } + else + { + Write-Output "Export download processing complete." + } + } + else + { + if ($result.StatusCode -ne 204) + { + $global:hadErrors = $true + $global:scopesWithErrors += $ScopeName + Write-Warning "Error returned by the Generate Cost Details API. Status Code: $($result.StatusCode). Message: $($result.Content)" + } + else + { + Write-Output "Request returned 204 No Content" + } + } +} + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ConsumptionContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "consumptionexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") + +$consumptionMetric = Get-AutomationVariable -Name "AzureOptimization_ConsumptionMetric" -ErrorAction SilentlyContinue # AmortizedCost|ActualCost +if ([string]::IsNullOrEmpty($consumptionMetric)) +{ + $consumptionMetric = "AmortizedCost" +} + +$consumptionAPIOption = Get-AutomationVariable -Name "AzureOptimization_ConsumptionAPIOption" -ErrorAction SilentlyContinue # CostDetails|UsageDetails +if ([string]::IsNullOrEmpty($consumptionAPIOption)) +{ + $consumptionAPIOption = "CostDetails" +} + +$consumptionScope = Get-AutomationVariable -Name "AzureOptimization_ConsumptionScope" -ErrorAction SilentlyContinue # Subscription|BillingAccount +if ([string]::IsNullOrEmpty($consumptionScope)) +{ + "Consumption Scope not specified, defaulting to Subscription" + $consumptionScope = "Subscription" +} +else +{ + "Consumption Scope is $consumptionScope" + if ($consumptionScope -eq "BillingAccount") + { + $BillingAccountID = Get-AutomationVariable -Name "AzureOptimization_BillingAccountID" + } + else + { + if ($consumptionScope -ne "Subscription") + { + throw "Invalid value for AzureOptimization_ConsumptionScope. Valid values are 'Subscription' or 'BillingAccount'." + } + } +} + +if ($cloudEnvironment -eq "AzureChinaCloud") +{ + $chinaEAEnrollment = Get-AutomationVariable -Name "AzureOptimization_AzureChinaEAEnrollment" -ErrorAction SilentlyContinue + $chinaEAKey = Get-AutomationVariable -Name "AzureOptimization_AzureChinaEAKey" -ErrorAction SilentlyContinue +} + +"Logging in to Azure with $authenticationOption..." + +if ($authenticationOption -eq "UserAssignedManagedIdentity") +{ + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID +} +else +{ + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudEnvironment = $externalCloudEnvironment +} + +# compute start+end dates + +if ([string]::IsNullOrEmpty($targetStartDate) -or [string]::IsNullOrEmpty($targetEndDate)) +{ + $targetStartDate = (Get-Date).Date.AddDays($consumptionOffsetDays * -1).ToString("yyyy-MM-dd") + $targetEndDate = $targetStartDate +} + +if ($consumptionScope -eq "Subscription") +{ + if (-not([string]::IsNullOrEmpty($TargetSubscription))) + { + $subscriptions = Get-AzSubscription -SubscriptionId $TargetSubscription + } + else + { + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } + } + "Exporting consumption data from $targetStartDate to $targetEndDate for $($subscriptions.Count) subscriptions..." +} +else +{ + "Exporting consumption data from $targetStartDate to $targetEndDate for Billing Account ID $BillingAccountID..." +} + + +# for each subscription, get billing data + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +if ($cloudEnvironment -eq "AzureChinaCloud" -and -not([string]::IsNullOrEmpty($chinaEAEnrollment)) -and -not([string]::IsNullOrEmpty($chinaEAKey))) +{ + $targetMonth = $targetStartDate.Substring(0,7) + $consumption = $null + $billingEntries = @() + + $BillingApiUri = "https://ea.azure.cn/rest/$chinaEAEnrollment/usage-report?month=$targetMonth&type=detail&fmt=Csv" + $PricesheetApiUri = "https://ea.azure.cn/rest/$chinaEAEnrollment/usage-report?month=$targetMonth&type=pricesheet&fmt=Csv" + + $Headers = @{} + $Headers.Add("Authorization","Bearer $chinaEAKey") + + Write-Output "Getting pricesheet for month $targetMonth (EA enrollment $chinaEAEnrollment)..." + + Invoke-RestMethod -Method Get -Uri $PricesheetApiUri -Headers $Headers -OutFile "pricesheet-$targetMonth.csv" + + Write-Output "Pricesheet data exported to disk as CSV." + + $csvFile = Get-Content -Path "pricesheet-$targetMonth.csv" + + Write-Output "Pricesheet data imported from disk as string." + + Remove-Item -Path "pricesheet-$targetMonth.csv" -Force + + Write-Output "Removed pricesheet-$targetMonth.csv from local disk..." + + $csvFile2 = $csvFile[2..($csvFile.Count-1)] + $headerLine = $csvFile2[0] + $columnHeaders = $headerLine.Split(",") + for ($i = 0; $i -lt $columnHeaders.Count; $i++) + { + if($columnHeaders[$i] -match '.+\((?.+)\)') + { + $columnHeaders[$i] = $Matches.ColumnName + } + } + $csvFile2[0] = $columnHeaders -join "," + + Write-Output "Removed first 2 lines and replaced header." + + $pricesheet = $csvFile2 | ConvertFrom-Csv + + Write-Output "Starting Azure China billing export process from $targetStartDate to $targetEndDate (month $targetMonth) for EA enrollment $chinaEAEnrollment..." + + $tries = 0 + $requestSuccess = $false + do + { + try { + $tries++ + Invoke-RestMethod -Method Get -Uri $BillingApiUri -Headers $Headers -OutFile "usagedetails-$targetStartDate.csv" + + Write-Output "Consumption data exported to disk as CSV." + + $csvFile = Get-Content -Path "usagedetails-$targetStartDate.csv" + + Write-Output "Consumption data imported from disk as string." + + Remove-Item -Path "usagedetails-$targetStartDate.csv" -Force + + Write-Output "Removed usagedetails-$targetStartDate.csv from local disk..." + + $csvFile2 = $csvFile[2..($csvFile.Count-1)] + $headerLine = $csvFile2[0] + $columnHeaders = $headerLine.Split(",") + for ($i = 0; $i -lt $columnHeaders.Count; $i++) + { + if($columnHeaders[$i] -match '.+\((?.+)\)') + { + $columnHeaders[$i] = $Matches.ColumnName + } + } + $csvFile2[0] = $columnHeaders -join "," + + Write-Output "Removed first 2 lines and replaced header." + + $consumption = $csvFile2 | ConvertFrom-Csv + $requestSuccess = $true + } + catch { + $ErrorMessage = $_.Exception.Message + Write-Warning "Error getting consumption data: $ErrorMessage. $tries of 3 tries. Waiting 60 seconds..." + Start-Sleep -s 60 + } + + } while ( -not($requestSuccess) -and $tries -lt 3 ) + + if (-not($requestSuccess)) + { + throw "Failed consumption export" + } + + Write-Output "Consumption data in memory as CSV. Processing lines..." + + foreach ($consumptionLine in $consumption) + { + $usageDate = [Datetime]::ParseExact($consumptionLine.Date, 'MM/dd/yyyy', $null).ToString("yyyy-MM-dd") + + if ($usageDate -ge $targetStartDate -and $usageDate -le $targetEndDate -and ($subscriptions.Count -gt 1 -or $subscriptions.Id -eq $consumptionLine.SubscriptionGuid)) + { + $instanceId = $null + $instanceName = $null + if ($null -ne $consumptionLine.'Instance ID') + { + $instanceId = $consumptionLine.'Instance ID'.ToLower() + $idParts = $consumptionLine.'Instance ID'.Split("/") + $instanceName = $idParts[$idParts.Count-1].ToLower() + } + + $rgName = $null + if ($null -ne $consumptionLine.'Resource Group') + { + $rgName = $consumptionLine.'Resource Group'.ToLower() + } + + $convertedCost = 0.0 + if ([double]$consumptionLine.ExtendedCost -ne 0) + { + $convertedCost = [double]$consumptionLine.ExtendedCost + } + $convertedPrice = 0.0 + if ([double]$consumptionLine.ResourceRate -ne 0) + { + $convertedPrice = [double]$consumptionLine.ResourceRate + } + + $unitPrice = 0.0 + $partNumber = "N/A" + foreach ($priceItem in $pricesheet) + { + if ($priceItem.Service -eq $consumptionLine.Product) + { + $partNumber = $priceItem.'Part Number' + if ($consumptionLine.'Meter Category' -eq "Virtual Machines") + { + $tempUnitPrice = [double] $priceItem.'Unit Price' + $uom = $priceItem.'Unit of Measure' + $currentUnitHours = [int] (Select-String -InputObject $uom -Pattern "^\d+").Matches[0].Value + if ($currentUnitHours -gt 0) + { + $unitPrice = [double] ($tempUnitPrice / $currentUnitHours) + } + } + else + { + $unitPrice = $convertedPrice + } + break + } + } + + $billingEntry = New-Object PSObject -Property @{ + Timestamp = $timestamp + SubscriptionId = $consumptionLine.SubscriptionGuid + ResourceGroup = $rgName + ResourceName = $instanceName + ResourceId = $instanceId + Date = $consumptionLine.Date + Tags = $consumptionLine.Tags + AdditionalInfo = $consumptionLine.AdditionalInfo + BillingCurrencyCode = "CNY" + ChargeType = "Usage" + ConsumedService = $consumptionLine.'Consumed Service' + CostInBillingCurrency = $convertedCost + EffectivePrice = $convertedPrice + Frequency = "UsageBased" + MeterCategory = $consumptionLine.'Meter Category' + MeterId = $consumptionLine.'Meter ID' + MeterName = $consumptionLine.'Meter Name' + MeterSubCategory = $consumptionLine.'Meter Sub-Category' + PartNumber = $partNumber + ProductName = $consumptionLine.Product + Quantity = $consumptionLine.'Consumed Quantity' + UnitOfMeasure = $consumptionLine.'Unit of Measure' + UnitPrice = $unitPrice + ResourceLocation = $consumptionLine.'Resource Location' + AccountOwnerId = $consumptionLine.AccountOwnerId + } + + $billingEntries += $billingEntry + } + } + + if ($targetStartDate -ne $targetEndDate) + { + $targetStartDate = "$targetStartDate-$targetEndDate" + } + + $csvExportPath = "$targetStartDate-eachina.csv" + + $billingEntries | Export-Csv -Path $csvExportPath -NoTypeInformation + + Write-Output "Exported $($billingEntries.Count) entries as CSV to $csvExportPath" + + $csvBlobName = $csvExportPath + $csvProperties = @{"ContentType" = "text/csv"}; + Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + + Write-Output "Uploaded to blob storage!" + + Remove-Item -Path $csvExportPath -Force + + Write-Output "Removed $csvExportPath from local disk..." +} +else +{ + if ($consumptionScope -eq "Subscription") + { + $CostDetailsSupportedQuotaIDs = @('EnterpriseAgreement_2014-09-01','Internal_2014-09-01','CSP_2015-05-01') + $ConsumptionSupportedQuotaIDs = @('PayAsYouGo_2014-09-01','MSDN_2014-09-01') + + foreach ($subscription in $subscriptions) + { + $subscriptionQuotaID = $subscription.SubscriptionPolicies.QuotaId + + if ($subscriptionQuotaID -in $ConsumptionSupportedQuotaIDs -or $consumptionAPIOption -eq "UsageDetails") + { + $consumption = $null + $billingEntries = @() + + $ConsumptionApiPath = "/subscriptions/$($subscription.Id)/providers/Microsoft.Consumption/usageDetails?api-version=2021-10-01&metric=$($consumptionMetric.ToLower())&%24expand=properties%2FmeterDetails%2Cproperties%2FadditionalInfo&%24filter=properties%2FusageStart%20ge%20%27$targetStartDate%27%20and%20properties%2FusageEnd%20le%20%27$targetEndDate%27" + + "Starting consumption export process from $targetStartDate to $targetEndDate for subscription $($subscription.Name)..." + + do + { + if (-not([string]::IsNullOrEmpty($consumption.nextLink))) + { + $ConsumptionApiPath = $consumption.nextLink.Substring($consumption.nextLink.IndexOf("/subscriptions/")) + } + $tries = 0 + $requestSuccess = $false + do + { + try { + $tries++ + $consumption = (Invoke-AzRestMethod -Path $ConsumptionApiPath -Method GET).Content | ConvertFrom-Json + $requestSuccess = $true + } + catch { + $ErrorMessage = $_.Exception.Message + Write-Warning "Error getting consumption data: $ErrorMessage. $tries of 3 tries. Waiting 60 seconds..." + Start-Sleep -s 60 + } + } while ( -not($requestSuccess) -and $tries -lt 3 ) + + foreach ($consumptionLine in $consumption.value) + { + if ((Get-Date $consumptionLine.properties.date).ToString("yyyy-MM-dd") -ge $targetStartDate -and (Get-Date $consumptionLine.properties.date).ToString("yyyy-MM-dd") -le $targetEndDate) + { + if ($consumptionLine.tags) + { + $tags = $consumptionLine.tags | ConvertTo-Json -Compress + } + else + { + $tags = $null + } + + $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 + } + $billingEntries += $billingEntry + } + } + } + while ($requestSuccess -and -not([string]::IsNullOrEmpty($consumption.nextLink))) + + if ($requestSuccess) + { + "Generated $($billingEntries.Count) entries..." + + "Uploading CSV to Storage" + + $ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name) + if ($ci.NumberFormat.NumberDecimalSeparator -ne '.') + { + "Current culture ($($ci.Name)) does not use . as decimal separator" + $ci.NumberFormat.NumberDecimalSeparator = '.' + [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci + } + + $csvExportPath = "$targetStartDate-$($subscription.Id)-$consumptionMetric.csv" + + $billingEntries | Export-Csv -Path $csvExportPath -NoTypeInformation + + $csvBlobName = $csvExportPath + $csvProperties = @{"ContentType" = "text/csv"}; + Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Uploaded $csvBlobName to Blob Storage..." + + Remove-Item -Path $csvExportPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + "[$now] Removed $csvExportPath from local disk..." + } + else + { + $global:hadErrors = $true + $global:scopesWithErrors += $ScopeName + Write-Warning "Failed to get consumption data for subscription $($subscription.Name)..." + } + } + elseif ($subscriptionQuotaID -in $CostDetailsSupportedQuotaIDs -or $consumptionAPIOption -eq "CostDetails") + { + "Starting cost details export process from $targetStartDate to $targetEndDate for subscription $($subscription.Name)..." + Generate-CostDetails -ScopeId "/subscriptions/$($subscription.Id)" -ScopeName $subscription.Id + } + else + { + $global:hadErrors = $true + $global:scopesWithErrors += $ScopeName + Write-Warning "Subscription quota $subscriptionQuotaID not supported" + } + } + } + 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 ($global:hadErrors) +{ + $scopesWithErrorsString = $global:scopesWithErrors -join "," + throw "There were errors during the export process with the following scopes: $scopesWithErrorsString. Please check the output for details." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-PolicyComplianceToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-PolicyComplianceToBlobStorage.ps1 new file mode 100644 index 000000000..d287e7b6b --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-PolicyComplianceToBlobStorage.ps1 @@ -0,0 +1,644 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetSubscription, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName, + + [Parameter(Mandatory = $false)] + [ValidateSet("ARG", "ARM")] + [string] $PolicyStatesEndpoint = "ARG" +) + +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" -ErrorAction SilentlyContinue # e.g., westeurope +if ([string]::IsNullOrEmpty($referenceRegion)) +{ + $referenceRegion = "westeurope" +} +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_PolicyStatesContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "policystateexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$ARGPageSize = 1000 + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +$cloudSuffix = "" + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudSuffix = $externalCloudEnvironment.ToLower() + "-" + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +$allpolicyStates = @() + +Write-Output "Getting subscriptions target $TargetSubscription" +if (-not([string]::IsNullOrEmpty($TargetSubscription))) +{ + $subscriptions = $TargetSubscription + $subscriptionSuffix = $TargetSubscription +} +else +{ + $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" -and $_.SubscriptionPolicies.QuotaId -notlike "AAD*" } | ForEach-Object { "$($_.Id)"} + $subscriptionSuffix = $cloudSuffix + "all-" + $tenantId +} + +Write-Output "Building Policy display names..." + +$policyAssignments = @{} +$policyInitiatives = @{} +$policyDefinitions = @{} +$excludedAssignmentScopes = @() +$allInitiatives = @() + +if ($PolicyStatesEndpoint -eq "ARG") +{ + $resultsSoFar = 0 + + $argQuery = @" + policyresources + | where type =~ 'microsoft.authorization/policyassignments' + | extend displayName = iif(isnotempty(properties.displayName), tostring(properties.displayName), 'N/A') + | distinct id, displayName + | order by id asc +"@ + + $argAssignmentsTotal = @() + + do + { + if ($resultsSoFar -eq 0) + { + $argAssignments = Search-AzGraph -Query $argQuery -First $ARGPageSize -UseTenantScope + } + else + { + $argAssignments = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -UseTenantScope + } + if ($argAssignments -and $argAssignments.GetType().Name -eq "PSResourceGraphResponse") + { + $argAssignments = $argAssignments.Data + } + $resultsCount = $argAssignments.Count + $resultsSoFar += $resultsCount + $argAssignmentsTotal += $argAssignments + + } while ($resultsCount -eq $ARGPageSize) + + Write-Output "Building $($argAssignmentsTotal.Count) assignment entries" + + foreach ($assignment in $argAssignmentsTotal) + { + $policyAssignments.Add($assignment.id, $assignment.displayName) + } + + $resultsSoFar = 0 + + $argQuery = @" + policyresources + | where type =~ 'microsoft.authorization/policysetdefinitions' + | extend displayName = iif(isnotempty(properties.displayName), tostring(properties.displayName), 'N/A') + | distinct id, displayName + | order by id asc +"@ + + $argInitiativesTotal = @() + + do + { + if ($resultsSoFar -eq 0) + { + $argInitiatives = Search-AzGraph -Query $argQuery -First $ARGPageSize -UseTenantScope + } + else + { + $argInitiatives = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -UseTenantScope + } + if ($argInitiatives -and $argInitiatives.GetType().Name -eq "PSResourceGraphResponse") + { + $argInitiatives = $argInitiatives.Data + } + $resultsCount = $argInitiatives.Count + $resultsSoFar += $resultsCount + $argInitiativesTotal += $argInitiatives + + } while ($resultsCount -eq $ARGPageSize) + + Write-Output "Building $($argInitiativesTotal.Count) initiative entries" + + foreach ($initiative in $argInitiativesTotal) + { + $policyInitiatives.Add($initiative.id, $initiative.displayName) + } + + $resultsSoFar = 0 + + $argQuery = @" + policyresources + | where type =~ 'microsoft.authorization/policydefinitions' + | extend displayName = iif(isnotempty(properties.displayName), tostring(properties.displayName), 'N/A') + | distinct id, displayName + | order by id asc +"@ + + $argDefinitionsTotal = @() + + do + { + if ($resultsSoFar -eq 0) + { + $argDefinitions = Search-AzGraph -Query $argQuery -First $ARGPageSize -UseTenantScope + } + else + { + $argDefinitions = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -UseTenantScope + } + if ($argDefinitions -and $argDefinitions.GetType().Name -eq "PSResourceGraphResponse") + { + $argDefinitions = $argDefinitions.Data + } + $resultsCount = $argDefinitions.Count + $resultsSoFar += $resultsCount + $argDefinitionsTotal += $argDefinitions + + } while ($resultsCount -eq $ARGPageSize) + + Write-Output "Building $($argDefinitionsTotal.Count) definition entries" + + foreach ($definition in $argDefinitionsTotal) + { + $policyDefinitions.Add($definition.id, $definition.displayName) + } +} +else +{ + foreach ($sub in $subscriptions) + { + Select-AzSubscription -SubscriptionId $sub | Out-Null + $assignments = Get-AzPolicyAssignment -IncludeDescendent + foreach ($assignment in $assignments) + { + if (-not($policyAssignments[$assignment.PolicyAssignmentId])) + { + $assignmentName = $assignment.Properties.DisplayName + if([string]::IsNullOrWhiteSpace($assignmentName)) { + $policyAssignments.Add($assignment.PolicyAssignmentId, 'N/A') + } + else { + $policyAssignments.Add($assignment.PolicyAssignmentId, $assignmentName) + } + } + if ($assignment.Properties.NotScopes -and -not($excludedAssignmentScopes | Where-Object { $_.PolicyAssignmentId -eq $assignment.PolicyAssignmentId })) + { + $excludedAssignmentScopes += $assignment + } + } + + $initiatives = Get-AzPolicySetDefinition + foreach ($initiative in $initiatives) + { + if (-not($policyInitiatives[$initiative.PolicySetDefinitionId])) + { + $setDefinitionName = $initiative.Properties.DisplayName + if([string]::IsNullOrWhiteSpace($setDefinitionName)) { + $policyInitiatives.Add($initiative.PolicySetDefinitionId, 'N/A') + } + else { + $policyInitiatives.Add($initiative.PolicySetDefinitionId, $setDefinitionName) + } + } + if (-not($allInitiatives | Where-Object { $_.PolicySetDefinitionId -eq $initiative.PolicySetDefinitionId })) + { + $allInitiatives += $initiative + } + } + + $definitions = Get-AzPolicyDefinition + foreach ($definition in $definitions) + { + if (-not($policyDefinitions[$definition.PolicyDefinitionId])) + { + $definitionName = $initiative.Properties.DisplayName + if([string]::IsNullOrWhiteSpace($definitionName)) { + $policyDefinitions.Add($definition.PolicyDefinitionId, 'N/A') + } + else { + $policyDefinitions.Add($definition.PolicyDefinitionId, $definitionName) + } + } + } + } +} + +$policyStatesTotal = @() + +Write-Output "Querying for Policy states using $PolicyStatesEndpoint endpoint..." + +if ($PolicyStatesEndpoint -eq "ARG") +{ + $resultsSoFar = 0 + + $argQuery = @" + policyresources + | where type =~ 'microsoft.policyinsights/policystates' + | extend complianceState = tostring(properties.complianceState) + | extend complianceReason = tostring(properties.complianceReasonCode) + | where complianceState != 'Compliant' and complianceReason !contains 'ResourceNotFound' + | extend effect = tostring(properties.policyDefinitionAction) + | extend assignmentId = tolower(properties.policyAssignmentId) + | extend definitionId = tolower(properties.policyDefinitionId) + | extend definitionReferenceId = tolower(properties.policyDefinitionReferenceId) + | extend initiativeId = tolower(properties.policySetDefinitionId) + | extend resourceId = tolower(properties.resourceId) + | extend resourceType = tostring(properties.resourceType) + | extend evaluatedOn = todatetime(properties.timestamp) + | summarize StatesCount = count() by id, tenantId, subscriptionId, resourceGroup, resourceId, resourceType, complianceState, complianceReason, effect, assignmentId, definitionReferenceId, definitionId, initiativeId, evaluatedOn + | union ( policyresources + | where type =~ 'microsoft.policyinsights/policystates' + | extend complianceState = tostring(properties.complianceState) + | where complianceState == 'Compliant' + | extend effect = tostring(properties.policyDefinitionAction) + | extend assignmentId = tolower(properties.policyAssignmentId) + | extend definitionId = tolower(properties.policyDefinitionId) + | extend definitionReferenceId = tolower(properties.policyDefinitionReferenceId) + | extend initiativeId = tolower(properties.policySetDefinitionId) + | summarize StatesCount = count() by tenantId, subscriptionId, complianceState, effect, assignmentId, definitionReferenceId, definitionId, initiativeId + ) + | join kind=leftouter ( + resources + | project resourceId=tolower(id), tags + ) on resourceId + | project-away resourceId1 + | order by id asc +"@ + + do + { + if ($resultsSoFar -eq 0) + { + $policyStates = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions + } + else + { + $policyStates = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions + } + if ($policyStates -and $policyStates.GetType().Name -eq "PSResourceGraphResponse") + { + $policyStates = $policyStates.Data + } + $resultsCount = $policyStates.Count + $resultsSoFar += $resultsCount + $policyStatesTotal += $policyStates + + } while ($resultsCount -eq $ARGPageSize) + + Write-Output "Building $($policyStatesTotal.Count) policyState entries" +} +else +{ + foreach ($sub in $subscriptions) + { + Select-AzSubscription -SubscriptionId $sub | Out-Null + $policyStates = Get-AzPolicyState -All + + $nonCompliantStates = $policyStates | Where-Object { $_.ComplianceState -ne "Compliant" } + + foreach ($policyState in $nonCompliantStates) + { + $policyStateObject = New-Object PSObject -Property @{ + tenantId = $tenantId + subscriptionId = $sub + resourceGroup = $policyState.ResourceGroup + resourceId = $policyState.ResourceId + resourceType = $policyState.ResourceType + complianceState = $policyState.ComplianceState + complianceReason = $policyState.AdditionalProperties.complianceReasonCode + effect = $policyState.PolicyDefinitionAction + assignmentId = $policyState.PolicyAssignmentId + initiativeId = $policyState.PolicySetDefinitionId + definitionId = $policyState.PolicyDefinitionId + definitionReferenceId = $policyState.PolicyDefinitionReferenceId + evaluatedOn = $policyState.Timestamp + StatesCount = 1 + } + $policyStatesTotal += $policyStateObject + } + + $compliantStates = $policyStates | Where-Object { $_.ComplianceState -eq "Compliant" } ` + | Group-Object PolicyDefinitionAction, PolicyAssignmentId, PolicyDefinitionId, PolicyDefinitionReferenceId, PolicySetDefinitionId + + foreach ($policyState in $compliantStates) + { + $compliantStateProps = $policyState.Name.Split(',') + $definitionReferenceId = $null + if ($compliantStateProps[3]) + { + $definitionReferenceId = $compliantStateProps[3].Trim().ToLower() + } + $initiativeId = $null + if ($compliantStateProps[4]) + { + $initiativeId = $compliantStateProps[4].Trim().ToLower() + } + + $policyStateObject = New-Object PSObject -Property @{ + tenantId = $tenantId + subscriptionId = $sub + complianceState = "Compliant" + effect = $compliantStateProps[0] + assignmentId = $compliantStateProps[1].Trim().ToLower() + definitionId = $compliantStateProps[2].Trim().ToLower() + definitionReferenceId = $definitionReferenceId + initiativeId = $initiativeId + StatesCount = $policyState.Count + } + $policyStatesTotal += $policyStateObject + } + } + + Write-Output "Building $($policyStatesTotal.Count) policyState entries" +} + +$datetime = (Get-Date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") +$statusDate = $datetime.ToString("yyyy-MM-dd") + +foreach ($policyState in $policyStatesTotal) +{ + $resourceGroup = $null + if ($policyState.resourceGroup) + { + $resourceGroup = $policyState.resourceGroup.ToLower() + } + + if (-not([string]::IsNullOrEmpty($policyState.tags))) + { + $tags = $policyState.tags | ConvertTo-Json -Compress + } + else + { + $tags = $null + } + + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $policyState.tenantId + SubscriptionGuid = $policyState.subscriptionId + ResourceGroupName = $resourceGroup + ResourceId = $policyState.resourceId + ResourceType = $policyState.resourceType + ComplianceState = $policyState.complianceState + ComplianceReason = $policyState.complianceReason + Effect = $policyState.effect + AssignmentId = $policyState.assignmentId + AssignmentName = $policyAssignments[$policyState.assignmentId] + InitiativeId = $policyState.initiativeId + InitiativeName = $policyInitiatives[$policyState.initiativeId] + DefinitionId = $policyState.definitionId + DefinitionName = $policyDefinitions[$policyState.definitionId] + DefinitionReferenceId = $policyState.definitionReferenceId + EvaluatedOn = $policyState.evaluatedOn + StatesCount = $policyState.StatesCount + Tags = $tags + StatusDate = $statusDate + } + + $allpolicyStates += $logentry +} + +if ($PolicyStatesEndpoint -eq "ARG") +{ + $resultsSoFar = 0 + + $argQuery = @" + policyresources + | where type =~ 'microsoft.authorization/policyassignments' + | where array_length(properties.notScopes) > 0 + | mv-expand notScope = properties.notScopes + | extend policyAssignmentId = tolower(id) + | extend assignmentPolicyDefinitionId = tolower(properties.policyDefinitionId) + | join kind=leftouter ( + policyresources + | where type =~ 'microsoft.authorization/policysetdefinitions' + | mv-expand policyDefinition = properties.policyDefinitions + | project policySetDefinitionId = tolower(id), policyDefinitionId = tolower(policyDefinition.policyDefinitionId), policyDefinitionReferenceId = tolower(policyDefinition.policyDefinitionReferenceId) + ) on `$left.assignmentPolicyDefinitionId == `$right.policySetDefinitionId + | project policyAssignmentId, notScope, assignmentPolicyDefinitionId, policySetDefinitionId, policyDefinitionId, policyDefinitionReferenceId + | order by policyDefinitionReferenceId, tostring(notScope) +"@ + + do + { + if ($resultsSoFar -eq 0) + { + $argExcludedAssignments = Search-AzGraph -Query $argQuery -First $ARGPageSize -UseTenantScope + } + else + { + $argExcludedAssignments = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -UseTenantScope + } + if ($argExcludedAssignments -and $argExcludedAssignments.GetType().Name -eq "PSResourceGraphResponse") + { + $argExcludedAssignments = $argExcludedAssignments.Data + } + $resultsCount = $argExcludedAssignments.Count + $resultsSoFar += $resultsCount + $excludedAssignmentScopes += $argExcludedAssignments + + } while ($resultsCount -eq $ARGPageSize) + + Write-Output "Adding excluded scopes from $($excludedAssignmentScopes.Count) assignments" + + foreach ($excludedAssignmentScope in $excludedAssignmentScopes) + { + if (-not([String]::IsNullOrEmpty($excludedAssignmentScope.policySetDefinitionId))) + { + $initiativeId = $excludedAssignmentScope.policySetDefinitionId + $initiativeName = $policyInitiatives[$initiativeId] + $definitionReferenceId = $excludedAssignmentScope.policyDefinitionReferenceId + $definitionId = $excludedAssignmentScope.policyDefinitionId + } + else + { + $initiativeId = $null + $initiativeName = $null + $definitionReferenceId = $null + $definitionId = $excludedAssignmentScope.assignmentPolicyDefinitionId + } + + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $tenantId + ResourceId = $excludedAssignmentScope.notScope + ComplianceState = 'Excluded' + AssignmentId = $excludedAssignmentScope.policyAssignmentId + AssignmentName = $policyAssignments[$excludedAssignmentScope.policyAssignmentId] + InitiativeId = $initiativeId + InitiativeName = $initiativeName + DefinitionId = $definitionId + DefinitionName = $policyDefinitions[$definitionId] + DefinitionReferenceId = $definitionReferenceId + StatusDate = $statusDate + } + + $allpolicyStates += $logentry + } +} +else +{ + Write-Output "Adding excluded scopes from $($excludedAssignmentScopes.Count) assignments" + + foreach ($excludedAssignment in $excludedAssignmentScopes) + { + $excludedIDs = @() + $excludedInitiative = $allInitiatives | Where-Object { $_.PolicySetDefinitionId -eq $excludedAssignment.Properties.PolicyDefinitionId } + if ($excludedInitiative) + { + $excludedDefinitions = $excludedInitiative.Properties.PolicyDefinitions + foreach ($excludedDefinition in $excludedDefinitions) + { + $excludedIDs += "$($excludedDefinition.policyDefinitionId)|$($excludedDefinition.policyDefinitionReferenceId)" + } + } + else + { + $excludedIDs += $excludedAssignment.Properties.PolicyDefinitionId + } + + foreach ($excludedID in $excludedIDs) + { + $excludedIDParts = $excludedID.Split('|') + $definitionId = $excludedIDParts[0].ToLower() + $definitionReferenceId = $null + if (-not([string]::IsNullOrEmpty($excludedIDParts[1]))) + { + $definitionReferenceId = $excludedIDParts[1].ToLower() + } + + $initiativeId = $null + $initiativeName = $null + if ($excludedInitiative) + { + $initiativeId = $excludedInitiative.PolicySetDefinitionId.ToLower() + $initiativeName = $policyInitiatives[$initiativeId] + } + + foreach ($notScope in $excludedAssignment.Properties.NotScopes) + { + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + TenantGuid = $tenantId + ResourceId = $notScope.ToLower() + ComplianceState = 'Excluded' + AssignmentId = $excludedAssignment.PolicyAssignmentId.ToLower() + AssignmentName = $policyAssignments[$excludedAssignment.PolicyAssignmentId] + InitiativeId = $initiativeId + InitiativeName = $initiativeName + DefinitionId = $definitionId + DefinitionName = $policyDefinitions[$definitionId] + DefinitionReferenceId = $definitionReferenceId + StatusDate = $statusDate + } + + $allpolicyStates += $logentry + } + } + } +} + +Write-Output "Uploading CSV to Storage" + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-policyStates-$subscriptionSuffix.csv" + +$allpolicyStates | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +Write-Output "Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +Write-Output "Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-PriceSheetToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-PriceSheetToBlobStorage.ps1 new file mode 100644 index 000000000..e315cd995 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-PriceSheetToBlobStorage.ps1 @@ -0,0 +1,452 @@ +param( + [Parameter(Mandatory = $false)] + [string] $BillingAccountID, + + [Parameter(Mandatory = $false)] + [string] $BillingProfileID, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName, + + [Parameter(Mandatory = $false)] + [string] $billingPeriod, # YYYYMM format + + [Parameter(Mandatory = $false)] + [string] $meterCategories, # comma-separated meter categories (e.g., "Virtual Machines,Storage") + + [Parameter(Mandatory = $false)] + [string] $meterRegions # comma-separated billing meter regions (e.g., "EU North,EU West") +) + +$ErrorActionPreference = "Stop" + +function Authenticate-AzureWithOption { + param ( + [string] $authOption = "ManagedIdentity", + [string] $cloudEnv = "AzureCloud", + [string] $clientID + ) + + switch ($authOption) { + "UserAssignedManagedIdentity" { + Connect-AzAccount -Identity -EnvironmentName $cloudEnv -AccountId $clientID + break + } + Default { #ManagedIdentity + Connect-AzAccount -Identity -EnvironmentName $cloudEnv + break + } + } +} + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_PriceSheetContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "pricesheetexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$meterCategoriesVar = Get-AutomationVariable -Name "AzureOptimization_PriceSheetMeterCategories" -ErrorAction SilentlyContinue +$meterRegionsVar = Get-AutomationVariable -Name "AzureOptimization_PriceSheetMeterRegions" -ErrorAction SilentlyContinue +$BillingAccountIDVar = Get-AutomationVariable -Name "AzureOptimization_BillingAccountID" -ErrorAction SilentlyContinue +$BillingProfileIDVar = Get-AutomationVariable -Name "AzureOptimization_BillingProfileID" -ErrorAction SilentlyContinue + +"Logging in to Azure with $authenticationOption..." + +if ($authenticationOption -eq "UserAssignedManagedIdentity") +{ + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID +} +else +{ + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudEnvironment = $externalCloudEnvironment +} + +# compute billing period + +if ([string]::IsNullOrEmpty($billingPeriod)) +{ + $billingPeriod = (Get-Date).Date.AddDays($consumptionOffsetDays * -1).ToString("yyyyMM") +} + +$exportDate = (Get-Date).ToUniversalTime().ToString("yyyyMMdd") + +if ([string]::IsNullOrEmpty($BillingAccountID) -and -not([string]::IsNullOrEmpty($BillingAccountIDVar))) +{ + $BillingAccountID = $BillingAccountIDVar +} + +if ([string]::IsNullOrEmpty($BillingProfileID) -and -not([string]::IsNullOrEmpty($BillingProfileIDVar))) +{ + $BillingProfileID = $BillingProfileIDVar +} + +$mcaBillingAccountIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}" +$mcaBillingProfileIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+)" + +if ([string]::IsNullOrEmpty($BillingAccountID)) +{ + throw "Billing Account ID undefined. Use either the AzureOptimization_BillingAccountID variable or the BillingAccountID parameter" +} +else { + if ($BillingAccountID -match $mcaBillingAccountIdRegex) + { + if ([string]::IsNullOrEmpty($BillingProfileID)) + { + throw "Billing Profile ID undefined for MCA. Use either the AzureOptimization_BillingProfileID variable or the BillingProfileID parameter" + } + if (-not($BillingProfileID -match $mcaBillingProfileIdRegex)) + { + throw "Billing Profile ID does not follow pattern for MCA: ([A-Za-z0-9]+(-[A-Za-z0-9]+)+)" + } + } +} + +if (-not([string]::IsNullOrEmpty($meterCategoriesVar))) +{ + $meterCategories = $meterCategoriesVar +} + +if (-not([string]::IsNullOrEmpty($meterRegionsVar))) +{ + $meterRegions = $meterRegionsVar +} + +$meterCategoryFilters = $null +$meterRegionFilters = $null + +if (-not([string]::IsNullOrEmpty($meterCategories))) +{ + $meterCategoryFilters = $meterCategories.Split(',') +} + +if (-not([string]::IsNullOrEmpty($meterRegions))) +{ + $meterRegionFilters = $meterRegions.Split(',') +} + +function Generate-Pricesheet { + param ( + [string] $InputCSVPath, + [string] $OutputCSVPath, + [string] $HeaderLine + ) + + # header normalization between MCA and EA + $headerConversion = @{ + 'Meter ID' = "MeterID"; + meterId = "MeterID"; + 'Meter name' = "MeterName"; + meterName = "MeterName"; + 'Meter category' = "MeterCategory"; + meterCategory = "MeterCategory"; + 'Meter sub-category' = "MeterSubCategory"; + meterSubCategory = "MeterSubCategory"; + 'Meter region' = "MeterRegion"; + meterRegion = "MeterRegion"; + 'Unit of measure' = "UnitOfMeasure"; + unitOfMeasure = "UnitOfMeasure"; + 'Part number' = "PartNumber"; + 'Unit price' = "UnitPrice"; + unitPrice = "UnitPrice"; + 'Currency code' = "CurrencyCode"; + currency = "CurrencyCode"; + 'Included quantity' = "IncludedQuantity"; + includedQuantity = "IncludedQuantity"; + 'Offer Id' = "OfferId"; + Term = "Term"; + 'Price type' = "PriceType"; + priceType = "PriceType" + } + + $r = [IO.File]::OpenText($InputCSVPath) + $w = [System.IO.StreamWriter]::new($OutputCSVPath) + $lineCounter = 0 + while ($r.Peek() -ge 0) { + $line = $r.ReadLine() + $lineCounter++ + if ($lineCounter -eq $HeaderLine) + { + $headers = $line.Split(",") + + for ($i = 0; $i -lt $headers.Length; $i++) + { + $header = $headers[$i] + if ($headerConversion.ContainsKey($header)) + { + $headers[$i] = $headerConversion[$header] + } + } + + $line = $headers -join "," + + if (-not($line -match "SubCategory")) + { + throw "Pricesheet format has changed at line $HeaderLine - $line" + } + + Write-Output "New headers: $line" + + $w.WriteLine($line) + } + else + { + if ($lineCounter -gt $HeaderLine) + { + $categoryWriteLine = $categoryWriteLineDefault + $regionWriteLine = $regionWriteLineDefault + + foreach ($meterCategory in $meterCategoryFilters) + { + if ($line -match ",$meterCategory,") + { + $categoryWriteLine = $true + break + } + } + + foreach ($meterRegion in $meterRegionFilters) + { + if ($line -match ",$meterRegion,") + { + $regionWriteLine = $true + break + } + } + + if ($categoryWriteLine -eq $true -and $regionWriteLine -eq $true) + { + $w.WriteLine($line) + } + } + } + } + $r.Dispose() + $w.Close() + + $csvBlobName = [System.IO.Path]::GetFileName($OutputCSVPath) + $csvProperties = @{"ContentType" = "text/csv"}; + Set-AzStorageBlobContent -File $OutputCSVPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + + Remove-Item -Path $InputCSVPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "[$now] Removed $InputCSVPath from local disk..." + + Remove-Item -Path $OutputCSVPath -Force + + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "[$now] Removed $OutputCSVPath from local disk..." +} + +Write-Output "Starting pricesheet export process for $billingPeriod billing period for Billing Account $BillingAccountID..." + +$MaxTries = 30 # The typical Retry-After is set to 20 seconds. We'll give 10 minutes overall to download the pricesheet report + +if ($BillingAccountID -match $mcaBillingAccountIdRegex) +{ + $PriceSheetApiPath = "/providers/Microsoft.Billing/billingAccounts/$BillingAccountID/billingProfiles/$BillingProfileID/providers/Microsoft.CostManagement/pricesheets/default/download?api-version=2023-03-01&format=csv" + $result = Invoke-AzRestMethod -Path $PriceSheetApiPath -Method POST +} +else +{ + $PriceSheetApiPath = "/providers/Microsoft.Billing/billingAccounts/$BillingAccountID/billingPeriods/$billingPeriod/providers/Microsoft.Consumption/pricesheets/download?api-version=2022-06-01&ln=en" + $result = Invoke-AzRestMethod -Path $PriceSheetApiPath -Method GET +} + +$requestResultPath = $result.Headers.Location.PathAndQuery +if ($result.StatusCode -in (200,202)) +{ + $tries = 0 + $requestSuccess = $false + + Write-Output "Obtained pricesheet results endpoint: $requestResultPath..." + + Write-Output "Was told to wait $($result.Headers.RetryAfter.Delta.TotalSeconds) seconds." + + $sleepSeconds = 60 + if ($result.Headers.RetryAfter.Delta.TotalSeconds -gt 0) + { + $sleepSeconds = $result.Headers.RetryAfter.Delta.TotalSeconds + } + + do + { + $tries++ + Write-Output "Checking whether export is ready (try $tries)..." + + Start-Sleep -Seconds $sleepSeconds + $downloadResult = Invoke-AzRestMethod -Method GET -Path $requestResultPath + + if ($downloadResult.StatusCode -eq 200) + { + Write-Output "Filtering data with meter categories $meterCategories and meter regions $meterRegions to $finalCsvExportPath..." + + $categoryWriteLineDefault = $true + if ($meterCategoryFilters.Count -gt 0) + { + $categoryWriteLineDefault = $false + } + $regionWriteLineDefault = $true + if ($meterRegionFilters.Count -gt 0) + { + $regionWriteLineDefault = $false + } + + Write-Output "Defaulting to meter categories writes $($categoryWriteLineDefault) and meter regions writes $($regionWriteLineDefault)..." + + if ($BillingAccountID -match $mcaBillingAccountIdRegex) + { + Write-Output "Export is ready. Proceeding with ZIP download..." + $downloadUrl = ($downloadResult.Content | ConvertFrom-Json).publishedEntity.properties.downloadUrl + $zipExportPath = "$env:TEMP\pricesheet-$BillingProfileID-$exportDate.zip" + $zipExpandPath = "$env:TEMP\pricesheet" + Invoke-WebRequest -Uri $downloadUrl -OutFile $zipExportPath + Write-Output "Blob downloaded to $zipExportPath successfully." + Expand-Archive -LiteralPath $zipExportPath -DestinationPath $zipExpandPath -Force + Write-Output "Zip expanded to $zipExpandPath successfully." + $csvFiles = Get-ChildItem -Path $zipExpandPath -Filter *.csv -Recurse + foreach ($csvFile in $csvFiles) + { + $csvExportPath = $csvFile.FullName + $finalCsvExportPath = "$env:TEMP\$($csvFile.Name)-final.csv" + Generate-Pricesheet -InputCSVPath $csvExportPath -OutputCSVPath $finalCsvExportPath -HeaderLine 1 + } + Remove-Item -Path $zipExportPath -Force + $now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "[$now] Removed $zipExportPath from local disk..." + } + else + { + Write-Output "Export is ready. Proceeding with CSV download..." + $downloadUrl = ($downloadResult.Content | ConvertFrom-Json).properties.downloadUrl + $csvExportPath = "$env:TEMP\pricesheet-$billingPeriod-$BillingAccountID.csv" + $finalCsvExportPath = "$env:TEMP\pricesheet-$billingPeriod-$BillingAccountID$($meterCategories.Replace(',',''))$($meterRegions.Replace(',',''))-$exportDate-final.csv" + Invoke-WebRequest -Uri $downloadUrl -OutFile $csvExportPath + Write-Output "Blob downloaded to $csvExportPath successfully." + Generate-Pricesheet -InputCSVPath $csvExportPath -OutputCSVPath $finalCsvExportPath -HeaderLine 3 + } + + $requestSuccess = $true + } + elseif ($downloadResult.StatusCode -eq 202) + { + Write-Output "Was told to wait a bit more... $($downloadResult.Headers.RetryAfter.Delta.TotalSeconds) seconds." + + $sleepSeconds = 60 + if ($downloadResult.Headers.RetryAfter.Delta.TotalSeconds -gt 0) + { + $sleepSeconds = $downloadResult.Headers.RetryAfter.Delta.TotalSeconds + } + } + elseif ($downloadResult.StatusCode -eq 401) + { + Write-Output "Had an authentication issue. Will login again and sleep just a couple of seconds." + + if ($authenticationOption -eq "UserAssignedManagedIdentity") + { + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID + } + else + { + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment + } + + $sleepSeconds = 2 + } + else + { + Write-Output "Got an unexpected response code: $($downloadResult.StatusCode)" + } + } + while (-not($requestSuccess) -and $tries -lt $MaxTries) + + if ($tries -ge $MaxTries) + { + throw "Couldn't complete request before the alloted number of $MaxTries retries" + } + + if (-not($requestSuccess)) + { + throw "Error returned by the Download PriceSheet API. Status Code: $($downloadResult.StatusCode). Message: $($downloadResult.Content)" + } + else + { + Write-Output "Export download processing complete." + } +} +else +{ + if ($result.StatusCode -ne 204) + { + throw "Error returned by the Download PriceSheet API. Status Code: $($result.StatusCode). Message: $($result.Content)" + } + else + { + Write-Output "Request returned 204 No Content" + } +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1 new file mode 100644 index 000000000..f5000dec2 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1 @@ -0,0 +1,258 @@ +param( + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RBACAssignmentsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "rbacexports" +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } + +$roleAssignments = @() + +"Iterating through all reachable subscriptions..." + +foreach ($subscription in $subscriptions) { + + Select-AzSubscription -SubscriptionId $subscription.Id -TenantId $tenantId | Out-Null + + $assignments = Get-AzRoleAssignment -IncludeClassicAdministrators -ErrorAction Continue + "Found $($assignments.Count) assignments for $($subscription.Name) subscription..." + + foreach ($assignment in $assignments) { + if ($null -eq $assignment.ObjectId -and $assignment.Scope.Contains($subscription.Id)) + { + $assignmentEntry = New-Object PSObject -Property @{ + Timestamp = $timestamp + TenantGuid = $tenantId + Cloud = $cloudEnvironment + Model = "AzureClassic" + PrincipalId = $assignment.SignInName + Scope = $assignment.Scope + RoleDefinition = $assignment.RoleDefinitionName + } + $roleAssignments += $assignmentEntry + } + else + { + $duplicateRoleAssignment = $roleAssignments | Where-Object { $_.PrincipalId -eq $assignment.ObjectId -and $_.Scope -eq $assignment.Scope -and $_.RoleDefinition -eq $assignment.RoleDefinitionName} + if (-not($duplicateRoleAssignment)) + { + $assignmentEntry = New-Object PSObject -Property @{ + Timestamp = $timestamp + TenantGuid = $tenantId + Cloud = $cloudEnvironment + Model = "AzureRM" + PrincipalId = $assignment.ObjectId + Scope = $assignment.Scope + RoleDefinition = $assignment.RoleDefinitionName + } + $roleAssignments += $assignmentEntry + } + } + } +} + +$fileDate = $datetime.ToString("yyyyMMdd") +$jsonExportPath = "$fileDate-$tenantId-rbacassignments.json" +$csvExportPath = "$fileDate-$tenantId-rbacassignments.csv" + +$roleAssignments | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath +"Exported to JSON: $($roleAssignments.Count) lines" +$rbacObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json +"JSON Import: $($rbacObjectsJson.Count) lines" +$rbacObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath +"Export to $csvExportPath" + +$csvBlobName = $csvExportPath +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +"[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +"[$now] Removed $csvExportPath from local disk..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +"[$now] Removed $jsonExportPath from local disk..." + +$roleAssignments = @() + +"Getting Microsoft Entra ID roles..." + +#workaround for https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/888 +$localPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile) +if (-not(get-item "$localPath\.graph\" -ErrorAction SilentlyContinue)) +{ + New-Item -Type Directory "$localPath\.graph" +} + +Import-Module Microsoft.Graph.Identity.DirectoryManagement + +switch ($cloudEnvironment) { + "AzureUSGovernment" { + $graphEnvironment = "USGov" + break + } + "AzureChinaCloud" { + $graphEnvironment = "China" + break + } + "AzureGermanCloud" { + $graphEnvironment = "Germany" + break + } + Default { + $graphEnvironment = "Global" + } +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Microsoft Graph with $externalCredentialName external credential..." + Connect-MgGraph -TenantId $externalTenantId -ClientSecretCredential $externalCredential -Environment $graphEnvironment -NoWelcome +} +else +{ + "Logging in to Microsoft Graph..." + Connect-MgGraph -Identity -Environment $graphEnvironment -NoWelcome +} + +$domainName = (Get-MgDomain | Where-Object { $_.IsVerified -and $_.IsDefault } | Select-Object -First 1).Id + +$roles = Get-MgDirectoryRole -ExpandProperty Members -Property DisplayName,Members +foreach ($role in $roles) +{ + $roleMembers = $role.Members | Where-Object { -not($_.DeletedDateTime) } + foreach ($roleMember in $roleMembers) + { + $assignmentEntry = New-Object PSObject -Property @{ + Timestamp = $timestamp + TenantGuid = $tenantId + Cloud = $cloudEnvironment + Model = "AzureAD" + PrincipalId = $roleMember.Id + Scope = $domainName + RoleDefinition = $role.DisplayName + } + $roleAssignments += $assignmentEntry + } +} + +$fileDate = $datetime.ToString("yyyyMMdd") +$jsonExportPath = "$fileDate-$tenantId-aadrbacassignments.json" +$csvExportPath = "$fileDate-$tenantId-aadrbacassignments.csv" + +$roleAssignments | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath +"Exported to JSON: $($roleAssignments.Count) lines" +$rbacObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json +"JSON Import: $($rbacObjectsJson.Count) lines" +$rbacObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath +"Export to $csvExportPath" + +$csvBlobName = $csvExportPath +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +"[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +"[$now] Removed $csvExportPath from local disk..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +"[$now] Removed $jsonExportPath from local disk..." diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ReservationsPriceToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ReservationsPriceToBlobStorage.ps1 new file mode 100644 index 000000000..9e4fa5554 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ReservationsPriceToBlobStorage.ps1 @@ -0,0 +1,146 @@ +param( + [Parameter(Mandatory = $false)] + [string] $Filter = "serviceName eq 'Virtual Machines' and priceType eq 'Reservation'" # e.g., serviceName eq 'Virtual Machines' and priceType eq 'Reservation' and armRegionName eq 'northeurope' +) + +$ErrorActionPreference = "Stop" + +function Authenticate-AzureWithOption { + param ( + [string] $authOption = "ManagedIdentity", + [string] $cloudEnv = "AzureCloud", + [string] $clientID + ) + + switch ($authOption) { + "UserAssignedManagedIdentity" { + Connect-AzAccount -Identity -EnvironmentName $cloudEnv -AccountId $clientID + break + } + Default { #ManagedIdentity + Connect-AzAccount -Identity -EnvironmentName $cloudEnv + break + } + } +} + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ReservationsPriceContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "reservationspriceexports" +} + +$filterVar = Get-AutomationVariable -Name "AzureOptimization_RetailPricesFilter" -ErrorAction SilentlyContinue +$currencyCode = Get-AutomationVariable -Name "AzureOptimization_RetailPricesCurrencyCode" + +"Logging in to Azure with $authenticationOption..." + +if ($authenticationOption -eq "UserAssignedManagedIdentity") +{ + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID +} +else +{ + Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudEnvironment = $externalCloudEnvironment +} + +if (-not([string]::IsNullOrEmpty($filterVar))) +{ + $Filter = $filterVar +} + +Write-Output "Starting retails prices export process with $currencyCode currency code and filter: $Filter ..." + +$RetailPricesApiPath = "https://prices.azure.com/api/retail/prices?currencyCode='$currencyCode'&`$filter=$Filter" + +$prices = @() + +do +{ + $Response = Invoke-RestMethod -Method Get -Uri $RetailPricesApiPath + if ($Response.Items.Count -gt 0) + { + $prices += $Response.Items + } + $RetailPricesApiPath = $Response.NextPageLink +} while ($Response.NextPageLink) + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyyMMdd") + +$fileFriendlyFilter = $Filter.Replace(" ","").Replace("'","") +$csvExportPath = "reservationsprice-$timestamp-$fileFriendlyFilter.csv" + +$ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name) +if ($ci.NumberFormat.NumberDecimalSeparator -ne '.') +{ + Write-Output "Current culture ($($ci.Name)) does not use . as decimal separator" + $ci.NumberFormat.NumberDecimalSeparator = '.' + [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci +} + +$prices | Export-Csv -NoTypeInformation -Path $csvExportPath + +Write-Output "Reservations price CSV exported to $csvExportPath successfully." + +$csvBlobName = $csvExportPath +$csvProperties = @{"ContentType" = "text/csv"}; +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ReservationsUsageToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ReservationsUsageToBlobStorage.ps1 new file mode 100644 index 000000000..80cdb3985 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-ReservationsUsageToBlobStorage.ps1 @@ -0,0 +1,304 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetScope, + + [Parameter(Mandatory = $false)] + [string] $BillingAccountID, + + [Parameter(Mandatory = $false)] + [string] $BillingProfileID, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName, + + [Parameter(Mandatory = $false)] + [string] $targetStartDate, # YYYY-MM-DD format + + [Parameter(Mandatory = $false)] + [string] $targetEndDate # YYYY-MM-DD format +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_ReservationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "reservationsexports" +} + +$BillingAccountIDVar = Get-AutomationVariable -Name "AzureOptimization_BillingAccountID" -ErrorAction SilentlyContinue +$BillingProfileIDVar = Get-AutomationVariable -Name "AzureOptimization_BillingProfileID" -ErrorAction SilentlyContinue + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") + +if ([string]::IsNullOrEmpty($BillingAccountID) -and -not([string]::IsNullOrEmpty($BillingAccountIDVar))) +{ + $BillingAccountID = $BillingAccountIDVar +} + +if ([string]::IsNullOrEmpty($BillingProfileID) -and -not([string]::IsNullOrEmpty($BillingProfileIDVar))) +{ + $BillingProfileID = $BillingProfileIDVar +} + +$mcaBillingAccountIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}" +$mcaBillingProfileIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+)" + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +# compute start+end dates + +if ([string]::IsNullOrEmpty($targetStartDate) -or [string]::IsNullOrEmpty($targetEndDate)) +{ + $targetStartDate = (Get-Date).Date.AddDays($consumptionOffsetDays * -1).ToString("yyyy-MM-dd") + $targetEndDate = $targetStartDate +} + +if (-not([string]::IsNullOrEmpty($TargetScope))) +{ + $scope = $TargetScope +} +else +{ + if ([string]::IsNullOrEmpty($BillingAccountID)) + { + throw "Billing Account ID undefined. Use either the AzureOptimization_BillingAccountID variable or the BillingAccountID parameter" + } + if ($BillingAccountID -match $mcaBillingAccountIdRegex) + { + if ([string]::IsNullOrEmpty($BillingProfileID)) + { + throw "Billing Profile ID undefined for MCA. Use either the AzureOptimization_BillingProfileID variable or the BillingProfileID parameter" + } + if (-not($BillingProfileID -match $mcaBillingProfileIdRegex)) + { + throw "Billing Profile ID does not follow pattern for MCA: ([A-Za-z0-9]+(-[A-Za-z0-9]+)+)" + } + $scope = "/providers/Microsoft.Billing/billingaccounts/$BillingAccountID/billingProfiles/$BillingProfileID" + } + else + { + $scope = "/providers/Microsoft.Billing/billingaccounts/$BillingAccountID" + } +} + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Starting reservations export process from $targetStartDate to $targetEndDate for scope $scope..." + +# get reservations details + +$reservationsDetailsResponse = $null +$reservationsDetails = @() +$reservationsDetailsPath = "$scope/reservations?api-version=2020-05-01&&refreshSummary=true" + +do +{ + if (-not([string]::IsNullOrEmpty($reservationsDetailsResponse.nextLink))) + { + $reservationsDetailsPath = $reservationsDetailsResponse.nextLink.Substring($reservationsDetailsResponse.nextLink.IndexOf("/providers/")) + } + + $result = Invoke-AzRestMethod -Path $reservationsDetailsPath -Method GET + + if (-not($result.StatusCode -in (200, 201, 202))) + { + throw "Error while getting reservations details: $($result.Content)" + } + + $reservationsDetailsResponse = $result.Content | ConvertFrom-Json + if ($reservationsDetailsResponse.value) + { + $reservationsDetails += $reservationsDetailsResponse.value + } +} +while (-not([string]::IsNullOrEmpty($reservationsDetailsResponse.nextLink))) + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Found $($reservationsDetails.Count) reservation details." + +# get reservations usage + +$reservationsUsage = @() +if ($BillingAccountID -match $mcaBillingAccountIdRegex) +{ + $reservationsUsagePath = "$scope/providers/Microsoft.Consumption/reservationSummaries?api-version=2023-05-01&startDate=$targetStartDate&endDate=$targetEndDate&grain=daily" +} +else +{ + $reservationsUsagePath = "$scope/providers/Microsoft.Consumption/reservationSummaries?api-version=2023-05-01&`$filter=properties/UsageDate ge $targetStartDate and properties/UsageDate le $targetEndDate&grain=daily" +} + +$result = Invoke-AzRestMethod -Path $reservationsUsagePath -Method GET + +if (-not($result.StatusCode -in (200, 201, 202))) +{ + throw "Error while getting reservations usage: $($result.Content)" +} + +$reservationsUsageResponse = $result.Content | ConvertFrom-Json +if ($reservationsUsageResponse.value) +{ + $reservationsUsage += $reservationsUsageResponse.value +} + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Found $($reservationsUsage.Count) reservation usages." + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +$reservations = @() + +foreach ($usage in $reservationsUsage) +{ + $reservationResourceId = "/providers/microsoft.capacity/reservationorders/$($usage.properties.reservationOrderId)/reservations/$($usage.properties.reservationId)" + $reservationDetail = $reservationsDetails | Where-Object { $_.id -eq $reservationResourceId } + $reservationEntry = New-Object PSObject -Property @{ + ReservationResourceId = $reservationResourceId + ReservationOrderId = $usage.properties.reservationOrderId + ReservationId = $usage.properties.reservationId + DisplayName = $reservationDetail.properties.displayName + SKUName = $usage.properties.skuName + Location = $reservationDetail.location + ResourceType = $reservationDetail.properties.reservedResourceType + AppliedScopeType = $reservationDetail.properties.userFriendlyAppliedScopeType + Term = $reservationDetail.properties.term + ProvisioningState = $reservationDetail.properties.displayProvisioningState + RenewState = $reservationDetail.properties.userFriendlyRenewState + PurchaseDate = $reservationDetail.properties.purchaseDate + ExpiryDate = $reservationDetail.properties.expiryDate + Archived = $reservationDetail.properties.archived + ReservedHours = $usage.properties.reservedHours + UsedHours = $usage.properties.usedHours + UsageDate = $usage.properties.usageDate + MinUtilPercentage = $usage.properties.minUtilizationPercentage + AvgUtilPercentage = $usage.properties.avgUtilizationPercentage + MaxUtilPercentage = $usage.properties.maxUtilizationPercentage + PurchasedQuantity = $usage.properties.purchasedQuantity + RemainingQuantity = $usage.properties.remainingQuantity + TotalReservedQuantity = $usage.properties.totalReservedQuantity + UsedQuantity = $usage.properties.usedQuantity + UtilizedPercentage = $usage.properties.utilizedPercentage + UtilTrend = $reservationDetail.properties.utilization.trend + Util1Days = ($reservationDetail.properties.utilization.aggregates | Where-Object { $_.grain -eq 1 }).value + Util7Days = ($reservationDetail.properties.utilization.aggregates | Where-Object { $_.grain -eq 7 }).value + Util30Days = ($reservationDetail.properties.utilization.aggregates | Where-Object { $_.grain -eq 30 }).value + Scope = $scope + TenantGuid = $tenantId + Cloud = $cloudEnvironment + CollectedDate = $timestamp + Timestamp = $timestamp + } + $reservations += $reservationEntry +} + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Generated $($reservations.Count) entries..." + +if ($BillingAccountID -match $mcaBillingAccountIdRegex) +{ + $csvExportPath = "$targetStartDate-$BillingProfileID.csv" +} +else +{ + $csvExportPath = "$targetStartDate-$BillingAccountID-$($scope.Split('/')[-1]).csv" +} + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploading CSV to Storage" + +$ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name) +if ($ci.NumberFormat.NumberDecimalSeparator -ne '.') +{ + Write-Output "Current culture ($($ci.Name)) does not use . as decimal separator" + $ci.NumberFormat.NumberDecimalSeparator = '.' + [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci +} + +$reservations | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath +$csvProperties = @{"ContentType" = "text/csv"}; +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-SavingsPlansUsageToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-SavingsPlansUsageToBlobStorage.ps1 new file mode 100644 index 000000000..a5eb70ab2 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Export-SavingsPlansUsageToBlobStorage.ps1 @@ -0,0 +1,252 @@ +param( + [Parameter(Mandatory = $false)] + [string] $TargetScope, + + [Parameter(Mandatory = $false)] + [string] $BillingAccountID, + + [Parameter(Mandatory = $false)] + [string] $BillingProfileID, + + [Parameter(Mandatory = $false)] + [string] $externalCloudEnvironment, + + [Parameter(Mandatory = $false)] + [string] $externalTenantId, + + [Parameter(Mandatory = $false)] + [string] $externalCredentialName +) + +$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" +} + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkEnv = Get-AutomationVariable -Name "AzureOptimization_StorageSinkEnvironment" -ErrorAction SilentlyContinue +if (-not($storageAccountSinkEnv)) +{ + $storageAccountSinkEnv = $cloudEnvironment +} +$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name "AzureOptimization_StorageSinkKey" -ErrorAction SilentlyContinue +$storageAccountSinkKey = $null +if ($storageAccountSinkKeyCred) +{ + $storageAccountSink = $storageAccountSinkKeyCred.UserName + $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password +} + +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_SavingsPlansContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) +{ + $storageAccountSinkContainer = "savingsplansexports" +} + +$BillingAccountIDVar = Get-AutomationVariable -Name "AzureOptimization_BillingAccountID" -ErrorAction SilentlyContinue +$BillingProfileIDVar = Get-AutomationVariable -Name "AzureOptimization_BillingProfileID" -ErrorAction SilentlyContinue + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName +} + +if ([string]::IsNullOrEmpty($BillingAccountID) -and -not([string]::IsNullOrEmpty($BillingAccountIDVar))) +{ + $BillingAccountID = $BillingAccountIDVar +} + +if ([string]::IsNullOrEmpty($BillingProfileID) -and -not([string]::IsNullOrEmpty($BillingProfileIDVar))) +{ + $BillingProfileID = $BillingProfileIDVar +} + +$mcaBillingAccountIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}" +$mcaBillingProfileIdRegex = "([A-Za-z0-9]+(-[A-Za-z0-9]+)+)" + +"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 + } +} + +if (-not($storageAccountSinkKey)) +{ + Write-Output "Getting Storage Account context with login" + Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId + $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context +} +else +{ + Write-Output "Getting Storage Account context with key" + $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv +} + +if (-not([string]::IsNullOrEmpty($externalCredentialName))) +{ + "Logging in to Azure with $externalCredentialName external credential..." + Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential + $cloudEnvironment = $externalCloudEnvironment +} + +$tenantId = (Get-AzContext).Tenant.Id + +if (-not([string]::IsNullOrEmpty($TargetScope))) +{ + $scope = $TargetScope +} +else +{ + if ([string]::IsNullOrEmpty($BillingAccountID)) + { + throw "Billing Account ID undefined. Use either the AzureOptimization_BillingAccountID variable or the BillingAccountID parameter" + } + if ($BillingAccountID -match $mcaBillingAccountIdRegex) + { + if ([string]::IsNullOrEmpty($BillingProfileID)) + { + throw "Billing Profile ID undefined for MCA. Use either the AzureOptimization_BillingProfileID variable or the BillingProfileID parameter" + } + if (-not($BillingProfileID -match $mcaBillingProfileIdRegex)) + { + throw "Billing Profile ID does not follow pattern for MCA: ([A-Za-z0-9]+(-[A-Za-z0-9]+)+)" + } + #$scope = "/providers/Microsoft.BillingBenefits" + $scope = "/providers/Microsoft.Billing/billingaccounts/$BillingAccountID" + } + else + { + $scope = "/providers/Microsoft.Billing/billingaccounts/$BillingAccountID" + } +} + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Starting savings plans export process for scope $scope..." + +$savingsPlansUsage = @() +if ($BillingAccountID -match $mcaBillingAccountIdRegex) +{ + #$savingsPlansUsagePath = "$scope/savingsPlans?api-version=2022-11-01&refreshsummary=true&take=100" + $savingsPlansUsagePath = "$scope/savingsPlans?api-version=2022-10-01-privatepreview&refreshsummary=true&take=100&`$filter=(properties/billingProfileId eq '/providers/Microsoft.Billing/billingAccounts/$BillingAccountID/billingProfiles/$BillingProfileID')" +} +else +{ + $savingsPlansUsagePath = "$scope/savingsPlans?api-version=2020-12-15-privatepreview&refreshsummary=true&take=100" +} + +$result = Invoke-AzRestMethod -Path $savingsPlansUsagePath -Method GET + +if (-not($result.StatusCode -in (200, 201, 202))) +{ + throw "Error while getting savings plans usage: $($result.Content)" +} + +$savingsPlansUsageResponse = $result.Content | ConvertFrom-Json +if ($savingsPlansUsageResponse.value) +{ + $savingsPlansUsage += $savingsPlansUsageResponse.value +} + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Found $($savingsPlansUsage.Count) savings plans usages." + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +$savingsPlans = @() + +foreach ($usage in $savingsPlansUsage) +{ + $savingsPlanEntry = New-Object PSObject -Property @{ + SavingsPlanResourceId = $usage.id + SavingsPlanOrderId = $usage.id.Substring(0,$usage.id.IndexOf("/savingsPlans/")) + SavingsPlanId = $usage.id.Split("/")[-1] + DisplayName = $usage.properties.displayName + SKUName = $usage.sku.name + Term = $usage.properties.term + ProvisioningState = $usage.properties.displayProvisioningState + AppliedScopeType = $usage.properties.userFriendlyAppliedScopeType + RenewState = $usage.properties.renew + PurchaseDate = $usage.properties.purchaseDateTime + BenefitStart = $usage.properties.benefitStartTime + ExpiryDate = $usage.properties.expiryDateTime + EffectiveDate = $usage.properties.effectiveDateTime + BillingScopeId = $usage.properties.billingScopeId + BillingAccountId = $usage.properties.billingAccountId + BillingProfileId = $usage.properties.billingProfileId + BillingPlan = $usage.properties.billingProfileId + CommitmentGrain = $usage.properties.commitment.grain + CommitmentCurrencyCode = $usage.properties.commitment.currencyCode + CommitmentAmount = $usage.properties.commitment.amount + UtilTrend = $usage.properties.utilization.trend + Util1Days = ($usage.properties.utilization.aggregates | Where-Object { $_.grain -eq 1 }).value + Util7Days = ($usage.properties.utilization.aggregates | Where-Object { $_.grain -eq 7 }).value + Util30Days = ($usage.properties.utilization.aggregates | Where-Object { $_.grain -eq 30 }).value + Scope = $scope + TenantGuid = $tenantId + Cloud = $cloudEnvironment + CollectedDate = $timestamp + Timestamp = $timestamp + } + $savingsPlans += $savingsPlanEntry +} + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Generated $($savingsPlans.Count) entries..." + +$targetDate = $datetime.ToString("yyyy-MM-dd") + +if ($BillingAccountID -match $mcaBillingAccountIdRegex) +{ + $csvExportPath = "$targetDate-$BillingProfileID.csv" +} +else +{ + $csvExportPath = "$targetDate-$BillingAccountID.csv" +} + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploading CSV to Storage" + +$ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name) +if ($ci.NumberFormat.NumberDecimalSeparator -ne '.') +{ + Write-Output "Current culture ($($ci.Name)) does not use . as decimal separator" + $ci.NumberFormat.NumberDecimalSeparator = '.' + [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci +} + +$savingsPlans | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath +$csvProperties = @{"ContentType" = "text/csv"}; +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $csvBlobName to Blob Storage..." + +Remove-Item -Path $csvExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $csvExportPath from local disk..." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1 new file mode 100644 index 000000000..e45dac978 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1 @@ -0,0 +1,329 @@ +param( + [Parameter(Mandatory = $true)] + [string] $StorageSinkContainer +) + +$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)) +{ + $sqldatabase = "azureoptimization" +} +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$sharedKey = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceKey" +$LogAnalyticsChunkSize = [int] (Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsChunkSize" -ErrorAction SilentlyContinue) +if (-not($LogAnalyticsChunkSize -gt 0)) +{ + $LogAnalyticsChunkSize = 6000 +} +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = $StorageSinkContainer +$StorageBlobsPageSize = [int] (Get-AutomationVariable -Name "AzureOptimization_StorageBlobsPageSize" -ErrorAction SilentlyContinue) +if (-not($StorageBlobsPageSize -gt 0)) +{ + $StorageBlobsPageSize = 1000 +} + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +"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 + } +} + +#region Functions + +# Function to create the authorization signature +Function Build-OMSSignature ($workspaceId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) { + $xHeaders = "x-ms-date:" + $date + $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource + $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) + $keyBytes = [Convert]::FromBase64String($sharedKey) + $sha256 = New-Object System.Security.Cryptography.HMACSHA256 + $sha256.Key = $keyBytes + $calculatedHash = $sha256.ComputeHash($bytesToHash) + $encodedHash = [Convert]::ToBase64String($calculatedHash) + $authorization = 'SharedKey {0}:{1}' -f $workspaceId, $encodedHash + return $authorization +} + +# Function to create and post the request +Function Post-OMSData($workspaceId, $sharedKey, $body, $logType, $TimeStampField, $AzureEnvironment) { + $method = "POST" + $contentType = "application/json" + $resource = "/api/logs" + $rfc1123date = [DateTime]::UtcNow.ToString("r") + $contentLength = $body.Length + $signature = Build-OMSSignature ` + -workspaceId $workspaceId ` + -sharedKey $sharedKey ` + -date $rfc1123date ` + -contentLength $contentLength ` + -method $method ` + -contentType $contentType ` + -resource $resource + + $uri = "https://" + $workspaceId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" + if ($AzureEnvironment -eq "AzureChinaCloud") + { + $uri = "https://" + $workspaceId + ".ods.opinsights.azure.cn" + $resource + "?api-version=2016-04-01" + } + if ($AzureEnvironment -eq "AzureUSGovernment") + { + $uri = "https://" + $workspaceId + ".ods.opinsights.azure.us" + $resource + "?api-version=2016-04-01" + } + if ($AzureEnvironment -eq "AzureGermanCloud") + { + throw "Azure Germany isn't suported for the Log Analytics Data Collector API" + } + + $OMSheaders = @{ + "Authorization" = $signature; + "Log-Type" = $logType; + "x-ms-date" = $rfc1123date; + "time-generated-field" = $TimeStampField; + } + + Try { + + $response = Invoke-WebRequest -Uri $uri -Method POST -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000 + } + catch { + if ($_.Exception.Response.StatusCode.Value__ -eq 401) { + "REAUTHENTICATING" + + $response = Invoke-WebRequest -Uri $uri -Method POST -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000 + } + else + { + return $_.Exception.Response.StatusCode.Value__ + } + } + + return $response.StatusCode +} +#endregion Functions + +# get reference to storage sink +Write-Output "Getting blobs list from $storageAccountSink storage account ($storageAccountSinkContainer container)..." +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +$allblobs = @() + +$continuationToken = $null +do +{ + $blobs = Get-AzStorageBlob -Container $storageAccountSinkContainer -MaxCount $StorageBlobsPageSize -ContinuationToken $continuationToken -Context $sa.Context | Sort-Object -Property LastModified + if ($blobs.Count -le 0) { break } + $allblobs += $blobs + $continuationToken = $blobs[$blobs.Count -1].ContinuationToken; +} +While ($null -ne $continuationToken) + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE StorageContainerName = '$storageAccountSinkContainer'" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$Conn.Close() +$Conn.Dispose() + +if ($controlRows.Count -eq 0 -or -not($controlRows[0].LastProcessedDateTime)) +{ + throw "Could not find a valid ingestion control row for $storageAccountSinkContainer" +} + +$controlRow = $controlRows[0] +$lastProcessedLine = $controlRow.LastProcessedLine +$lastProcessedDateTime = $controlRow.LastProcessedDateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +$LogAnalyticsSuffix = $controlRow.LogAnalyticsSuffix +$logname = $lognamePrefix + $LogAnalyticsSuffix + +Write-Output "Processing blobs modified after $lastProcessedDateTime (line $lastProcessedLine) and ingesting them into the $($logname)_CL table..." + +$newProcessedTime = $null + +$unprocessedBlobs = @() + +foreach ($blob in $allblobs) { + $blobLastModified = $blob.LastModified.UtcDateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + if ($lastProcessedDateTime -lt $blobLastModified -or ` + ($lastProcessedDateTime -eq $blobLastModified -and $lastProcessedLine -gt 0)) { + Write-Output "$($blob.Name) found (modified on $blobLastModified)" + $unprocessedBlobs += $blob + } +} + +$unprocessedBlobs = $unprocessedBlobs | Sort-Object -Property LastModified + +Write-Output "Found $($unprocessedBlobs.Count) new blobs to process..." + +foreach ($blob in $unprocessedBlobs) { + $newProcessedTime = $blob.LastModified.UtcDateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "About to process $($blob.Name)..." + $blobFilePath = "$env:TEMP\$($blob.Name)" + Get-AzStorageBlobContent -CloudBlob $blob.ICloudBlob -Context $sa.Context -Force -Destination $blobFilePath | Out-Null + + $r = [IO.File]::OpenText($blobFilePath) + + $linesProcessed = 0 + $lineCounter = 0 + $chunkLines = @() + + while ($r.Peek() -ge 0) + { + $line = $r.ReadLine() + if ($lineCounter -eq 0) + { + $header = $line + $chunkLines += $line + } + else + { + $linesProcessed++ + } + if ($lastProcessedLine -lt $linesProcessed -and $lineCounter -gt 0) + { + $chunkLines += $line + } + if (($lineCounter -eq $LogAnalyticsChunkSize -or $r.Peek() -lt 0) -and $linesProcessed -gt 0) + { + $csvObject = $chunkLines | ConvertFrom-Csv + $jsonObject = ConvertTo-Json -InputObject $csvObject + + $res = Post-OMSData -workspaceId $workspaceId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($jsonObject)) -logType $logname -TimeStampField "Timestamp" -AzureEnvironment $cloudEnvironment + if ($res -ge 200 -and $res -lt 300) + { + Write-Output "Succesfully uploaded $lineCounter $LogAnalyticsSuffix rows to Log Analytics" + if ($r.Peek() -lt 0) { + $lastProcessedLine = -1 + } + else { + $lastProcessedLine = $linesProcessed - 1 + } + + $updatedLastProcessedLine = $lastProcessedLine + $updatedLastProcessedDateTime = $lastProcessedDateTime + if ($r.Peek() -lt 0) { + $updatedLastProcessedDateTime = $newProcessedTime + } + $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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandText = $sqlStatement + $Cmd.CommandTimeout=120 + $Cmd.ExecuteReader() + $Conn.Close() + $Conn.Dispose() + } + else + { + Write-Warning "Failed to upload $lineCounter $LogAnalyticsSuffix rows. Error code: $res" + $r.Dispose() + Remove-Item -Path $blobFilePath -Force + throw + } + + $chunkLines = @() + $chunkLines += $header + $lineCounter = 1 + } + else + { + $lineCounter++ + } + } + $r.Dispose() + + if ($linesProcessed -eq 0) + { + Write-Output "No rows found" + $updatedLastProcessedLine = -1 + $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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandText = $sqlStatement + $Cmd.CommandTimeout=120 + $Cmd.ExecuteReader() + $Conn.Close() + $Conn.Dispose() + } + else + { + Write-Output "Processed $linesProcessed row(s) in total." + } + + Remove-Item -Path $blobFilePath -Force +} + +Write-Output "DONE" \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1 new file mode 100644 index 000000000..9220678b0 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1 @@ -0,0 +1,53 @@ +$ErrorActionPreference = "Stop" + +$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" +} +$RecommendationsMaxAge = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendationsMaxAgeInDays" -ErrorAction SilentlyContinue) +if (-not($RecommendationsMaxAge -gt 0)) +{ + $RecommendationsMaxAge = 365 +} + +$recommendationsTable = "Recommendations" + +$tries = 0 +$connectionSuccess = $false + +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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = 0 + $Cmd.CommandText = "DELETE FROM [dbo].[$recommendationsTable] WHERE GeneratedDate < GETDATE()-$RecommendationsMaxAge" + $DeletedRows = $Cmd.ExecuteNonQuery() + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } + finally { + $Conn.Close() + $Conn.Dispose() + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +Write-Output "Cleaned up $DeletedRows recommendations." \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1 new file mode 100644 index 000000000..391ed24c9 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1 @@ -0,0 +1,321 @@ +$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)) +{ + $sqldatabase = "azureoptimization" +} +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$sharedKey = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceKey" +$LogAnalyticsChunkSize = [int] (Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsChunkSize" -ErrorAction SilentlyContinue) +if (-not($LogAnalyticsChunkSize -gt 0)) +{ + $LogAnalyticsChunkSize = 6000 +} +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} +$StorageBlobsPageSize = [int] (Get-AutomationVariable -Name "AzureOptimization_StorageBlobsPageSize" -ErrorAction SilentlyContinue) +if (-not($StorageBlobsPageSize -gt 0)) +{ + $StorageBlobsPageSize = 1000 +} + +#region Functions + +# Function to create the authorization signature +Function Build-OMSSignature ($workspaceId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) { + $xHeaders = "x-ms-date:" + $date + $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource + $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) + $keyBytes = [Convert]::FromBase64String($sharedKey) + $sha256 = New-Object System.Security.Cryptography.HMACSHA256 + $sha256.Key = $keyBytes + $calculatedHash = $sha256.ComputeHash($bytesToHash) + $encodedHash = [Convert]::ToBase64String($calculatedHash) + $authorization = 'SharedKey {0}:{1}' -f $workspaceId, $encodedHash + return $authorization +} + +# Function to create and post the request +Function Post-OMSData($workspaceId, $sharedKey, $body, $logType, $TimeStampField, $AzureEnvironment) { + $method = "POST" + $contentType = "application/json" + $resource = "/api/logs" + $rfc1123date = [DateTime]::UtcNow.ToString("r") + $contentLength = $body.Length + $signature = Build-OMSSignature ` + -workspaceId $workspaceId ` + -sharedKey $sharedKey ` + -date $rfc1123date ` + -contentLength $contentLength ` + -method $method ` + -contentType $contentType ` + -resource $resource + + $uri = "https://" + $workspaceId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" + if ($AzureEnvironment -eq "AzureChinaCloud") + { + $uri = "https://" + $workspaceId + ".ods.opinsights.azure.cn" + $resource + "?api-version=2016-04-01" + } + if ($AzureEnvironment -eq "AzureUSGovernment") + { + $uri = "https://" + $workspaceId + ".ods.opinsights.azure.us" + $resource + "?api-version=2016-04-01" + } + if ($AzureEnvironment -eq "AzureGermanCloud") + { + throw "Azure Germany isn't suported for the Log Analytics Data Collector API" + } + + $OMSheaders = @{ + "Authorization" = $signature; + "Log-Type" = $logType; + "x-ms-date" = $rfc1123date; + "time-generated-field" = $TimeStampField; + } + + Try { + + $response = Invoke-WebRequest -Uri $uri -Method POST -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000 + } + catch { + if ($_.Exception.Response.StatusCode.Value__ -eq 401) { + "REAUTHENTICATING" + + $response = Invoke-WebRequest -Uri $uri -Method POST -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000 + } + else + { + return $_.Exception.Response.StatusCode.Value__ + } + } + + return $response.StatusCode +} +#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 + } +} + +# get reference to storage sink +Write-Output "Getting reference to $storageAccountSink storage account (recommendations exports sink)" +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context + +$allblobs = @() + +Write-Output "Getting blobs list..." +$continuationToken = $null +do +{ + $blobs = Get-AzStorageBlob -Container $storageAccountSinkContainer -MaxCount $StorageBlobsPageSize -ContinuationToken $continuationToken -Context $saCtx | Sort-Object -Property LastModified + if ($blobs.Count -le 0) { break } + $allblobs += $blobs + $continuationToken = $blobs[$blobs.Count -1].ContinuationToken; +} +While ($null -ne $continuationToken) + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE StorageContainerName = '$storageAccountSinkContainer'" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$Conn.Close() +$Conn.Dispose() + +if ($controlRows.Count -eq 0 -or -not($controlRows[0].LastProcessedDateTime)) +{ + throw "Could not find a valid ingestion control row for $storageAccountSinkContainer" +} + +$controlRow = $controlRows[0] +$lastProcessedLine = $controlRow.LastProcessedLine +$lastProcessedDateTime = $controlRow.LastProcessedDateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +$LogAnalyticsSuffix = $controlRow.LogAnalyticsSuffix +$logname = $lognamePrefix + $LogAnalyticsSuffix + +Write-Output "Processing blobs modified after $lastProcessedDateTime (line $lastProcessedLine) and ingesting them into the $($logname)_CL table..." + +$newProcessedTime = $null + +$unprocessedBlobs = @() + +foreach ($blob in $allblobs) { + $blobLastModified = $blob.LastModified.UtcDateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + if ($lastProcessedDateTime -lt $blobLastModified -or ` + ($lastProcessedDateTime -eq $blobLastModified -and $lastProcessedLine -gt 0)) { + Write-Output "$($blob.Name) found (modified on $blobLastModified)" + $unprocessedBlobs += $blob + } +} + +$unprocessedBlobs = $unprocessedBlobs | Sort-Object -Property LastModified + +Write-Output "Found $($unprocessedBlobs.Count) new blobs to process..." + +foreach ($blob in $unprocessedBlobs) { + $newProcessedTime = $blob.LastModified.UtcDateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "About to process $($blob.Name)..." + Get-AzStorageBlobContent -CloudBlob $blob.ICloudBlob -Context $saCtx -Force + $jsonObject = Get-Content -Path $blob.Name | ConvertFrom-Json + Write-Output "Blob contains $($jsonObject.Count) results..." + + if ($null -eq $jsonObject) + { + $recCount = 0 + } + elseif ($null -eq $jsonObject.Count) + { + $recCount = 1 + } + else + { + $recCount = $jsonObject.Count + } + + $linesProcessed = 0 + $jsonObjectSplitted = @() + + if ($recCount -gt 1) + { + for ($i = 0; $i -lt $recCount; $i += $LogAnalyticsChunkSize) { + $jsonObjectSplitted += , @($jsonObject[$i..($i + ($LogAnalyticsChunkSize - 1))]); + } + } + else + { + $jsonObjectArray = @() + $jsonObjectArray += $jsonObject + $jsonObjectSplitted += , $jsonObjectArray + } + + for ($j = 0; $j -lt $jsonObjectSplitted.Count; $j++) + { + if ($jsonObjectSplitted[$j]) + { + $currentObjectLines = $jsonObjectSplitted[$j].Count + if ($lastProcessedLine -lt $linesProcessed) + { + for ($i = 0; $i -lt $jsonObjectSplitted[$j].Count; $i++) + { + $jsonObjectSplitted[$j][$i].RecommendationDescription = $jsonObjectSplitted[$j][$i].RecommendationDescription.Replace("'", "") + $jsonObjectSplitted[$j][$i].RecommendationAction = $jsonObjectSplitted[$j][$i].RecommendationAction.Replace("'", "") + $jsonObjectSplitted[$j][$i].AdditionalInfo = $jsonObjectSplitted[$j][$i].AdditionalInfo | ConvertTo-Json -Compress + $jsonObjectSplitted[$j][$i].Tags = $jsonObjectSplitted[$j][$i].Tags | ConvertTo-Json -Compress + } + + $jsonObject = ConvertTo-Json -InputObject $jsonObjectSplitted[$j] + $res = Post-OMSData -workspaceId $workspaceId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($jsonObject)) -logType $logname -TimeStampField "Timestamp" -AzureEnvironment $cloudEnvironment + If ($res -ge 200 -and $res -lt 300) { + Write-Output "Succesfully uploaded $currentObjectLines $LogAnalyticsSuffix rows to Log Analytics" + $linesProcessed += $currentObjectLines + if ($j -eq ($jsonObjectSplitted.Count - 1)) { + $lastProcessedLine = -1 + } + else { + $lastProcessedLine = $linesProcessed - 1 + } + + $updatedLastProcessedLine = $lastProcessedLine + $updatedLastProcessedDateTime = $lastProcessedDateTime + if ($j -eq ($jsonObjectSplitted.Count - 1)) { + $updatedLastProcessedDateTime = $newProcessedTime + } + $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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandText = $sqlStatement + $Cmd.CommandTimeout=120 + $Cmd.ExecuteReader() + $Conn.Close() + $Conn.Dispose() + } + Else { + $linesProcessed += $currentObjectLines + Write-Warning "Failed to upload $currentObjectLines $LogAnalyticsSuffix rows. Error code: $res" + throw + } + } + else + { + $linesProcessed += $currentObjectLines + } + } + } + + Remove-Item -Path $blob.Name -Force +} + +Write-Output "DONE" \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1 new file mode 100644 index 000000000..f7041940f --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1 @@ -0,0 +1,285 @@ +$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)) +{ + $sqldatabase = "azureoptimization" +} +$ChunkSize = [int] (Get-AutomationVariable -Name "AzureOptimization_SQLServerInsertSize" -ErrorAction SilentlyContinue) +if (-not($ChunkSize -gt 0)) +{ + $ChunkSize = 900 +} +$SqlTimeout = 120 + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} +$StorageBlobsPageSize = [int] (Get-AutomationVariable -Name "AzureOptimization_StorageBlobsPageSize" -ErrorAction SilentlyContinue) +if (-not($StorageBlobsPageSize -gt 0)) +{ + $StorageBlobsPageSize = 1000 +} + +"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 + } +} + +# get reference to storage sink +Write-Output "Getting reference to $storageAccountSink storage account (recommendations exports sink)" +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context + +$allblobs = @() + +Write-Output "Getting blobs list..." +$continuationToken = $null +do +{ + $blobs = Get-AzStorageBlob -Container $storageAccountSinkContainer -MaxCount $StorageBlobsPageSize -ContinuationToken $continuationToken -Context $saCtx | Sort-Object -Property LastModified + if ($blobs.Count -le 0) { break } + $allblobs += $blobs + $continuationToken = $blobs[$blobs.Count -1].ContinuationToken; +} +While ($null -ne $continuationToken) + +$SqlServerIngestControlTable = "SqlServerIngestControl" +$recommendationsTable = "Recommendations" + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$SqlServerIngestControlTable] WHERE StorageContainerName = '$storageAccountSinkContainer' and SqlTableName = '$recommendationsTable'" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +if ($controlRows.Count -eq 0) +{ + throw "Could not find a control row for $storageAccountSinkContainer container and $recommendationsTable table." +} + +$controlRow = $controlRows[0] +$lastProcessedLine = $controlRow.LastProcessedLine +$lastProcessedDateTime = $controlRow.LastProcessedDateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + +$Conn.Close() +$Conn.Dispose() + +Write-Output "Processing blobs modified after $lastProcessedDateTime (line $lastProcessedLine) and ingesting them into the Recommendations SQL table..." + +$newProcessedTime = $null + +$unprocessedBlobs = @() + +foreach ($blob in $allblobs) { + $blobLastModified = $blob.LastModified.UtcDateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + if ($lastProcessedDateTime -lt $blobLastModified -or ` + ($lastProcessedDateTime -eq $blobLastModified -and $lastProcessedLine -gt 0)) { + Write-Output "$($blob.Name) found (modified on $blobLastModified)" + $unprocessedBlobs += $blob + } +} + +$unprocessedBlobs = $unprocessedBlobs | Sort-Object -Property LastModified + +Write-Output "Found $($unprocessedBlobs.Count) new blobs to process..." + +foreach ($blob in $unprocessedBlobs) { + $newProcessedTime = $blob.LastModified.UtcDateTime.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") + Write-Output "About to process $($blob.Name)..." + Get-AzStorageBlobContent -CloudBlob $blob.ICloudBlob -Context $saCtx -Force + $jsonObject = Get-Content -Path $blob.Name | ConvertFrom-Json + Write-Output "Blob contains $($jsonObject.Count) results..." + + if ($null -eq $jsonObject) + { + $recCount = 0 + } + elseif ($null -eq $jsonObject.Count) + { + $recCount = 1 + } + else + { + $recCount = $jsonObject.Count + } + + $linesProcessed = 0 + $jsonObjectSplitted = @() + + if ($recCount -gt 1) + { + for ($i = 0; $i -lt $recCount; $i += $ChunkSize) { + $jsonObjectSplitted += , @($jsonObject[$i..($i + ($ChunkSize - 1))]); + } + } + else + { + $jsonObjectArray = @() + $jsonObjectArray += $jsonObject + $jsonObjectSplitted += , $jsonObjectArray + } + + for ($j = 0; $j -lt $jsonObjectSplitted.Count; $j++) + { + if ($jsonObjectSplitted[$j]) + { + $currentObjectLines = $jsonObjectSplitted[$j].Count + if ($lastProcessedLine -lt $linesProcessed) + { + $sqlStatement = "INSERT INTO [$recommendationsTable]" + $sqlStatement += " (RecommendationId, GeneratedDate, Cloud, Category, ImpactedArea, Impact, RecommendationType, RecommendationSubType," + $sqlStatement += " RecommendationSubTypeId, RecommendationDescription, RecommendationAction, InstanceId, InstanceName, AdditionalInfo," + $sqlStatement += " ResourceGroup, SubscriptionGuid, SubscriptionName, TenantGuid, FitScore, Tags, DetailsUrl) VALUES" + for ($i = 0; $i -lt $jsonObjectSplitted[$j].Count; $i++) + { + $jsonObjectSplitted[$j][$i].RecommendationDescription = $jsonObjectSplitted[$j][$i].RecommendationDescription.Replace("'", "") + $jsonObjectSplitted[$j][$i].RecommendationAction = $jsonObjectSplitted[$j][$i].RecommendationAction.Replace("'", "") + if ($null -ne $jsonObjectSplitted[$j][$i].InstanceName) + { + $jsonObjectSplitted[$j][$i].InstanceName = $jsonObjectSplitted[$j][$i].InstanceName.Replace("'", "") + } + $additionalInfoString = $jsonObjectSplitted[$j][$i].AdditionalInfo | ConvertTo-Json -Compress + $tagsString = $jsonObjectSplitted[$j][$i].Tags | ConvertTo-Json -Compress + $subscriptionGuid = "NULL" + if ($jsonObjectSplitted[$j][$i].SubscriptionGuid) + { + $subscriptionGuid = "'$($jsonObjectSplitted[$j][$i].SubscriptionGuid)'" + } + $subscriptionName = "NULL" + if ($jsonObjectSplitted[$j][$i].SubscriptionName) + { + $subscriptionName = $jsonObjectSplitted[$j][$i].SubscriptionName.Replace("'", "") + $subscriptionName = "'$subscriptionName'" + } + $resourceGroup = "NULL" + if ($jsonObjectSplitted[$j][$i].ResourceGroup) + { + $resourceGroup = "'$($jsonObjectSplitted[$j][$i].ResourceGroup)'" + } + $sqlStatement += " (NEWID(), CONVERT(DATETIME, '$($jsonObjectSplitted[$j][$i].Timestamp)'), '$($jsonObjectSplitted[$j][$i].Cloud)'" + $sqlStatement += ", '$($jsonObjectSplitted[$j][$i].Category)', '$($jsonObjectSplitted[$j][$i].ImpactedArea)'" + $sqlStatement += ", '$($jsonObjectSplitted[$j][$i].Impact)', '$($jsonObjectSplitted[$j][$i].RecommendationType)'" + $sqlStatement += ", '$($jsonObjectSplitted[$j][$i].RecommendationSubType)', '$($jsonObjectSplitted[$j][$i].RecommendationSubTypeId)'" + $sqlStatement += ", '$($jsonObjectSplitted[$j][$i].RecommendationDescription)', '$($jsonObjectSplitted[$j][$i].RecommendationAction)'" + $sqlStatement += ", '$($jsonObjectSplitted[$j][$i].InstanceId)', '$($jsonObjectSplitted[$j][$i].InstanceName)', '$additionalInfoString'" + $sqlStatement += ", $resourceGroup, $subscriptionGuid, $subscriptionName, '$($jsonObjectSplitted[$j][$i].TenantGuid)'" + $sqlStatement += ", $($jsonObjectSplitted[$j][$i].FitScore), '$tagsString', '$($jsonObjectSplitted[$j][$i].DetailsURL)')" + if ($i -ne ($jsonObjectSplitted[$j].Count-1)) + { + $sqlStatement += "," + } + } + + $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;") + $Conn2.Open() + + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn2 + $Cmd.CommandText = $sqlStatement + $Cmd.CommandTimeout=120 + try + { + $Cmd.ExecuteReader() + } + catch + { + Write-Output "Failed statement: $sqlStatement" + throw + } + + $Conn2.Close() + + $linesProcessed += $currentObjectLines + Write-Output "Processed $linesProcessed lines..." + if ($j -eq ($jsonObjectSplitted.Count - 1)) { + $lastProcessedLine = -1 + } + else { + $lastProcessedLine = $linesProcessed - 1 + } + + $updatedLastProcessedLine = $lastProcessedLine + $updatedLastProcessedDateTime = $lastProcessedDateTime + if ($j -eq ($jsonObjectSplitted.Count - 1)) { + $updatedLastProcessedDateTime = $newProcessedTime + } + $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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandText = $sqlStatement + $Cmd.CommandTimeout=$SqlTimeout + $Cmd.ExecuteReader() + $Conn.Close() + } + else + { + $linesProcessed += $currentObjectLines + } + } + } + + Remove-Item -Path $blob.Name -Force +} + +Write-Output "DONE" \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1 new file mode 100644 index 000000000..2ebbe91fb --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1 @@ -0,0 +1,224 @@ +$ErrorActionPreference = "Stop" + +$cloudEnvironment = Get-AutomationVariable -Name "AzureOptimization_CloudEnvironment" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud +if ([string]::IsNullOrEmpty($cloudEnvironment)) +{ + $cloudEnvironment = "AzureCloud" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$sharedKey = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceKey" +$LogAnalyticsChunkSize = [int] (Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsChunkSize" -ErrorAction SilentlyContinue) +if (-not($LogAnalyticsChunkSize -gt 0)) +{ + $LogAnalyticsChunkSize = 6000 +} +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$SqlTimeout = 300 +$FiltersTable = "Filters" + +#region Functions + +# Function to create the authorization signature +Function Build-OMSSignature ($workspaceId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) { + $xHeaders = "x-ms-date:" + $date + $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource + $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) + $keyBytes = [Convert]::FromBase64String($sharedKey) + $sha256 = New-Object System.Security.Cryptography.HMACSHA256 + $sha256.Key = $keyBytes + $calculatedHash = $sha256.ComputeHash($bytesToHash) + $encodedHash = [Convert]::ToBase64String($calculatedHash) + $authorization = 'SharedKey {0}:{1}' -f $workspaceId, $encodedHash + return $authorization +} + +# Function to create and post the request +Function Post-OMSData($workspaceId, $sharedKey, $body, $logType, $TimeStampField, $AzureEnvironment) { + $method = "POST" + $contentType = "application/json" + $resource = "/api/logs" + $rfc1123date = [DateTime]::UtcNow.ToString("r") + $contentLength = $body.Length + $signature = Build-OMSSignature ` + -workspaceId $workspaceId ` + -sharedKey $sharedKey ` + -date $rfc1123date ` + -contentLength $contentLength ` + -method $method ` + -contentType $contentType ` + -resource $resource + + $uri = "https://" + $workspaceId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" + if ($AzureEnvironment -eq "AzureChinaCloud") + { + $uri = "https://" + $workspaceId + ".ods.opinsights.azure.cn" + $resource + "?api-version=2016-04-01" + } + if ($AzureEnvironment -eq "AzureUSGovernment") + { + $uri = "https://" + $workspaceId + ".ods.opinsights.azure.us" + $resource + "?api-version=2016-04-01" + } + if ($AzureEnvironment -eq "AzureGermanCloud") + { + throw "Azure Germany isn't suported for the Log Analytics Data Collector API" + } + + $OMSheaders = @{ + "Authorization" = $signature; + "Log-Type" = $logType; + "x-ms-date" = $rfc1123date; + "time-generated-field" = $TimeStampField; + } + + Try { + + $response = Invoke-WebRequest -Uri $uri -Method POST -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000 + } + catch { + if ($_.Exception.Response.StatusCode.Value__ -eq 401) { + "REAUTHENTICATING" + + $response = Invoke-WebRequest -Uri $uri -Method POST -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000 + } + else + { + return $_.Exception.Response.StatusCode.Value__ + } + } + + return $response.StatusCode +} +#endregion Functions + +Write-Output "Getting excluded recommendation sub-type IDs..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$FiltersTable] WHERE IsEnabled = 1 AND (FilterEndDate IS NULL OR FilterEndDate > GETDATE())" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $filters = New-Object System.Data.DataTable + $sqlAdapter.Fill($filters) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$Conn.Close() +$Conn.Dispose() + +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +$filterObjects = @() + +$filterObject = New-Object PSObject -Property @{ + Timestamp = $timestamp + FilterId = (New-Guid).Guid + RecommendationSubTypeId = [System.Guid]::empty.Guid + FilterType = "Dummy" + InstanceId = [System.Guid]::empty.Guid + InstanceName = "Dummy" + FilterStartDate = "2019-01-01T00:00:00.000Z" + FilterEndDate = "2199-12-31T23:59:59.000Z" + Author = "AOE" + Notes = "This is a dummy suppression required to build the full suppressions schema in Log Analytics" +} +$filterObjects += $filterObject + +foreach ($filter in $filters) +{ + $filterEndDate = $null + if (-not([string]::IsNullOrEmpty($filter.FilterEndDate))) + { + Write-Output $filter.FilterEndDate + $filterEndDate = $filter.FilterEndDate.ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + else + { + $filterEndDate = "2199-12-31T23:59:59.000Z" + } + + $filterStartDate = $null + if (-not([string]::IsNullOrEmpty($filter.FilterStartDate))) + { + $filterStartDate = $filter.FilterStartDate.ToString("yyyy-MM-ddTHH:mm:00.000Z") + } + else + { + $filterStartDate = "2019-01-01T00:00:00.000Z" + } + + $instanceId = $null + $instanceName = $null + $ObjectGuid = [System.Guid]::empty + if ([System.Guid]::TryParse($filter.InstanceId, [System.Management.Automation.PSReference]$ObjectGuid)) + { + $instanceId = $filter.InstanceId + } + else + { + $instanceName = $filter.InstanceId + } + + $filterObject = New-Object PSObject -Property @{ + Timestamp = $timestamp + FilterId = $filter.FilterId + RecommendationSubTypeId = $filter.RecommendationSubTypeId + FilterType = $filter.FilterType + InstanceId = $instanceId + InstanceName = $instanceName + FilterStartDate = $filterStartDate + FilterEndDate = $filterEndDate + Author = $filter.Author + Notes = $filter.Notes + } + $filterObjects += $filterObject +} + +$filtersJson = $filterObjects | ConvertTo-Json + +$LogAnalyticsSuffix = "SuppressionsV1" +$logname = $lognamePrefix + $LogAnalyticsSuffix + +$res = Post-OMSData -workspaceId $workspaceId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($filtersJson)) -logType $logname -TimeStampField "Timestamp" -AzureEnvironment $cloudEnvironment +If ($res -ge 200 -and $res -lt 300) { + Write-Output "Succesfully uploaded $($filterObjects.Count) $LogAnalyticsSuffix rows to Log Analytics" +} +Else { + Write-Warning "Failed to upload $($filterObjects.Count) $LogAnalyticsSuffix rows. Error code: $res" + throw +} diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1 new file mode 100644 index 000000000..00e81c592 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1 @@ -0,0 +1,369 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$expiringCredsDays = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendationAADMinCredValidityDays") +$notExpiringCredsDays = ([int] (Get-AutomationVariable -Name "AzureOptimization_RecommendationAADMaxCredValidityYears")) * 365 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('AADObjects')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$aadObjectsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AADObjects' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $aadObjectsTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 1 + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +$recommendationsErrors = 0 + +# Execute the expiring creds recommendation query against Log Analytics + +$baseQuery = @" + let expiryInterval = $($expiringCredsDays)d; + let AppsAndKeys = materialize ($aadObjectsTableName + | where TimeGenerated > ago(1d) + | where ObjectType_s in ('Application','ServicePrincipal') + | where ObjectSubType_s != 'ManagedIdentity' + | where Keys_s startswith '[' + | extend Keys = parse_json(Keys_s) + | project-away Keys_s + | mv-expand Keys + | evaluate bag_unpack(Keys) + | union ( + $aadObjectsTableName + | where TimeGenerated > ago(1d) + | where ObjectType_s in ('Application','ServicePrincipal') + | where ObjectSubType_s != 'ManagedIdentity' + | where isnotempty(Keys_s) and Keys_s !startswith '[' + | extend Keys = parse_json(Keys_s) + | project-away Keys_s + | evaluate bag_unpack(Keys) + ) + ); + let ExpirationInRisk = AppsAndKeys + | where EndDate < now()+expiryInterval + | project ApplicationId_g, KeyId, RiskDate = EndDate; + let NotInRisk = AppsAndKeys + | where EndDate > now()+expiryInterval + | project ApplicationId_g, KeyId, ComfortDate = EndDate; + let ApplicationsInRisk = ExpirationInRisk + | join kind=leftouter ( NotInRisk ) on ApplicationId_g + | where isempty(ComfortDate) + | summarize ExpiresOn = max(RiskDate) by ApplicationId_g; + AppsAndKeys + | join kind=inner (ApplicationsInRisk) on ApplicationId_g + | summarize ExpiresOn = max(EndDate) by ApplicationId_g, ObjectType_s, DisplayName_s, Cloud_s, KeyType, TenantGuid_g + | order by ExpiresOn desc +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.ApplicationId_g + $detailsURL = "https://portal.azure.$azureTld/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Credentials/appId/$queryInstanceId" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["ObjectType"] = $result.ObjectType_s + $additionalInfoDictionary["KeyType"] = $result.KeyType + $additionalInfoDictionary["ExpiresOn"] = $result.ExpiresOn + + $fitScore = 5 + + $tags = @{} + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "OperationalExcellence" + ImpactedArea = "Microsoft.AzureActiveDirectory/objects" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "AADExpiringCredentials" + RecommendationSubTypeId = "3292c489-2782-498b-aad0-a4cef50f6ca2" + RecommendationDescription = "Microsoft Entra application with credentials expired or about to expire" + RecommendationAction = "Update the Microsoft Entra application credential before the expiration date" + InstanceId = $result.ApplicationId_g + InstanceName = $result.DisplayName_s + AdditionalInfo = $additionalInfoDictionary + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "aadexpiringcerts-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +# Execute the not expiring in less than X years creds recommendation query against Log Analytics + +$baseQuery = @" + let expiryInterval = $($notExpiringCredsDays)d; + let AppsAndKeys = materialize ($aadObjectsTableName + | where TimeGenerated > ago(1d) + | where ObjectSubType_s != 'ManagedIdentity' + | where Keys_s startswith '[' + | extend Keys = parse_json(Keys_s) + | project-away Keys_s + | mv-expand Keys + | evaluate bag_unpack(Keys) + | union ( + $aadObjectsTableName + | where TimeGenerated > ago(1d) + | where ObjectSubType_s != 'ManagedIdentity' + | where isnotempty(Keys_s) and Keys_s !startswith '[' + | extend Keys = parse_json(Keys_s) + | project-away Keys_s + | evaluate bag_unpack(Keys) + ) + ); + AppsAndKeys + | where EndDate > now()+expiryInterval + | project ApplicationId_g, ObjectType_s, DisplayName_s, Cloud_s, KeyType, TenantGuid_g, EndDate +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.ApplicationId_g + $detailsURL = "https://portal.azure.$azureTld/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Credentials/appId/$queryInstanceId" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["ObjectType"] = $result.ObjectType_s + $additionalInfoDictionary["KeyType"] = $result.KeyType + $additionalInfoDictionary["ExpiresOn"] = $result.EndDate + + $fitScore = 5 + + $tags = @{} + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Security" + ImpactedArea = "Microsoft.AzureActiveDirectory/objects" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "AADNotExpiringCredentials" + RecommendationSubTypeId = "ecd969c8-3f16-481a-9577-5ed32e5e1a1d" + RecommendationDescription = "Microsoft Entra application with credentials expiration not set or too far in time" + RecommendationAction = "Update the Microsoft Entra application credential with a shorter expiration date" + InstanceId = $result.ApplicationId_g + InstanceName = $result.DisplayName_s + AdditionalInfo = $additionalInfoDictionary + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "aadnotexpiringcerts-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +if ($recommendationsErrors -gt 0) +{ + throw "Some of the recommendations queries failed. Please, review the job logs for additional information." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1 new file mode 100644 index 000000000..d959112c2 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1 @@ -0,0 +1,515 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$deploymentDate = Get-AutomationVariable -Name "AzureOptimization_DeploymentDate" # yyyy-MM-dd format +$deploymentDate = $deploymentDate.Replace('"', "") + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$assignmentsPercentageThresholdVar = Get-AutomationVariable -Name "AzureOptimization_RecommendationRBACAssignmentsPercentageThreshold" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($assignmentsPercentageThresholdVar) -or $assignmentsPercentageThresholdVar -eq 0) +{ + $assignmentsPercentageThreshold = 80 +} +else +{ + $assignmentsPercentageThreshold = [int] $assignmentsPercentageThresholdVar +} + +$assignmentsSubscriptionsLimitVar = Get-AutomationVariable -Name "AzureOptimization_RecommendationRBACSubscriptionsAssignmentsLimit" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($assignmentsSubscriptionsLimitVar) -or $assignmentsSubscriptionsLimitVar -eq 0) +{ + $assignmentsSubscriptionsLimit = 4000 +} +else +{ + $assignmentsSubscriptionsLimit = [int] $assignmentsSubscriptionsLimitVar +} + +$assignmentsMgmtGroupsLimitVar = Get-AutomationVariable -Name "AzureOptimization_RecommendationRBACMgmtGroupsAssignmentsLimit" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($assignmentsMgmtGroupsLimitVar) -or $assignmentsMgmtGroupsLimitVar -eq 0) +{ + $assignmentsMgmtGroupsLimit = 500 +} +else +{ + $assignmentsMgmtGroupsLimit = [int] $assignmentsMgmtGroupsLimitVar +} + +$rgPercentageThresholdVar = Get-AutomationVariable -Name "AzureOptimization_RecommendationResourceGroupsPerSubPercentageThreshold" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($rgPercentageThresholdVar) -or $rgPercentageThresholdVar -eq 0) +{ + $rgPercentageThreshold = 80 +} +else +{ + $rgPercentageThreshold = [int] $rgPercentageThresholdVar +} + +$rgLimitVar = Get-AutomationVariable -Name "AzureOptimization_RecommendationResourceGroupsPerSubLimit" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($rgLimitVar) -or $rgLimitVar -eq 0) +{ + $rgLimit = 980 +} +else +{ + $rgLimit = [int] $rgLimitVar +} + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('RBACAssignments','ARGResourceContainers')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$rbacTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'RBACAssignments' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $rbacTableName and $subscriptionsTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +$recommendationsErrors = 0 + +$assignmentsThreshold = $assignmentsSubscriptionsLimit * ($assignmentsPercentageThreshold / 100) + +Write-Output "Looking for subscriptions with more than $assignmentsPercentageThreshold% of the $assignmentsSubscriptionsLimit RBAC assignments limit..." + +$baseQuery = @" + $rbacTableName + | where TimeGenerated > ago(1d) and Model_s == 'AzureRM' and Scope_s startswith '/subscriptions/' + | extend SubscriptionGuid_g = tostring(split(Scope_s, '/')[2]) + | summarize AssignmentsCount=count() by SubscriptionGuid_g, TenantGuid_g, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s, Tags_s, InstanceId_s + ) on SubscriptionGuid_g + | where AssignmentsCount >= $assignmentsThreshold +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/users" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["assignmentsCount"] = $result.AssignmentsCount + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "OperationalExcellence" + ImpactedArea = "Microsoft.Resources/subscriptions" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "HighRBACAssignmentsSubscriptions" + RecommendationSubTypeId = "c6a88d8c-3242-44b0-9793-c91897ef68bc" + RecommendationDescription = "Subscriptions close to the maximum limit of RBAC assignments" + RecommendationAction = "Remove unneeded RBAC assignments or use group-based (or nested group-based) assignments" + InstanceId = $result.InstanceId_s + InstanceName = $result.SubscriptionName + AdditionalInfo = $additionalInfoDictionary + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "subscriptionsrbaclimits-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +$assignmentsThreshold = $assignmentsMgmtGroupsLimit * ($assignmentsPercentageThreshold / 100) + +Write-Output "Looking for management groups with more than $assignmentsPercentageThreshold% of the $assignmentsMgmtGroupsLimit RBAC assignments limit..." + +$baseQuery = @" + $rbacTableName + | where TimeGenerated > ago(1d) and Model_s == 'AzureRM' and Scope_s has 'managementGroups' + | extend ManagementGroupId = tostring(split(Scope_s, '/')[4]) + | summarize AssignmentsCount=count() by ManagementGroupId, TenantGuid_g, Scope_s, Cloud_s + | where AssignmentsCount >= $assignmentsThreshold +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/blade/Microsoft_Azure_ManagementGroups/ManagementGroupBrowseBlade/MGBrowse_overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["assignmentsCount"] = $result.AssignmentsCount + + $fitScore = 5 + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "OperationalExcellence" + ImpactedArea = "Microsoft.Management/managementGroups" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "HighRBACAssignmentsManagementGroups" + RecommendationSubTypeId = "b36dea3e-ef21-45a9-a704-6f629fab236d" + RecommendationDescription = "Management Groups close to the maximum limit of RBAC assignments" + RecommendationAction = "Remove unneeded RBAC assignments or use group-based (or nested group-based) assignments" + InstanceId = $result.Scope_s + InstanceName = $result.ManagementGroupId + AdditionalInfo = $additionalInfoDictionary + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "mgmtgroupsrbaclimits-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +$rgThreshold = $rgLimit * ($rgPercentageThreshold / 100) + +Write-Output "Looking for subscriptions with more than $rgPercentageThreshold% of the $rgLimit Resource Groups limit..." + +$baseQuery = @" + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions/resourceGroups' + | summarize RGCount=count() by SubscriptionGuid_g, TenantGuid_g, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s, Tags_s, InstanceId_s + ) on SubscriptionGuid_g + | where RGCount >= $rgThreshold +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/resourceGroups" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["resourceGroupsCount"] = $result.RGCount + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "OperationalExcellence" + ImpactedArea = "Microsoft.Resources/subscriptions" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "HighResourceGroupCountSubscriptions" + RecommendationSubTypeId = "4468da8d-1e72-4998-b6d2-3bc38ddd9330" + RecommendationDescription = "Subscriptions close to the maximum limit of resource groups" + RecommendationAction = "Remove unneeded resource groups or split your resource groups across multiple subscriptions" + InstanceId = $result.InstanceId_s + InstanceName = $result.SubscriptionName + AdditionalInfo = $additionalInfoDictionary + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "subscriptionsrglimits-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +if ($recommendationsErrors -gt 0) +{ + throw "Some of the recommendations queries failed. Please, review the job logs for additional information." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1 new file mode 100644 index 000000000..2b1de2d5d --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1 @@ -0,0 +1,311 @@ +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +# must be less than or equal to the advisor exports frequency +$daysBackwards = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendAdvisorPeriodInDays" -ErrorAction SilentlyContinue) +if (-not($daysBackwards -gt 0)) { + $daysBackwards = 7 +} + +$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" +} + +$CategoryFilter = Get-AutomationVariable -Name "AzureOptimization_AdvisorFilter" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($CategoryFilter)) +{ + $CategoryFilter = "HighAvailability,Security,Performance,OperationalExcellence" # comma-separated list of categories +} + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" +$FiltersTable = "Filters" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGVirtualMachine','AzureAdvisor','ARGResourceContainers')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$advisorTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureAdvisor' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $subscriptionsTableName and $advisorTableName" + +$Conn.Close() +$Conn.Dispose() + +Write-Output "Getting excluded recommendation sub-type IDs..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$FiltersTable] WHERE FilterType = 'Exclude' AND IsEnabled = 1 AND (FilterEndDate IS NULL OR FilterEndDate > GETDATE())" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $filters = New-Object System.Data.DataTable + $sqlAdapter.Fill($filters) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$Conn.Close() +$Conn.Dispose() + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +# Execute the recommendation query against Log Analytics + +$FinalCategoryFilter = "" + +if (-not([string]::IsNullOrEmpty($CategoryFilter))) +{ + $categories = $CategoryFilter.Split(',') + for ($i = 0; $i -lt $categories.Count; $i++) + { + $categories[$i] = "'" + $categories[$i] + "'" + } + $FinalCategoryFilter = " and Category in (" + ($categories -join ",") + ")" +} + +$baseQuery = @" +let advisorInterval = $($daysBackwards)d; +$advisorTableName +| where todatetime(TimeGenerated) > ago(advisorInterval)$FinalCategoryFilter +| extend AdvisorRecIdIndex = indexof(InstanceId_s, '/providers/microsoft.advisor/recommendations') +| extend InstanceName_s = iif(isnotempty(InstanceName_s),InstanceName_s,iif(AdvisorRecIdIndex > 0, split(substring(InstanceId_s, 0, AdvisorRecIdIndex),'/')[-1], split(InstanceId_s,'/')[-1])) +| summarize by InstanceId_s, InstanceName_s, Category, Description_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroup, Cloud_s, AdditionalInfo_s, RecommendationText_s, ImpactedArea_s, Impact_s, RecommendationTypeId_g, Tags_s +| join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s +) on SubscriptionGuid_g +"@ + +Write-Output "Getting $CategoryFilter recommendations for $($daysBackwards)d Advisor..." + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $daysBackwards) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + throw "Execution aborted" +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +Write-Output "Generating fit score..." + +foreach ($result in $results) { + + if ($filters | Where-Object { $_.RecommendationSubTypeId -eq $result.RecommendationTypeId_g}) + { + continue + } + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $additionalInfoDictionary = @{} + if (-not([string]::IsNullOrEmpty($result.AdditionalInfo_s))) + { + ($result.AdditionalInfo_s | ConvertFrom-Json).PsObject.Properties | ForEach-Object { $additionalInfoDictionary[$_.Name] = $_.Value } + } + + $fitScore = 5 + + $queryInstanceId = $result.InstanceId_s + + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $recommendationSubType = "Advisor" + $result.Category + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = $result.Category + ImpactedArea = $result.ImpactedArea_s + Impact = $result.Impact_s + RecommendationType = "BestPractices" + RecommendationSubType = $recommendationSubType + RecommendationSubTypeId = $result.RecommendationTypeId_g + RecommendationDescription = $result.Description_s.Replace("'","") + RecommendationAction = $result.RecommendationText_s.Replace("'","") + InstanceId = $result.InstanceId_s + InstanceName = $result.InstanceName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroup + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +Write-Output "Exporting final $($recommendations.Count) results as a JSON file..." + +$fileDate = $datetime.ToString("yyyyMMdd") +$jsonExportPath = "advisor-asis-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +Write-Output "Uploading $jsonExportPath to blob storage..." + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json" }; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1 new file mode 100644 index 000000000..fd3d91b12 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1 @@ -0,0 +1,899 @@ +$ErrorActionPreference = "Stop" + +function Find-SkuHourlyPrice { + param ( + [object[]] $SKUPriceSheet, + [string] $SKUName + ) + + $skuPriceObject = $null + + if ($SKUPriceSheet) + { + $skuNameParts = $SKUName.Split('_') + + if ($skuNameParts.Count -eq 3) # e.g., Standard_D1_v2 + { + $skuNameFilter = "*" + $skuNameParts[1] + " *" + $skuVersionFilter = "*" + $skuNameParts[2] + $skuPrices = $SKUPriceSheet | Where-Object { $_.MeterName_s -like $skuNameFilter ` + -and $_.MeterName_s -notlike '*Low Priority' -and $_.MeterName_s -notlike '*Expired' ` + -and $_.MeterName_s -like $skuVersionFilter -and $_.MeterSubCategory_s -notlike '*Windows' -and $_.UnitPrice_s -ne 0 } + + if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2) + { + $skuPriceObject = $skuPrices[0] + } + if ($skuPrices.Count -gt 2) # D1-like scenarios + { + $skuFilter = "*" + $skuNameParts[1] + " " + $skuNameParts[2] + "*" + $skuPrices = $skuPrices | Where-Object { $_.MeterName_s -like $skuFilter } + + if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2) + { + $skuPriceObject = $skuPrices[0] + } + } + } + + if ($skuNameParts.Count -eq 2) # e.g., Standard_D1 + { + $skuNameFilter = "*" + $skuNameParts[1] + "*" + + $skuPrices = $SKUPriceSheet | Where-Object { $_.MeterName_s -like $skuNameFilter ` + -and $_.MeterName_s -notlike '*Low Priority' -and $_.MeterName_s -notlike '*Expired' ` + -and $_.MeterName_s -notlike '* v*' -and $_.MeterSubCategory_s -notlike '*Windows' -and $_.UnitPrice_s -ne 0 } + + if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2) + { + $skuPriceObject = $skuPrices[0] + } + if ($skuPrices.Count -gt 2) # D1-like scenarios + { + $skuFilterLeft = "*" + $skuNameParts[1] + "/*" + $skuFilterRight = "*/" + $skuNameParts[1] + "*" + $skuPrices = $skuPrices | Where-Object { $_.MeterName_s -like $skuFilterLeft -or $_.MeterName_s -like $skuFilterRight } + + if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2) + { + $skuPriceObject = $skuPrices[0] + } + } + } + } + + $targetHourlyPrice = [double]::MaxValue + if ($null -ne $skuPriceObject) + { + $targetUnitHours = [int] (Select-String -InputObject $skuPriceObject.UnitOfMeasure_s -Pattern "^\d+").Matches[0].Value + if ($targetUnitHours -gt 0) + { + $targetHourlyPrice = [double] ($skuPriceObject.UnitPrice_s / $targetUnitHours) + } + } + + return $targetHourlyPrice +} + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" + +# must be less than or equal to the advisor exports frequency +$daysBackwards = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendAdvisorPeriodInDays" -ErrorAction SilentlyContinue) +if (-not($daysBackwards -gt 0)) { + $daysBackwards = 7 +} + +$perfDaysBackwards = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendPerfPeriodInDays" -ErrorAction SilentlyContinue) +if (-not($perfDaysBackwards -gt 0)) { + $perfDaysBackwards = 7 +} + +$perfTimeGrain = Get-AutomationVariable -Name "AzureOptimization_RecommendPerfTimeGrain" -ErrorAction SilentlyContinue +if (-not($perfTimeGrain)) { + $perfTimeGrain = "1h" +} + +# percentiles variables +$cpuPercentile = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfPercentileCpu" -ErrorAction SilentlyContinue) +if (-not($cpuPercentile -gt 0)) { + $cpuPercentile = 99 +} +$memoryPercentile = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfPercentileMemory" -ErrorAction SilentlyContinue) +if (-not($memoryPercentile -gt 0)) { + $memoryPercentile = 99 +} +$networkPercentile = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfPercentileNetwork" -ErrorAction SilentlyContinue) +if (-not($networkPercentile -gt 0)) { + $networkPercentile = 99 +} +$diskPercentile = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfPercentileDisk" -ErrorAction SilentlyContinue) +if (-not($diskPercentile -gt 0)) { + $diskPercentile = 99 +} + +# perf thresholds variables +$cpuPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdCpuPercentage" -ErrorAction SilentlyContinue) +if (-not($cpuPercentageThreshold -gt 0)) { + $cpuPercentageThreshold = 30 +} +$memoryPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdMemoryPercentage" -ErrorAction SilentlyContinue) +if (-not($memoryPercentageThreshold -gt 0)) { + $memoryPercentageThreshold = 50 +} +$networkMpbsThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdNetworkMbps" -ErrorAction SilentlyContinue) +if (-not($networkMpbsThreshold -gt 0)) { + $networkMpbsThreshold = 750 +} + +# perf thresholds variables (shutdown) +$cpuPercentageShutdownThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdCpuShutdownPercentage" -ErrorAction SilentlyContinue) +if (-not($cpuPercentageShutdownThreshold -gt 0)) { + $cpuPercentageShutdownThreshold = 5 +} +$memoryPercentageShutdownThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdMemoryShutdownPercentage" -ErrorAction SilentlyContinue) +if (-not($memoryPercentageShutdownThreshold -gt 0)) { + $memoryPercentageShutdownThreshold = 100 +} +$networkMpbsShutdownThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdNetworkShutdownMbps" -ErrorAction SilentlyContinue ) +if (-not($networkMpbsShutdownThreshold -gt 0)) { + $networkMpbsShutdownThreshold = 10 +} + +$rightSizeRecommendationId = Get-AutomationVariable -Name "AzureOptimization_RecommendationAdvisorCostRightSizeId" -ErrorAction SilentlyContinue +if (-not($rightSizeRecommendationId)) { + $rightSizeRecommendationId = 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974' +} + +$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)) +{ + $sqldatabase = "azureoptimization" +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" +$FiltersTable = "Filters" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGVirtualMachine','AzureAdvisor','AzureConsumption','ARGResourceContainers','Pricesheet')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$vmsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVirtualMachine' }).LogAnalyticsSuffix + "_CL" +$advisorTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureAdvisor' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" +$pricesheetTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'Pricesheet' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $vmsTableName, $subscriptionsTableName, $advisorTableName, $pricesheetTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +Write-Output "Getting excluded recommendation sub-type IDs..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$FiltersTable] WHERE FilterType = 'Exclude' AND IsEnabled = 1 AND (FilterEndDate IS NULL OR FilterEndDate > GETDATE())" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $filters = New-Object System.Data.DataTable + $sqlAdapter.Fill($filters) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + + +Write-Output "Getting Virtual Machine SKUs for the $referenceRegion region..." +# Get all the VM SKUs information for the reference Azure region +$skus = Get-AzComputeResourceSku -Location $referenceRegion | Where-Object { $_.ResourceType -eq "virtualMachines" } + +Write-Output "Getting the current Pricesheet..." + +if ($cloudEnvironment -eq "AzureCloud") +{ + $pricesheetRegion = "EU West" +} + +try +{ + $pricesheetEntries = @() + + $baseQuery = @" + $pricesheetTableName + | where TimeGenerated > ago(14d) + | where MeterCategory_s == 'Virtual Machines' and MeterRegion_s == '$pricesheetRegion' and PriceType_s == 'Consumption' + | distinct MeterName_s, MeterSubCategory_s, MeterCategory_s, MeterRegion_s, UnitPrice_s, UnitOfMeasure_s +"@ + + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days 14) -Wait 600 -IncludeStatistics + $pricesheetEntries = [System.Linq.Enumerable]::ToArray($queryResults.Results) + + Write-Output "Query finished with $($pricesheetEntries.Count) results." + Write-Output "Query statistics: $($queryResults.Statistics.query)" +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + Write-Output "Consumption pricesheet not available, will estimate savings based in cores count..." +} + +$linuxMemoryPerfAdditionalWorkspaces = "" +$windowsMemoryPerfAdditionalWorkspaces = "" +$processorPerfAdditionalWorkspaces = "" +$windowsNetworkPerfAdditionalWorkspaces = "" +$diskPerfAdditionalWorkspaces = "" +if ($additionalPerfWorkspaces) +{ + $additionalWorkspaces = $additionalPerfWorkspaces.Split(",") + foreach ($additionalWorkspace in $additionalWorkspaces) { + $additionalWorkspace = $additionalWorkspace.Trim() + $linuxMemoryPerfAdditionalWorkspaces += @" + | union ( workspace('$additionalWorkspace').Perf + | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) + | where CounterName == '% Used Memory' + | extend WorkspaceId = TenantId + | summarize hint.strategy=shuffle PMemoryPercentage = percentile(CounterValue, memoryPercentileValue) by _ResourceId, WorkspaceId) +"@ + $windowsMemoryPerfAdditionalWorkspaces += @" + | union ( workspace('$additionalWorkspace').Perf + | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) + | where CounterName == 'Available MBytes' + | extend WorkspaceId = TenantId + | project TimeGenerated, MemoryAvailableMBs = CounterValue, _ResourceId, WorkspaceId) +"@ + $processorPerfAdditionalWorkspaces += @" + | union ( workspace('$additionalWorkspace').Perf + | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) + | where ObjectName == 'Processor' and CounterName == '% Processor Time' and InstanceName == '_Total' + | extend WorkspaceId = TenantId + | summarize hint.strategy=shuffle PCPUPercentage = percentile(CounterValue, cpuPercentileValue) by _ResourceId, WorkspaceId) +"@ + $windowsNetworkPerfAdditionalWorkspaces += @" + | union ( workspace('$additionalWorkspace').Perf + | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) + | where CounterName == 'Bytes Total/sec' + | extend WorkspaceId = TenantId + | summarize hint.strategy=shuffle PCounter = percentile(CounterValue, networkPercentileValue) by InstanceName, _ResourceId, WorkspaceId + | summarize PNetwork = sum(PCounter) by _ResourceId, WorkspaceId) +"@ + $diskPerfAdditionalWorkspaces += @" + | union ( workspace('$additionalWorkspace').Perf + | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) + | where CounterName in ('Disk Reads/sec', 'Disk Writes/sec', 'Disk Read Bytes/sec', 'Disk Write Bytes/sec') and InstanceName !in ('_Total', 'D:', '/mnt/resource', '/mnt') + | extend WorkspaceId = TenantId + | summarize hint.strategy=shuffle PCounter = percentile(CounterValue, diskPercentileValue) by bin(TimeGenerated, perfTimeGrain), CounterName, InstanceName, _ResourceId, WorkspaceId + | summarize SumPCounter = sum(PCounter) by CounterName, TimeGenerated, _ResourceId, WorkspaceId + | summarize MaxPReadIOPS = maxif(SumPCounter, CounterName == 'Disk Reads/sec'), + MaxPWriteIOPS = maxif(SumPCounter, CounterName == 'Disk Writes/sec'), + MaxPReadMiBps = (maxif(SumPCounter, CounterName == 'Disk Read Bytes/sec') / 1024 / 1024), + MaxPWriteMiBps = (maxif(SumPCounter, CounterName == 'Disk Write Bytes/sec') / 1024 / 1024) by _ResourceId, WorkspaceId) +"@ + } +} + +# Execute the recommendation query against Log Analytics + +$baseQuery = @" +let advisorInterval = $($daysBackwards)d; +let perfInterval = $($perfDaysBackwards)d; +let perfTimeGrain = $perfTimeGrain; +let cpuPercentileValue = $cpuPercentile; +let memoryPercentileValue = $memoryPercentile; +let networkPercentileValue = $networkPercentile; +let diskPercentileValue = $diskPercentile; +let rightSizeRecommendationId = '$rightSizeRecommendationId'; +let billingInterval = 30d; +let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(30d) | summarize max(todatetime(Date_s)))); +let stime = etime-billingInterval; +let RightSizeInstanceIds = materialize($advisorTableName +| where todatetime(TimeGenerated) > ago(advisorInterval) and Category == 'Cost' and RecommendationTypeId_g == rightSizeRecommendationId +| distinct InstanceId_s); +let LinuxMemoryPerf = Perf +| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) +| where CounterName == '% Used Memory' +| extend WorkspaceId = TenantId +| summarize hint.strategy=shuffle PMemoryPercentage = percentile(CounterValue, memoryPercentileValue) by _ResourceId, WorkspaceId$linuxMemoryPerfAdditionalWorkspaces; +let WindowsMemoryPerf = Perf +| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) +| where CounterName == 'Available MBytes' +| extend WorkspaceId = TenantId +| project TimeGenerated, MemoryAvailableMBs = CounterValue, _ResourceId, WorkspaceId$windowsMemoryPerfAdditionalWorkspaces; +let MemoryPerf = $vmsTableName +| where TimeGenerated > ago(1d) +| distinct InstanceId_s, MemoryMB_s +| join kind=inner hint.strategy=broadcast ( + WindowsMemoryPerf +) on `$left.InstanceId_s == `$right._ResourceId +| extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 +| summarize hint.strategy=shuffle PMemoryPercentage = percentile(MemoryPercentage, memoryPercentileValue) by _ResourceId, WorkspaceId +| union LinuxMemoryPerf; +let ProcessorPerf = Perf +| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) +| where ObjectName == 'Processor' and CounterName == '% Processor Time' and InstanceName == '_Total' +| extend WorkspaceId = TenantId +| summarize hint.strategy=shuffle PCPUPercentage = percentile(CounterValue, cpuPercentileValue) by _ResourceId, WorkspaceId$processorPerfAdditionalWorkspaces; +let WindowsNetworkPerf = Perf +| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) +| where CounterName == 'Bytes Total/sec' +| extend WorkspaceId = TenantId +| summarize hint.strategy=shuffle PCounter = percentile(CounterValue, networkPercentileValue) by InstanceName, _ResourceId, WorkspaceId +| summarize PNetwork = sum(PCounter) by _ResourceId, WorkspaceId$windowsNetworkPerfAdditionalWorkspaces; +let DiskPerf = Perf +| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) +| where CounterName in ('Disk Reads/sec', 'Disk Writes/sec', 'Disk Read Bytes/sec', 'Disk Write Bytes/sec') and InstanceName !in ('_Total', 'D:', '/mnt/resource', '/mnt') +| extend WorkspaceId = TenantId +| summarize hint.strategy=shuffle PCounter = percentile(CounterValue, diskPercentileValue) by bin(TimeGenerated, perfTimeGrain), CounterName, InstanceName, _ResourceId, WorkspaceId +| summarize SumPCounter = sum(PCounter) by CounterName, TimeGenerated, _ResourceId, WorkspaceId +| summarize MaxPReadIOPS = maxif(SumPCounter, CounterName == 'Disk Reads/sec'), + MaxPWriteIOPS = maxif(SumPCounter, CounterName == 'Disk Writes/sec'), + MaxPReadMiBps = (maxif(SumPCounter, CounterName == 'Disk Read Bytes/sec') / 1024 / 1024), + MaxPWriteMiBps = (maxif(SumPCounter, CounterName == 'Disk Write Bytes/sec') / 1024 / 1024) by _ResourceId, WorkspaceId$diskPerfAdditionalWorkspaces; +$advisorTableName +| where todatetime(TimeGenerated) > ago(advisorInterval) and Category == 'Cost' +| extend AdvisorRecIdIndex = indexof(InstanceId_s, '/providers/microsoft.advisor/recommendations') +| extend InstanceName_s = iif(isnotempty(InstanceName_s),InstanceName_s,iif(AdvisorRecIdIndex > 0, split(substring(InstanceId_s, 0, AdvisorRecIdIndex),'/')[-1], split(InstanceId_s,'/')[-1])) +| distinct InstanceId_s, InstanceName_s, Description_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroup, Cloud_s, AdditionalInfo_s, RecommendationText_s, ImpactedArea_s, Impact_s, RecommendationTypeId_g, Tags_s +| join kind=leftouter ( + $consumptionTableName + | where todatetime(Date_s) between (stime..etime) + | extend VMConsumedQuantity = iif(ResourceId contains 'virtualmachines' and MeterCategory_s == 'Virtual Machines', todouble(Quantity_s), 0.0) + | extend VMPrice = iif(ResourceId contains 'virtualmachines' and MeterCategory_s == 'Virtual Machines', todouble(EffectivePrice_s), 0.0) + | extend FinalCost = iif(ResourceId contains 'virtualmachines', VMPrice * VMConsumedQuantity, todouble(CostInBillingCurrency_s)) + | extend InstanceId_s = tolower(ResourceId) + | summarize Last30DaysCost = sum(FinalCost), Last30DaysQuantity = sum(VMConsumedQuantity) by InstanceId_s +) on InstanceId_s +| join kind=leftouter ( + $vmsTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, NicCount_s, DataDiskCount_s +) on InstanceId_s +| where RecommendationTypeId_g != rightSizeRecommendationId or (RecommendationTypeId_g == rightSizeRecommendationId and toint(NicCount_s) >= 0 and toint(DataDiskCount_s) >= 0) +| join kind=leftouter hint.strategy=broadcast ( MemoryPerf ) on `$left.InstanceId_s == `$right._ResourceId +| join kind=leftouter hint.strategy=broadcast ( ProcessorPerf ) on `$left.InstanceId_s == `$right._ResourceId +| join kind=leftouter hint.strategy=broadcast ( WindowsNetworkPerf ) on `$left.InstanceId_s == `$right._ResourceId +| join kind=leftouter hint.strategy=broadcast ( DiskPerf ) on `$left.InstanceId_s == `$right._ResourceId +| extend MaxPIOPS = MaxPReadIOPS + MaxPWriteIOPS, MaxPMiBps = MaxPReadMiBps + MaxPWriteMiBps +| extend PNetworkMbps = PNetwork * 8 / 1000 / 1000 +| distinct Last30DaysCost, Last30DaysQuantity, InstanceId_s, InstanceName_s, Description_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroup, Cloud_s, AdditionalInfo_s, RecommendationText_s, ImpactedArea_s, Impact_s, RecommendationTypeId_g, NicCount_s, DataDiskCount_s, PMemoryPercentage, PCPUPercentage, PNetworkMbps, MaxPIOPS, MaxPMiBps, Tags_s, WorkspaceId +| join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s +) on SubscriptionGuid_g +"@ + +Write-Output "Will run the following query (use this query against the LA workspace for troubleshooting): $baseQuery" + +Write-Output "Getting cost recommendations for $($daysBackwards)d Advisor and $($perfDaysBackwards)d Perf history and a $perfTimeGrain time grain..." + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + throw "Execution aborted" +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +$skuPricesFound = @{} + +Write-Output "Generating fit score..." + +foreach ($result in $results) { + + if ($filters | Where-Object { $_.RecommendationSubTypeId -eq $result.RecommendationTypeId_g}) + { + continue + } + + $queryInstanceId = $result.InstanceId_s + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $additionalInfoDictionary = @{} + if (-not([string]::IsNullOrEmpty($result.AdditionalInfo_s))) + { + ($result.AdditionalInfo_s | ConvertFrom-Json).PsObject.Properties | ForEach-Object { $additionalInfoDictionary[$_.Name] = $_.Value } + } + + # Fixing reservation model inconsistencies + if (-not([string]::IsNullOrEmpty($additionalInfoDictionary["location"]))) + { + $additionalInfoDictionary["region"] = $additionalInfoDictionary["location"] + } + if (-not([string]::IsNullOrEmpty($additionalInfoDictionary["targetResourceCount"]))) + { + $additionalInfoDictionary["qty"] = $additionalInfoDictionary["targetResourceCount"] + } + if (-not([string]::IsNullOrEmpty($additionalInfoDictionary["vmSize"]))) + { + $additionalInfoDictionary["displaySKU"] = $additionalInfoDictionary["vmSize"] + } + + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + + $fitScore = 5 + $hasCpuRamPerfMetrics = $false + + if ($additionalInfoDictionary.targetSku -and $result.RecommendationTypeId_g -eq $rightSizeRecommendationId) { + $additionalInfoDictionary["SupportsDataDisksCount"] = "true" + $additionalInfoDictionary["DataDiskCount"] = "$($result.DataDiskCount_s)" + $additionalInfoDictionary["SupportsNICCount"] = "true" + $additionalInfoDictionary["NicCount"] = "$($result.NicCount_s)" + $additionalInfoDictionary["SupportsIOPS"] = "true" + $additionalInfoDictionary["MetricIOPS"] = "$($result.MaxPIOPS)" + $additionalInfoDictionary["SupportsMiBps"] = "true" + $additionalInfoDictionary["MetricMiBps"] = "$($result.MaxPMiBps)" + $additionalInfoDictionary["BelowCPUThreshold"] = "true" + $additionalInfoDictionary["MetricCPUPercentage"] = "$($result.PCPUPercentage)" + $additionalInfoDictionary["BelowMemoryThreshold"] = "true" + $additionalInfoDictionary["MetricMemoryPercentage"] = "$($result.PMemoryPercentage)" + $additionalInfoDictionary["BelowNetworkThreshold"] = "true" + $additionalInfoDictionary["MetricNetworkMbps"] = "$($result.PNetworkMbps)" + + $targetSku = $null + if ($additionalInfoDictionary.targetSku -ne "Shutdown") { + $currentSku = $skus | Where-Object { $_.Name -eq $additionalInfoDictionary.currentSku } + $currentSkuvCPUs = [int]($currentSku.Capabilities | Where-Object { $_.Name -eq 'vCPUsAvailable' }).Value + $targetSku = $skus | Where-Object { $_.Name -eq $additionalInfoDictionary.targetSku } + $targetSkuvCPUs = [int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'vCPUsAvailable' }).Value + $targetMaxDataDiskCount = [int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'MaxDataDiskCount' }).Value + if ($targetMaxDataDiskCount -gt 0) { + if (-not([string]::isNullOrEmpty($result.DataDiskCount_s))) { + if ([int]$result.DataDiskCount_s -gt $targetMaxDataDiskCount) { + $fitScore = 1 + $additionalInfoDictionary["SupportsDataDisksCount"] = "false:needs$($result.DataDiskCount_s)-max$targetMaxDataDiskCount" + } + } + else { + $fitScore -= 1 + $additionalInfoDictionary["SupportsDataDisksCount"] = "unknown:max$targetMaxDataDiskCount" + } + } + else { + $fitScore -= 1 + $additionalInfoDictionary["SupportsDataDisksCount"] = "unknown:needs$($result.DataDiskCount_s)" + } + $targetMaxNICCount = [int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'MaxNetworkInterfaces' }).Value + if ($targetMaxNICCount -gt 0) { + if (-not([string]::isNullOrEmpty($result.NicCount_s))) { + if ([int]$result.NicCount_s -gt $targetMaxNICCount) { + $fitScore = 1 + $additionalInfoDictionary["SupportsNICCount"] = "false:needs$($result.NicCount_s)-max$targetMaxNICCount" + } + } + else { + $fitScore -= 1 + $additionalInfoDictionary["SupportsNICCount"] = "unknown:max$targetMaxNICCount" + } + } + else { + $fitScore -= 1 + $additionalInfoDictionary["SupportsNICCount"] = "unknown:needs$($result.NicCount_s)" + } + $targetUncachedDiskIOPS = [int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'UncachedDiskIOPS' }).Value + if ($targetUncachedDiskIOPS -gt 0) { + if (-not([string]::isNullOrEmpty($result.MaxPIOPS))) { + if ([double]$result.MaxPIOPS -ge [double]$targetUncachedDiskIOPS) { + $fitScore -= 1 + $additionalInfoDictionary["SupportsIOPS"] = "false:needs$($result.MaxPIOPS)-max$targetUncachedDiskIOPS" + } + } + else { + $fitScore -= 0.5 + $additionalInfoDictionary["SupportsIOPS"] = "unknown:max$targetUncachedDiskIOPS" + } + } + else { + $fitScore -= 1 + $additionalInfoDictionary["SupportsIOPS"] = "unknown:needs$($result.MaxPIOPS)" + } + $targetUncachedDiskMiBps = [double]([int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'UncachedDiskBytesPerSecond' }).Value) / 1024 / 1024 + if ($targetUncachedDiskMiBps -gt 0) { + if (-not([string]::isNullOrEmpty($result.MaxPMiBps))) { + if ([double]$result.MaxPMiBps -ge $targetUncachedDiskMiBps) { + $fitScore -= 1 + $additionalInfoDictionary["SupportsMiBps"] = "false:needs$($result.MaxPMiBps)-max$targetUncachedDiskMiBps" + } + } + else { + $fitScore -= 0.5 + $additionalInfoDictionary["SupportsMiBps"] = "unknown:max$targetUncachedDiskMiBps" + } + } + else { + $additionalInfoDictionary["SupportsMiBps"] = "unknown:needs$($result.MaxPMiBps)" + } + + $savingCoefficient = [double] $currentSkuvCPUs / $targetSkuvCPUs + + if ($savingCoefficient -gt 1) + { + $targetSkuSavingsMonthly = [double]$result.Last30DaysCost - ([double]$result.Last30DaysCost / $savingCoefficient) + } + else + { + $targetSkuSavingsMonthly = [double]$result.Last30DaysCost / 2 + } + + if ($targetSku -and $null -eq $skuPricesFound[$targetSku.Name]) + { + $skuPricesFound[$targetSku.Name] = Find-SkuHourlyPrice -SKUName $targetSku.Name -SKUPriceSheet $pricesheetEntries + } + + $tentativeTargetSkuSavingsMonthly = -1 + + if ($targetSku -and $skuPricesFound[$targetSku.Name] -gt 0 -and $skuPricesFound[$targetSku.Name] -lt [double]::MaxValue) + { + $targetSkuPrice = $skuPricesFound[$targetSku.Name] + + if ($null -eq $skuPricesFound[$currentSku.Name]) + { + $skuPricesFound[$currentSku.Name] = Find-SkuHourlyPrice -SKUName $currentSku.Name -SKUPriceSheet $pricesheetEntries + } + + if ($skuPricesFound[$currentSku.Name] -gt 0) + { + $currentSkuPrice = $skuPricesFound[$currentSku.Name] + $tentativeTargetSkuSavingsMonthly = ($currentSkuPrice * [double] $result.Last30DaysQuantity) - ($targetSkuPrice * [double] $result.Last30DaysQuantity) + } + else + { + $tentativeTargetSkuSavingsMonthly = [double]$result.Last30DaysCost - ($targetSkuPrice * [double] $result.Last30DaysQuantity) + } + } + + if ($tentativeTargetSkuSavingsMonthly -ge 0) + { + $targetSkuSavingsMonthly = $tentativeTargetSkuSavingsMonthly + } + + if ($targetSkuSavingsMonthly -eq [double]::PositiveInfinity) + { + $targetSkuSavingsMonthly = [double] $result.Last30DaysCost / 2 + } + + $savingsMonthly = $targetSkuSavingsMonthly + + } + else + { + $savingsMonthly = [double]$result.Last30DaysCost + } + + $cpuThreshold = $cpuPercentageThreshold + $memoryThreshold = $memoryPercentageThreshold + $networkThreshold = $networkMpbsThreshold + if ($additionalInfoDictionary.targetSku -eq "Shutdown") { + $cpuThreshold = $cpuPercentageShutdownThreshold + $memoryThreshold = $memoryPercentageShutdownThreshold + $networkThreshold = $networkMpbsShutdownThreshold + } + + if (-not([string]::isNullOrEmpty($result.PCPUPercentage))) { + if ([double]$result.PCPUPercentage -ge [double]$cpuThreshold) { + $fitScore -= 0.5 + $additionalInfoDictionary["BelowCPUThreshold"] = "false:needs$($result.PCPUPercentage)-max$cpuThreshold" + } + $hasCpuRamPerfMetrics = $true + } + else { + $fitScore -= 0.5 + $additionalInfoDictionary["BelowCPUThreshold"] = "unknown:max$cpuThreshold" + } + if (-not([string]::isNullOrEmpty($result.PMemoryPercentage))) { + if ([double]$result.PMemoryPercentage -ge [double]$memoryThreshold) { + $fitScore -= 0.5 + $additionalInfoDictionary["BelowMemoryThreshold"] = "false:needs$($result.PMemoryPercentage)-max$memoryThreshold" + } + $hasCpuRamPerfMetrics = $true + } + else { + $fitScore -= 0.5 + $additionalInfoDictionary["BelowMemoryThreshold"] = "unknown:max$memoryThreshold" + } + if (-not([string]::isNullOrEmpty($result.PNetworkMbps))) { + if ([double]$result.PNetworkMbps -ge [double]$networkThreshold) { + $fitScore -= 0.1 + $additionalInfoDictionary["BelowNetworkThreshold"] = "false:needs$($result.PNetworkMbps)-max$networkThreshold" + } + } + else { + $fitScore -= 0.1 + $additionalInfoDictionary["BelowNetworkThreshold"] = "unknown:max$networkThreshold" + } + + $fitScore = [Math]::max(0.0, $fitScore) + } + else + { + if (-not([string]::IsNullOrEmpty($additionalInfoDictionary["annualSavingsAmount"]))) + { + $savingsMonthly = [double] $additionalInfoDictionary["annualSavingsAmount"] / 12 + } + else + { + if ($result.RecommendationTypeId_g -eq $rightSizeRecommendationId) + { + $savingsMonthly = [double] $result.Last30DaysCost + } + else + { + $savingsMonthly = 0.0 # unknown + } + } + } + + $additionalInfoDictionary["savingsAmount"] = [double] $savingsMonthly + + $queryInstanceId = $result.InstanceId_s + if (-not($hasCpuRamPerfMetrics)) + { + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + } + else + { + $queryWorkspace = "" + if (-not([string]::IsNullOrEmpty($result.WorkspaceId)) -and $result.WorkspaceId -ne $workspaceId) + { + $queryWorkspace = "workspace('$($result.WorkspaceId)')." + } + + $queryText = @" + let perfInterval = $($perfDaysBackwards)d; + let armId = tolower(`'$queryInstanceId`'); + let gInt = $perfTimeGrain; + let LinuxMemoryPerf = $($queryWorkspace)Perf + | where TimeGenerated > ago(perfInterval) + | where CounterName == '% Used Memory' and _ResourceId =~ armId + | project TimeGenerated, MemoryPercentage = CounterValue; + let WindowsMemoryPerf = $($queryWorkspace)Perf + | where TimeGenerated > ago(perfInterval) + | where CounterName == 'Available MBytes' and _ResourceId =~ armId + | extend MemoryAvailableMBs = CounterValue, InstanceId = tolower(_ResourceId) + | project TimeGenerated, MemoryAvailableMBs, InstanceId; + let MemoryPerf = WindowsMemoryPerf + | join kind=inner ( + $vmsTableName + | where TimeGenerated > ago(1d) + | extend InstanceId = tolower(InstanceId_s) + | distinct InstanceId, MemoryMB_s + ) on InstanceId + | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 + | project TimeGenerated, MemoryPercentage + | union LinuxMemoryPerf + | summarize P$($memoryPercentile)MemoryPercentage = percentile(MemoryPercentage, $memoryPercentile) by bin(TimeGenerated, gInt); + let ProcessorPerf = $($queryWorkspace)Perf + | where TimeGenerated > ago(perfInterval) + | where CounterName == '% Processor Time' and InstanceName == '_Total' and _ResourceId =~ armId + | summarize P$($cpuPercentile)CPUPercentage = percentile(CounterValue, $cpuPercentile) by bin(TimeGenerated, gInt); + MemoryPerf + | join kind=inner (ProcessorPerf) on TimeGenerated + | render timechart +"@ + + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $datetime.AddDays(-30).ToString("yyyy-MM-dd") + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = $result.ImpactedArea_s + Impact = $result.Impact_s + RecommendationType = "Saving" + RecommendationSubType = "AdvisorCost" + RecommendationSubTypeId = $result.RecommendationTypeId_g + RecommendationDescription = $result.Description_s + RecommendationAction = $result.RecommendationText_s + InstanceId = $result.InstanceId_s + InstanceName = $result.InstanceName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroup + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +Write-Output "Exporting final $($recommendations.Count) results as a JSON file..." + +$fileDate = $datetime.ToString("yyyyMMdd") +$jsonExportPath = "advisor-cost-augmented-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +Write-Output "Uploading $jsonExportPath to blob storage..." + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json" }; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1 new file mode 100644 index 000000000..11f8dbf56 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1 @@ -0,0 +1,693 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$deploymentDate = Get-AutomationVariable -Name "AzureOptimization_DeploymentDate" # yyyy-MM-dd format +$deploymentDate = $deploymentDate.Replace('"', "") + +$perfDaysBackwards = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendPerfPeriodInDays" -ErrorAction SilentlyContinue) +if (-not($perfDaysBackwards -gt 0)) { + $perfDaysBackwards = 7 +} + +$perfTimeGrain = Get-AutomationVariable -Name "AzureOptimization_RecommendPerfTimeGrain" -ErrorAction SilentlyContinue +if (-not($perfTimeGrain)) { + $perfTimeGrain = "1h" +} + +# percentiles variables +$cpuPercentile = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfPercentileCpu" -ErrorAction SilentlyContinue) +if (-not($cpuPercentile -gt 0)) { + $cpuPercentile = 99 +} +$memoryPercentile = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfPercentileMemory" -ErrorAction SilentlyContinue) +if (-not($memoryPercentile -gt 0)) { + $memoryPercentile = 99 +} + +# perf thresholds variables +$cpuPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdCpuPercentage" -ErrorAction SilentlyContinue) +if (-not($cpuPercentageThreshold -gt 0)) { + $cpuPercentageThreshold = 30 +} +$memoryPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdMemoryPercentage" -ErrorAction SilentlyContinue) +if (-not($memoryPercentageThreshold -gt 0)) { + $memoryPercentageThreshold = 50 +} +$cpuDegradedMaxPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdCpuDegradedMaxPercentage" -ErrorAction SilentlyContinue) +if (-not($cpuDegradedMaxPercentageThreshold -gt 0)) { + $cpuDegradedMaxPercentageThreshold = 95 +} +$cpuDegradedAvgPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdCpuDegradedAvgPercentage" -ErrorAction SilentlyContinue) +if (-not($cpuDegradedAvgPercentageThreshold -gt 0)) { + $cpuDegradedAvgPercentageThreshold = 75 +} +$memoryDegradedPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdMemoryDegradedPercentage" -ErrorAction SilentlyContinue) +if (-not($memoryDegradedPercentageThreshold -gt 0)) { + $memoryDegradedPercentageThreshold = 90 +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('AppServicePlans','MonitorMetrics','AzureConsumption','ARGResourceContainers')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$appServicePlansTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AppServicePlans' }).LogAnalyticsSuffix + "_CL" +$metricsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'MonitorMetrics' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $appServicePlansTableName, $subscriptionsTableName, $metricsTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +$recommendationsErrors = 0 + +# Execute the recommendation query against Log Analytics +Write-Output "Looking for underused App Service Plans, with less than $cpuPercentageThreshold% CPU and $memoryPercentageThreshold% RAM usage..." + +$baseQuery = @" + let billingInterval = 30d; + let perfInterval = $($perfDaysBackwards)d; + let cpuPercentileValue = $cpuPercentile; + let memoryPercentileValue = $memoryPercentile; + let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(30d) | summarize max(todatetime(Date_s)))); + let stime = etime-billingInterval; + + let BilledPlans = $consumptionTableName + | where todatetime(Date_s) between (stime..etime) and ResourceId has 'microsoft.web/serverfarms' + | extend ConsumedQuantity = todouble(Quantity_s) + | extend FinalCost = todouble(EffectivePrice_s) * ConsumedQuantity + | extend InstanceId_s = tolower(ResourceId) + | summarize Last30DaysCost = sum(FinalCost), Last30DaysQuantity = sum(ConsumedQuantity) by InstanceId_s; + + let ProcessorPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where ResourceId has 'microsoft.web/serverfarms' + | where MetricNames_s == "CpuPercentage" and AggregationType_s == 'Maximum' + | extend InstanceId_s = ResourceId + | summarize PCPUPercentage = percentile(todouble(MetricValue_s), cpuPercentileValue) by InstanceId_s; + + let MemoryPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where ResourceId has 'microsoft.web/serverfarms' + | where MetricNames_s == "MemoryPercentage" and AggregationType_s == 'Maximum' + | extend InstanceId_s = ResourceId + | summarize PMemoryPercentage = percentile(todouble(MetricValue_s), memoryPercentileValue) by InstanceId_s; + + $appServicePlansTableName + | where TimeGenerated > ago(1d) and ComputeMode_s == 'Dedicated' and SkuTier_s != 'Free' + | distinct InstanceId_s, AppServicePlanName_s, ResourceGroupName_s, SubscriptionGuid_g, Cloud_s, TenantGuid_g, SkuSize_s, NumberOfWorkers_s, Tags_s + | join kind=inner ( BilledPlans ) on InstanceId_s + | join kind=leftouter ( MemoryPerf ) on InstanceId_s + | join kind=leftouter ( ProcessorPerf ) on InstanceId_s + | project InstanceId_s, AppServicePlan = AppServicePlanName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, SkuSize_s, NumberOfWorkers_s, PMemoryPercentage, PCPUPercentage, Tags_s, Last30DaysCost, Last30DaysQuantity + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionId + | where isnotempty(PMemoryPercentage) and isnotempty(PCPUPercentage) and PMemoryPercentage < $memoryPercentageThreshold and PCPUPercentage < $cpuPercentageThreshold +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" +let perfInterval = $($perfDaysBackwards)d; +let armId = `'$queryInstanceId`'; +let gInt = $perfTimeGrain; +let MemoryPerf = $metricsTableName +| where TimeGenerated > ago(perfInterval) +| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) +| where ResourceId == armId +| where MetricNames_s == 'MemoryPercentage' and AggregationType_s == 'Maximum' +| extend MemoryPercentage = todouble(MetricValue_s) +| summarize percentile(MemoryPercentage, $memoryPercentile) by bin(CollectedDate, gInt); +let ProcessorPerf = $metricsTableName +| where TimeGenerated > ago(perfInterval) +| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) +| where ResourceId == armId +| where MetricNames_s == 'CpuPercentage' and AggregationType_s == 'Maximum' +| extend ProcessorPercentage = todouble(MetricValue_s) +| summarize percentile(ProcessorPercentage, $cpuPercentile) by bin(CollectedDate, gInt); +MemoryPerf +| join kind=inner (ProcessorPerf) on CollectedDate +| render timechart +"@ + + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $datetime.AddDays(-30).ToString("yyyy-MM-dd") + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = "$($result.SkuSize_s)" + $additionalInfoDictionary["InstanceCount"] = [int] $result.NumberOfWorkers_s + $additionalInfoDictionary["MetricCPUPercentage"] = "$($result.PCPUPercentage)" + $additionalInfoDictionary["MetricMemoryPercentage"] = "$($result.PMemoryPercentage)" + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + $additionalInfoDictionary["savingsAmount"] = ([double] $result.Last30DaysCost / 2) + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Web/serverFarms" + Impact = "High" + RecommendationType = "Saving" + RecommendationSubType = "UnderusedAppServicePlans" + RecommendationSubTypeId = "042adaca-ebdf-49b4-bc1b-2800b6e40fea" + RecommendationDescription = "Underused App Service Plans (performance capacity waste)" + RecommendationAction = "Right-size underused App Service Plans or scale it in" + InstanceId = $result.InstanceId_s + InstanceName = $result.AppServicePlan + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroup + SubscriptionGuid = $result.SubscriptionId + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "appserviceplans-underused-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for performance constrained App Service Plans, with more than $cpuDegradedMaxPercentageThreshold% Max. CPU, $cpuDegradedAvgPercentageThreshold% Avg. CPU and $memoryDegradedPercentageThreshold% RAM usage..." + +$baseQuery = @" + let perfInterval = $($perfDaysBackwards)d; + + let MemoryPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where ResourceId has 'microsoft.web/serverfarms' + | where MetricNames_s == "MemoryPercentage" and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum' + | extend InstanceId_s = ResourceId + | summarize PMemoryPercentage = avg(todouble(MetricValue_s)) by InstanceId_s; + + let ProcessorMaxPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where ResourceId has 'microsoft.web/serverfarms' + | where MetricNames_s == "CpuPercentage" and AggregationType_s == 'Maximum' + | extend InstanceId_s = ResourceId + | summarize PCPUMaxPercentage = avg(todouble(MetricValue_s)) by InstanceId_s; + + let ProcessorAvgPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where ResourceId has 'microsoft.web/serverfarms' + | where MetricNames_s == "CpuPercentage" and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum' + | extend InstanceId_s = ResourceId + | summarize PCPUAvgPercentage = avg(todouble(MetricValue_s)) by InstanceId_s; + + $appServicePlansTableName + | where TimeGenerated > ago(1d) and ComputeMode_s == 'Dedicated' and SkuTier_s != 'Free' + | distinct InstanceId_s, AppServicePlanName_s, ResourceGroupName_s, SubscriptionGuid_g, Cloud_s, TenantGuid_g, SkuSize_s, NumberOfWorkers_s, Tags_s + | join kind=leftouter ( MemoryPerf ) on InstanceId_s + | join kind=leftouter ( ProcessorMaxPerf ) on InstanceId_s + | join kind=leftouter ( ProcessorAvgPerf ) on InstanceId_s + | project InstanceId_s, AppServicePlan = AppServicePlanName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, SkuSize_s, NumberOfWorkers_s, PMemoryPercentage, PCPUMaxPercentage, PCPUAvgPercentage, Tags_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionId + | where isnotempty(PMemoryPercentage) and isnotempty(PCPUAvgPercentage) and isnotempty(PCPUMaxPercentage) and (PMemoryPercentage > $memoryDegradedPercentageThreshold or (PCPUMaxPercentage > $cpuDegradedMaxPercentageThreshold and PCPUAvgPercentage > $cpuDegradedAvgPercentageThreshold)) +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" +let perfInterval = $($perfDaysBackwards)d; +let armId = `'$queryInstanceId`'; +let gInt = $perfTimeGrain; +let MemoryPerf = $metricsTableName +| where TimeGenerated > ago(perfInterval) +| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) +| where ResourceId == armId +| where MetricNames_s == 'MemoryPercentage' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum' +| extend MemoryPercentage = todouble(MetricValue_s) +| summarize percentile(MemoryPercentage, $memoryPercentile) by bin(CollectedDate, gInt); +let ProcessorMaxPerf = $metricsTableName +| where TimeGenerated > ago(perfInterval) +| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) +| where ResourceId == armId +| where MetricNames_s == 'CpuPercentage' and AggregationType_s == 'Maximum' +| extend ProcessorMaxPercentage = todouble(MetricValue_s) +| summarize percentile(ProcessorMaxPercentage, $cpuPercentile) by bin(CollectedDate, gInt); +let ProcessorAvgPerf = $metricsTableName +| where TimeGenerated > ago(perfInterval) +| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) +| where ResourceId == armId +| where MetricNames_s == 'CpuPercentage' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum' +| extend ProcessorAvgPercentage = todouble(MetricValue_s) +| summarize percentile(ProcessorAvgPercentage, $cpuPercentile) by bin(CollectedDate, gInt); +MemoryPerf +| join kind=inner (ProcessorMaxPerf) on CollectedDate +| join kind=inner (ProcessorAvgPerf) on CollectedDate +| render timechart +"@ + + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $datetime.AddDays(-30).ToString("yyyy-MM-dd") + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = "$($result.SkuSize_s)" + $additionalInfoDictionary["InstanceCount"] = [int] $result.NumberOfWorkers_s + $additionalInfoDictionary["MetricCPUAvgPercentage"] = "$($result.PCPUAvgPercentage)" + $additionalInfoDictionary["MetricCPUMaxPercentage"] = "$($result.PCPUMaxPercentage)" + $additionalInfoDictionary["MetricMemoryPercentage"] = "$($result.PMemoryPercentage)" + + $fitScore = 3 # needs a more complete analysis to improve score + + if ([double] $result.PCPUMaxPercentage -gt [double] $cpuDegradedMaxPercentageThreshold -and [double] $result.PCPUAvgPercentage -gt [double] $cpuDegradedAvgPercentageThreshold) + { + $fitScore = 4 + } + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Performance" + ImpactedArea = "Microsoft.Web/serverFarms" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "PerfConstrainedAppServicePlans" + RecommendationSubTypeId = "351574cb-c105-4538-a778-11dfbe4857bf" + RecommendationDescription = "App Service Plan performance has been constrained by lack of resources" + RecommendationAction = "Resize App Service Plan to higher SKU or scale it out" + InstanceId = $result.InstanceId_s + InstanceName = $result.AppServicePlan + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroup + SubscriptionGuid = $result.SubscriptionId + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "appserviceplans-perfconstrained-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for empty App Service Plans..." + +$baseQuery = @" +let interval = 30d; +let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(interval) | summarize max(todatetime(Date_s)))); +let stime = etime-interval; +$appServicePlansTableName +| where TimeGenerated > ago(1d) and ComputeMode_s == 'Dedicated' and SkuTier_s != 'Free' and toint(NumberOfSites_s) == 0 +| distinct AppServicePlanName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuSize_s, NumberOfWorkers_s, Tags_s, Cloud_s +| join kind=leftouter ( + $consumptionTableName + | where todatetime(Date_s) between (stime..etime) + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s +) on InstanceId_s +| summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by AppServicePlanName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuSize_s, NumberOfWorkers_s, Tags_s, Cloud_s +| join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s +) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" + $appServicePlansTableName + | where InstanceId_s == '$queryInstanceId' + | where toint(NumberOfSites_s) == 0 + | distinct InstanceId_s, AppServicePlanName_s, TimeGenerated + | summarize FirstUnusedDate = min(TimeGenerated) by InstanceId_s, AppServicePlanName_s + | join kind=leftouter ( + $consumptionTableName + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on InstanceId_s + | summarize CostsSinceUnused = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > FirstUnusedDate) by AppServicePlanName_s, FirstUnusedDate +"@ + + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $deploymentDate + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = $result.SkuSize_s + $additionalInfoDictionary["InstanceCount"] = $result.NumberOfWorkers_s + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + $additionalInfoDictionary["savingsAmount"] = [double] $result.Last30DaysCost + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Web/serverFarms" + Impact = "High" + RecommendationType = "Saving" + RecommendationSubType = "EmptyAppServicePlans" + RecommendationSubTypeId = "ef525225-8b91-47a3-81f3-e674e94564b6" + RecommendationDescription = "App Service Plans without any application incur in unnecessary costs" + RecommendationAction = "Delete the App Service Plan" + InstanceId = $result.InstanceId_s + InstanceName = $result.AppServicePlanName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "appserviceplans-empty-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +if ($recommendationsErrors -gt 0) +{ + throw "Some of the recommendations queries failed. Please, review the job logs for additional information." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1 new file mode 100644 index 000000000..1999e2e1d --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1 @@ -0,0 +1,533 @@ +$ErrorActionPreference = "Stop" + +function Find-DiskMonthlyPrice { + param ( + [object[]] $SKUPriceSheet, + [string] $DiskSizeTier + ) + + $diskSkus = $SKUPriceSheet | Where-Object { $_.MeterName_s.Replace(" Disks","") -eq $DiskSizeTier } + $targetMonthlyPrice = [double]::MaxValue + if ($diskSkus) + { + $targetMonthlyPrice = [double] ($diskSkus | Sort-Object -Property UnitPrice_s | Select-Object -First 1).UnitPrice_s + } + return $targetMonthlyPrice +} + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$deploymentDate = Get-AutomationVariable -Name "AzureOptimization_DeploymentDate" # yyyy-MM-dd format +$deploymentDate = $deploymentDate.Replace('"', "") + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +# perf thresholds variables +$iopsPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdDiskIOPSPercentage" -ErrorAction SilentlyContinue) +if (-not($iopsPercentageThreshold -gt 0)) { + $iopsPercentageThreshold = 5 +} +$mbsPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdDiskMBsPercentage" -ErrorAction SilentlyContinue) +if (-not($mbsPercentageThreshold -gt 0)) { + $mbsPercentageThreshold = 5 +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$perfDaysBackwards = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendPerfPeriodInDays" -ErrorAction SilentlyContinue) +if (-not($perfDaysBackwards -gt 0)) { + $perfDaysBackwards = 7 +} + +$perfTimeGrain = Get-AutomationVariable -Name "AzureOptimization_RecommendPerfTimeGrain" -ErrorAction SilentlyContinue +if (-not($perfTimeGrain)) { + $perfTimeGrain = "1h" +} + +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGManagedDisk','MonitorMetrics','ARGResourceContainers','AzureConsumption','Pricesheet')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$disksTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGManagedDisk' }).LogAnalyticsSuffix + "_CL" +$metricsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'MonitorMetrics' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" +$pricesheetTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'Pricesheet' }).LogAnalyticsSuffix + "_CL" + + +Write-Output "Will run query against tables $disksTableName, $metricsTableName, $subscriptionsTableName, $pricesheetTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +Write-Output "Getting Disks SKUs for the $referenceRegion region..." + +$skus = Get-AzComputeResourceSku -Location $referenceRegion | Where-Object { $_.ResourceType -eq "disks" } + +Write-Output "Getting the current Pricesheet..." + +if ($cloudEnvironment -eq "AzureCloud") +{ + $pricesheetRegion = "EU West" +} + +try +{ + $pricesheetEntries = @() + + $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' + | distinct MeterName_s, MeterSubCategory_s, MeterCategory_s, MeterRegion_s, UnitPrice_s, UnitOfMeasure_s +"@ + + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days 14) -Wait 600 -IncludeStatistics + $pricesheetEntries = [System.Linq.Enumerable]::ToArray($queryResults.Results) + + Write-Output "Query finished with $($pricesheetEntries.Count) results." + Write-Output "Query statistics: $($queryResults.Statistics.query)" +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + Write-Output "Consumption pricesheet not available, will estimate savings based in price difference ratio..." +} + +$skuPricesFound = @{} + +Write-Output "Looking for underutilized Disks, with less than $iopsPercentageThreshold% IOPS and $mbsPercentageThreshold% MB/s usage..." + +$baseQuery = @" + let billingInterval = 30d; + let perfInterval = $($perfDaysBackwards)d; + let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(billingInterval) | summarize max(todatetime(Date_s)))); + let stime = etime-billingInterval; + + let BilledDisks = $consumptionTableName + | where todatetime(Date_s) between (stime..etime) and ResourceId contains '/disks/' and MeterCategory_s == 'Storage' and MeterSubCategory_s has 'Premium' and MeterName_s has 'Disk' + | extend DiskConsumedQuantity = todouble(Quantity_s) + | extend DiskPrice = todouble(EffectivePrice_s) + | extend FinalCost = DiskPrice * DiskConsumedQuantity + | extend ResourceId = tolower(ResourceId) + | summarize Last30DaysCost = sum(FinalCost), Last30DaysQuantity = sum(DiskConsumedQuantity) by ResourceId; + + $metricsTableName + | where MetricNames_s == 'Composite Disk Read Operations/sec,Composite Disk Write Operations/sec' and TimeGenerated > ago(perfInterval) and isnotempty(MetricValue_s) + | 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' + | 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 + | extend IOPSPercentage = MaxIOPSMetric/MaxIOPSDisk*100 + | where IOPSPercentage < $iopsPercentageThreshold + | join kind=inner ( + $metricsTableName + | where MetricNames_s == 'Composite Disk Read Bytes/sec,Composite Disk Write Bytes/sec' and TimeGenerated > ago(perfInterval) and isnotempty(MetricValue_s) + | 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' + | 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 + | extend MBsPercentage = MaxMBsMetric/MaxMBsDisk*100 + | where MBsPercentage < $mbsPercentageThreshold + ) on ResourceId + | join kind=inner ( BilledDisks ) on ResourceId + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionId +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + throw "Execution aborted" +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $targetSku = $null + $currentDiskTier = $null + + if ([string]::IsNullOrEmpty($result.DiskTier_s)) # older disks do not have Tier info in their properties + { + $currentSkuCandidates = @() + foreach ($sku in $skus) + { + $currentSkuCandidate = $null + $skuMinSizeGB = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MinSizeGiB' }).Value + $skuMaxSizeGB = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxSizeGiB' }).Value + $skuMaxIOps = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxIOps' }).Value + $skuMaxBandwidthMBps = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxBandwidthMBps' }).Value + + 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]) + { + $skuPricesFound[$sku.Size] = Find-DiskMonthlyPrice -DiskSizeTier $sku.Size -SKUPriceSheet $pricesheetEntries + } + + $currentSkuCandidate = New-Object PSObject -Property @{ + Name = $sku.Size + MaxSizeGB = $skuMaxSizeGB + } + + $currentSkuCandidates += $currentSkuCandidate + } + } + $currentDiskTier = ($currentSkuCandidates | Sort-Object -Property MaxSizeGB | Select-Object -First 1).Name + } + else + { + $currentDiskTier = $result.DiskTier_s + } + + if ($null -eq $skuPricesFound[$currentDiskTier]) + { + $skuPricesFound[$currentDiskTier] = Find-DiskMonthlyPrice -DiskSizeTier $currentDiskTier -SKUPriceSheet $pricesheetEntries + } + + $targetSkuPerfTier = $result.SKU_s.Replace("Premium", "StandardSSD") + $targetSkuCandidates = @() + + foreach ($sku in $skus) + { + $targetSkuCandidate = $null + + $skuMinSizeGB = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MinSizeGiB' }).Value + $skuMaxSizeGB = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxSizeGiB' }).Value + $skuMaxIOps = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxIOps' }).Value + $skuMaxBandwidthMBps = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxBandwidthMBps' }).Value + + 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) + { + if ($null -eq $skuPricesFound[$sku.Size]) + { + $skuPricesFound[$sku.Size] = Find-DiskMonthlyPrice -DiskSizeTier $sku.Size -SKUPriceSheet $pricesheetEntries + } + + if ($skuPricesFound[$sku.Size] -lt [double]::MaxValue -and $skuPricesFound[$sku.Size] -lt $skuPricesFound[$currentDiskTier]) + { + $targetSkuCandidate = New-Object PSObject -Property @{ + Name = $sku.Size + MonthlyPrice = $skuPricesFound[$sku.Size] + MaxSizeGB = $skuMaxSizeGB + MaxIOPS = $skuMaxIOps + MaxMBps = $skuMaxBandwidthMBps + } + + $targetSkuCandidates += $targetSkuCandidate + } + } + } + + $targetSku = $targetSkuCandidates | Sort-Object -Property MonthlyPrice | Select-Object -First 1 + + if ($null -ne $targetSku) + { + $queryInstanceId = $result.ResourceId + $queryText = @" + let billingInterval = 30d; + let armId = `'$queryInstanceId`'; + let gInt = $perfTimeGrain; + let ThroughputMBsPerf = $metricsTableName + | where TimeGenerated > ago(billingInterval) + | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) + | where ResourceId == armId + | where MetricNames_s == 'Composite Disk Read Bytes/sec,Composite Disk Write Bytes/sec' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum' + | extend ThroughputMBs = todouble(MetricValue_s)/1024/1024 + | project CollectedDate, ThroughputMBs, InstanceId_s=ResourceId + | join kind=inner ( + $disksTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, DiskThroughput_s + ) on InstanceId_s + | extend MBsPercentage = ThroughputMBs / todouble(DiskThroughput_s) * 100 + | summarize max(MBsPercentage) by bin(CollectedDate, gInt); + let IOPSPerf = $metricsTableName + | where TimeGenerated > ago(billingInterval) + | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) + | where ResourceId == armId + | where MetricNames_s == 'Composite Disk Read Operations/sec,Composite Disk Write Operations/sec' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum' + | extend IOPS = todouble(MetricValue_s) + | project CollectedDate, IOPS, InstanceId_s=ResourceId + | join kind=inner ( + $disksTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, DiskIOPS_s + ) on InstanceId_s + | extend IOPSPercentage = IOPS / todouble(DiskIOPS_s) * 100 + | summarize max(IOPSPercentage) by bin(CollectedDate, gInt); + ThroughputMBsPerf + | join kind=inner (IOPSPerf) on CollectedDate + | render timechart +"@ + + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $datetime.AddDays(-30).ToString("yyyy-MM-dd") + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["DiskType"] = "Managed" + $additionalInfoDictionary["currentSku"] = $result.SKU_s + $additionalInfoDictionary["targetSku"] = $targetSkuPerfTier + $additionalInfoDictionary["DiskSizeGB"] = [int] $result.DiskSizeGB_s + $additionalInfoDictionary["currentTier"] = $currentDiskTier + $additionalInfoDictionary["targetTier"] = $targetSku.Name + $additionalInfoDictionary["MaxIOPSMetric"] = [double] $($result.MaxIOPSMetric) + $additionalInfoDictionary["MaxMBpsMetric"] = [double] $($result.MaxMBsMetric) + $additionalInfoDictionary["MetricIOPSPercentage"] = [double] $($result.IOPSPercentage) + $additionalInfoDictionary["MetricMBpsPercentage"] = [double] $($result.MBsPercentage) + $additionalInfoDictionary["targetMaxSizeGB"] = [int] $targetSku.MaxSizeGB + $additionalInfoDictionary["targetMaxIOPS"] = [int] $targetSku.MaxIOPS + $additionalInfoDictionary["targetMaxMBps"] =[int] $targetSku.MaxMBps + + $fitScore = 4 # needs Maximum of Maximum for metrics to have higher fit score + if ([int] $result.DiskSizeGB_s -gt 512) + { + $fitScore = 3.5 #disk will not support credit-based bursting, therefore the recommendation risk increases a bit + } + + $fitScore = [Math]::max(0.0, $fitScore) + + $savingCoefficient = 2 # Standard SSD is generally close to half the price of Premium SSD + + $targetSkuSavingsMonthly = $result.Last30DaysCost - ($result.Last30DaysCost / $savingCoefficient) + + $tentativeTargetSkuSavingsMonthly = -1 + + if ($targetSku -and $skuPricesFound[$targetSku.Name] -lt [double]::MaxValue) + { + $targetSkuPrice = $skuPricesFound[$targetSku.Name] + + if ($skuPricesFound[$currentDiskTier] -lt [double]::MaxValue) + { + $currentSkuPrice = $skuPricesFound[$currentDiskTier] + $tentativeTargetSkuSavingsMonthly = ($currentSkuPrice * [double] $result.Last30DaysQuantity) - ($targetSkuPrice * [double] $result.Last30DaysQuantity) + } + else + { + $tentativeTargetSkuSavingsMonthly = $result.Last30DaysCost - ($targetSkuPrice * [double] $result.Last30DaysQuantity) + } + } + + if ($tentativeTargetSkuSavingsMonthly -ge 0) + { + $targetSkuSavingsMonthly = $tentativeTargetSkuSavingsMonthly + } + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + if ($targetSkuSavingsMonthly -eq [double]::PositiveInfinity) + { + $targetSkuSavingsMonthly = [double] $result.Last30DaysCost / 2 + } + + $additionalInfoDictionary["savingsAmount"] = [double] $targetSkuSavingsMonthly + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Compute/disks" + Impact = "High" + RecommendationType = "Saving" + RecommendationSubType = "UnderusedPremiumSSDDisks" + RecommendationSubTypeId = "4854b5dc-4124-4ade-879e-6a7bb65350ab" + RecommendationDescription = "Premium SSD disk has been underutilized" + RecommendationAction = "Change disk tier at least to the equivalent for Standard SSD" + InstanceId = $result.ResourceId + InstanceName = $result.DiskName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroup + SubscriptionGuid = $result.SubscriptionId + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation + } +} + +# Export the recommendations as JSON to blob storage + +Write-Output "Exporting final $($recommendations.Count) results as a JSON file..." + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "disks-underutilized-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 new file mode 100644 index 000000000..484b289e2 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1 @@ -0,0 +1,445 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$perfDaysBackwards = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendPerfPeriodInDays" -ErrorAction SilentlyContinue) +if (-not($perfDaysBackwards -gt 0)) { + $perfDaysBackwards = 7 +} + +$dtuPercentile = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfPercentileSqlDtu" -ErrorAction SilentlyContinue) +if (-not($dtuPercentile -gt 0)) { + $dtuPercentile = 99 +} +$dtuPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdDtuPercentage" -ErrorAction SilentlyContinue) +if (-not($dtuPercentageThreshold -gt 0)) { + $dtuPercentageThreshold = 40 +} +$dtuDegradedPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdDtuDegradedPercentage" -ErrorAction SilentlyContinue) +if (-not($dtuDegradedPercentageThreshold -gt 0)) { + $dtuDegradedPercentageThreshold = 75 +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGSqlDb','MonitorMetrics','AzureConsumption','ARGResourceContainers')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$sqlDbsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGSqlDb' }).LogAnalyticsSuffix + "_CL" +$metricsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'MonitorMetrics' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $sqlDbsTableName, $subscriptionsTableName, $metricsTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +$recommendationsErrors = 0 + +# Execute the recommendation query against Log Analytics +Write-Output "Looking for underused SQL Databases, with less than $dtuPercentageThreshold % Max. DTU usage..." + +$baseQuery = @" + let DTUPercentageThreshold = $dtuPercentageThreshold; + let MetricsInterval = $($perfDaysBackwards)d; + let BillingInterval = 30d; + let dtuPercentPercentile = $dtuPercentile; + let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(BillingInterval) | summarize max(todatetime(Date_s)))); + let stime = etime-BillingInterval; + let CandidateDatabaseIds = $sqlDbsTableName + | where TimeGenerated > ago(1d) and SkuName_s in ('Standard','Premium') + | distinct InstanceId_s; + $metricsTableName + | where TimeGenerated > ago(MetricsInterval) + | where ResourceId in (CandidateDatabaseIds) and MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Maximum' + | summarize P99DTUPercentage = percentile(todouble(MetricValue_s), dtuPercentPercentile) by ResourceId + | where P99DTUPercentage < DTUPercentageThreshold + | join ( + $sqlDbsTableName + | where TimeGenerated > ago(1d) + | project ResourceId = InstanceId_s, DBName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s + ) on ResourceId + | join kind=leftouter ( + $consumptionTableName + | where todatetime(Date_s) between (stime..etime) + | project ResourceId=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on ResourceId + | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by DBName_s, ResourceId, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s, P99DTUPercentage + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.ResourceId + $queryText = @" + $metricsTableName + | where ResourceId == '$queryInstanceId' + | where MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Maximum' + | project TimeGenerated, DTUPercentage = toint(MetricValue_s) + | render timechart +"@ + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $datetime.AddDays(-30).ToString("yyyy-MM-dd") + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = "$($result.SkuName_s) $($result.ServiceObjectiveName_s)" + $additionalInfoDictionary["DTUPercentage"] = [int] $result.P99DTUPercentage + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + $additionalInfoDictionary["savingsAmount"] = ([double] $result.Last30DaysCost / 2) + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Sql/servers/databases" + Impact = "High" + RecommendationType = "Saving" + RecommendationSubType = "UnderusedSqlDatabases" + RecommendationSubTypeId = "ff68f4e5-1197-4be9-8e5f-8760d7863cb4" + RecommendationDescription = "Underused SQL Databases (performance capacity waste)" + RecommendationAction = "Right-size underused SQL Databases" + InstanceId = $result.ResourceId + InstanceName = $result.DBName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "sqldbs-underused-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for performance constrained SQL Databases, with more than $dtuDegradedPercentageThreshold % Avg. DTU usage..." + +$baseQuery = @" + let DTUPercentageThreshold = $dtuDegradedPercentageThreshold; + let MetricsInterval = $($perfDaysBackwards)d; + let CandidateDatabaseIds = $sqlDbsTableName + | where TimeGenerated > ago(1d) and SkuName_s in ('Basic','Standard','Premium') + | distinct InstanceId_s; + $metricsTableName + | where TimeGenerated > ago(MetricsInterval) + | where ResourceId in (CandidateDatabaseIds) and MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum' + | summarize AvgDTUPercentage = avg(todouble(MetricValue_s)) by ResourceId + | where AvgDTUPercentage > DTUPercentageThreshold + | join ( + $sqlDbsTableName + | where TimeGenerated > ago(1d) + | project ResourceId = InstanceId_s, DBName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s + ) on ResourceId + | project DBName_s, ResourceId, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s, AvgDTUPercentage + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.ResourceId + $queryText = @" + $metricsTableName + | where ResourceId == '$queryInstanceId' + | where MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Average' + | project TimeGenerated, DTUPercentage = toint(MetricValue_s) + | render timechart +"@ + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $datetime.AddDays(-30).ToString("yyyy-MM-dd") + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = "$($result.SkuName_s) $($result.ServiceObjectiveName_s)" + $additionalInfoDictionary["DTUPercentage"] = [int] $result.AvgDTUPercentage + + $fitScore = 4 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Performance" + ImpactedArea = "Microsoft.Sql/servers/databases" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "PerfConstrainedSqlDatabases" + RecommendationSubTypeId = "724ff2f5-8c83-4105-b00d-029c4560d774" + RecommendationDescription = "SQL Database performance has been constrained by lack of resources" + RecommendationAction = "Resize SQL Database to higher SKU or scale it out" + InstanceId = $result.ResourceId + InstanceName = $result.DBName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "sqldbs-perfconstrained-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + + +if ($recommendationsErrors -gt 0) +{ + throw "Some of the recommendations queries failed. Please, review the job logs for additional information." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1 new file mode 100644 index 000000000..e3832d8c6 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1 @@ -0,0 +1,326 @@ +function ConvertTo-Hashtable { + [CmdletBinding()] + [OutputType('hashtable')] + param ( + [Parameter(ValueFromPipeline)] + $InputObject + ) + + process { + if ($null -eq $InputObject) { + return $null + } + + if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { + $collection = @( + foreach ($object in $InputObject) { + ConvertTo-Hashtable -InputObject $object + } + ) + Write-Output -NoEnumerate $collection + } elseif ($InputObject -is [psobject]) { + $hash = @{} + foreach ($property in $InputObject.PSObject.Properties) { + $hash[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value + } + $hash + } else { + $InputObject + } + } +} + +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +# storage account thresholds variables +$growthPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendationStorageAcountGrowthThresholdPercentage" -ErrorAction SilentlyContinue) +if (-not($growthPercentageThreshold -gt 0)) { + $growthPercentageThreshold = 5 +} +$monthlyCostThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendationStorageAcountGrowthMonthlyCostThreshold" -ErrorAction SilentlyContinue) +if (-not($monthlyCostThreshold -gt 0)) { + $monthlyCostThreshold = 50 +} +$growthLookbackDays = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendationStorageAcountGrowthLookbackDays" -ErrorAction SilentlyContinue) +if (-not($growthLookbackDays -gt 0)) { + $growthLookbackDays = 30 +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +$tenantId = (Get-AzContext).Tenant.Id + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGResourceContainers','AzureConsumption')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $subscriptionsTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = $growthLookbackDays + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +Write-Output "Looking for ever growing Storage Accounts, with more than $monthlyCostThreshold/month costs, growing more than $growthPercentageThreshold% over the last $growthLookbackDays days..." + +$dailyCostThreshold = [Math]::Round($monthlyCostThreshold / 30) + +$baseQuery = @" +let interval = $($growthLookbackDays)d; +let etime = endofday(todatetime(toscalar($consumptionTableName | where todatetime(Date_s) > ago(interval) and todatetime(Date_s) < now() | summarize max(todatetime(Date_s))))); +let etime_subs = endofday(todatetime(toscalar($subscriptionsTableName | where TimeGenerated > ago(interval) | summarize max(TimeGenerated)))); +let stime = endofday(etime-interval); +let lastday_stime = endofday(etime-1d); +let lastday_stime_subs = endofday(etime_subs-1d); +let costThreshold = $dailyCostThreshold; +let growthPercentageThreshold = $growthPercentageThreshold; +let StorageAccountsWithLastTags = $consumptionTableName +| where todatetime(Date_s) between (lastday_stime..etime) +| where MeterCategory_s == 'Storage' and ConsumedService_s == 'Microsoft.Storage' and MeterName_s endswith 'Data Stored' and ChargeType_s == 'Usage' +| extend ResourceId = tolower(ResourceId) +| distinct ResourceId, Tags_s; +$consumptionTableName +| where todatetime(Date_s) between (stime..etime) +| where MeterCategory_s == 'Storage' and ConsumedService_s == 'Microsoft.Storage' and MeterName_s endswith 'Data Stored' and ChargeType_s == 'Usage' +| extend ResourceId = tolower(ResourceId) +| make-series CostSum=sum(todouble(CostInBillingCurrency_s)) default=0.0 on todatetime(Date_s) from stime to etime step 1d by ResourceId, ResourceGroup, SubscriptionId +| extend InitialDailyCost = todouble(CostSum[0]), CurrentDailyCost = todouble(CostSum[array_length(CostSum)-1]) +| extend GrowthPercentage = round((CurrentDailyCost-InitialDailyCost)/InitialDailyCost*100) +| where InitialDailyCost > 0 and CurrentDailyCost > costThreshold and GrowthPercentage > growthPercentageThreshold +| project ResourceId, InitialDailyCost, CurrentDailyCost, GrowthPercentage, ResourceGroup, SubscriptionId +| join kind=leftouter (StorageAccountsWithLastTags) on ResourceId +| join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > lastday_stime_subs + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionId=SubscriptionGuid_g, SubscriptionName = ContainerName_s +) on SubscriptionId +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + throw "Execution aborted" +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.ResourceId + $queryText = @" + $consumptionTableName + | where MeterCategory_s == 'Storage' and ConsumedService_s == 'Microsoft.Storage' and MeterName_s endswith 'Data Stored' and ChargeType_s == 'Usage' + | extend ResourceId = tolower(ResourceId) + | where ResourceId =~ '$queryInstanceId' + | summarize DailyCosts = sum(todouble(CostInBillingCurrency_s)) by bin(todatetime(Date_s), 1d) + | render timechart +"@ + + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $datetime.AddDays(-1 * $recommendationSearchTimeSpan).ToString("yyyy-MM-dd") + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $costsAmount = ([double] $result.InitialDailyCost + [double] $result.CurrentDailyCost) / 2 * 30 + + $additionalInfoDictionary["InitialDailyCost"] = $result.InitialDailyCost + $additionalInfoDictionary["CurrentDailyCost"] = $result.CurrentDailyCost + $additionalInfoDictionary["GrowthPercentage"] = $result.GrowthPercentage + $additionalInfoDictionary["CostsAmount"] = $costsAmount + $additionalInfoDictionary["savingsAmount"] = $costsAmount * 0.25 # estimated 25% savings + + $fitScore = 4 # savings are estimated with a significant error margin + + $fitScore = [Math]::max(0.0, $fitScore) + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + if (-not($result.Tags_s -like "{*")) + { + $result.Tags_s = '{' + $result.Tags_s + '}' + } + $tags = ConvertFrom-Json $result.Tags_s | ConvertTo-Hashtable + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $cloudEnvironment + Category = "Cost" + ImpactedArea = "Microsoft.Storage/storageAccounts" + Impact = "Medium" + RecommendationType = "Saving" + RecommendationSubType = "StorageAccountsGrowing" + RecommendationSubTypeId = "08e049ca-18b0-4d22-b174-131a91d0381c" + RecommendationDescription = "Storage Account without retention policy in place" + RecommendationAction = "Review whether the Storage Account has a retention policy for example via Lifecycle Management" + InstanceId = $result.ResourceId + InstanceName = $result.ResourceId.Split('/')[-1] + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroup + SubscriptionGuid = $result.SubscriptionId + SubscriptionName = $result.SubscriptionName + TenantGuid = $tenantId + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +Write-Output "Exporting final $($recommendations.Count) results as a JSON file..." + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "storageaccounts-costsgrowing-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1 new file mode 100644 index 000000000..9dd38466b --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1 @@ -0,0 +1,270 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$deploymentDate = Get-AutomationVariable -Name "AzureOptimization_DeploymentDate" # yyyy-MM-dd format +$deploymentDate = $deploymentDate.Replace('"', "") + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGManagedDisk','AzureConsumption','ARGResourceContainers')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$disksTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGManagedDisk' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $disksTableName, $subscriptionsTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +# Execute the recommendation query against Log Analytics + +$baseQuery = @" + let interval = 30d; + let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(interval) | summarize max(todatetime(Date_s)))); + let stime = etime-interval; + $disksTableName + | where TimeGenerated > ago(1d) and isempty(OwnerVMId_s) and Tags_s !has 'ASR-ReplicaDisk' and Tags_s !has 'asrseeddisk' + | distinct DiskName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SKU_s, DiskSizeGB_s, Tags_s, Cloud_s + | join kind=leftouter ( + $consumptionTableName + | where todatetime(Date_s) between (stime..etime) + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on InstanceId_s + | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by DiskName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SKU_s, DiskSizeGB_s, Tags_s, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + throw "Execution aborted" +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" + $disksTableName + | where InstanceId_s == '$queryInstanceId' and isempty(OwnerVMId_s) + | distinct InstanceId_s, DiskName_s, DiskSizeGB_s, SKU_s, TimeGenerated + | summarize LastAttachedDate = min(TimeGenerated) by InstanceId_s, DiskName_s, DiskSizeGB_s, SKU_s + | join kind=leftouter ( + $consumptionTableName + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on InstanceId_s + | summarize CostsSinceDetached = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > LastAttachedDate) by DiskName_s, LastAttachedDate, DiskSizeGB_s, SKU_s +"@ + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $deploymentDate + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["DiskType"] = "Managed" + $additionalInfoDictionary["currentSku"] = $result.SKU_s + $additionalInfoDictionary["DiskSizeGB"] = [int] $result.DiskSizeGB_s + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + $additionalInfoDictionary["savingsAmount"] = [double] $result.Last30DaysCost + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Compute/disks" + Impact = "Medium" + RecommendationType = "Saving" + RecommendationSubType = "UnattachedDisks" + RecommendationSubTypeId = "c84d5e86-e2d6-4d62-be7c-cecfbd73b0db" + RecommendationDescription = "Unattached disks (without owner VM) incur in unnecessary costs" + RecommendationAction = "Delete or downgrade disk to Standard SKU" + InstanceId = $result.InstanceId_s + InstanceName = $result.DiskName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "unattacheddisks-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1 new file mode 100644 index 000000000..737cb9935 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1 @@ -0,0 +1,271 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$deploymentDate = Get-AutomationVariable -Name "AzureOptimization_DeploymentDate" # yyyy-MM-dd format +$deploymentDate = $deploymentDate.Replace('"', "") + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGAppGateway','AzureConsumption','ARGResourceContainers')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$appGWsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGAppGateway' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $appGWsTableName, $subscriptionsTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +# Execute the Cost recommendation query against Log Analytics + +$baseQuery = @" + let interval = 30d; + let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(interval) | summarize max(todatetime(Date_s)))); + let stime = etime-interval; + $appGWsTableName + | where TimeGenerated > ago(1d) + | where toint(BackendPoolsCount_s) == 0 or ((BackendIPCount_s == 0 or isempty(BackendIPCount_s)) and (BackendAddressesCount_s == 0 or isempty(BackendAddressesCount_s))) + | distinct InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, SkuCapacity_s, Tags_s, Cloud_s + | join kind=leftouter ( + $consumptionTableName + | where todatetime(Date_s) between (stime..etime) + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on InstanceId_s + | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, SkuCapacity_s, Tags_s, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + throw "Execution aborted" +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" + $appGWsTableName + | where InstanceId_s == '$queryInstanceId' + | where toint(BackendPoolsCount_s) == 0 or ((toint(BackendIPCount_s) == 0 or isempty(BackendIPCount_s)) and (toint(BackendAddressesCount_s) == 0 or isempty(BackendAddressesCount_s))) + | distinct InstanceId_s, InstanceName_s, TimeGenerated + | summarize FirstUnusedDate = min(TimeGenerated) by InstanceId_s, InstanceName_s + | join kind=leftouter ( + $consumptionTableName + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on InstanceId_s + | summarize CostsSinceUnused = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > FirstUnusedDate) by InstanceName_s, FirstUnusedDate +"@ + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $deploymentDate + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = $result.SkuName_s + $additionalInfoDictionary["InstanceCount"] = $result.SkuCapacity_s + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + $additionalInfoDictionary["savingsAmount"] = [double] $result.Last30DaysCost + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Network/applicationGateways" + Impact = "High" + RecommendationType = "Saving" + RecommendationSubType = "UnusedAppGateways" + RecommendationSubTypeId = "dc3d2baa-26c8-435e-aa9d-edb2bfd6fff6" + RecommendationDescription = "Application Gateways without a backend pool incur in unnecessary costs" + RecommendationAction = "Delete the Application Gateway" + InstanceId = $result.InstanceId_s + InstanceName = $result.InstanceName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "unusedappgateways-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1 new file mode 100644 index 000000000..193104391 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1 @@ -0,0 +1,402 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$deploymentDate = Get-AutomationVariable -Name "AzureOptimization_DeploymentDate" # yyyy-MM-dd format +$deploymentDate = $deploymentDate.Replace('"', "") + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGLoadBalancer','AzureConsumption','ARGResourceContainers')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$lbsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGLoadBalancer' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $lbsTableName, $subscriptionsTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +$recommendationsErrors = 0 + +# Execute the Cost recommendation query against Log Analytics + +$baseQuery = @" + let interval = 30d; + let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(interval) | summarize max(todatetime(Date_s)))); + let stime = etime-interval; + $lbsTableName + | where TimeGenerated > ago(1d) + | where SkuName_s == 'Standard' + | where (toint(BackendPoolsCount_s) == 0 or ((toint(BackendIPCount_s) == 0 or isempty(BackendIPCount_s)) and (toint(BackendAddressesCount_s) == 0 or isempty(BackendAddressesCount_s)))) and toint(InboundNatPoolsCount_s) == 0 + | where toint(LbRulesCount_s) != 0 or toint(InboundNatRulesCount_s) != 0 or toint(OutboundRulesCount_s) != 0 + | distinct InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, Tags_s, Cloud_s + | join kind=leftouter ( + $consumptionTableName + | where todatetime(Date_s) between (stime..etime) + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on InstanceId_s + | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, Tags_s, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Costs query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" + $lbsTableName + | where InstanceId_s == '$queryInstanceId' + | where SkuName_s == 'Standard' + | where (toint(BackendPoolsCount_s) == 0 or ((BackendIPCount_s == 0 or isempty(BackendIPCount_s)) and (BackendAddressesCount_s == 0 or isempty(BackendAddressesCount_s)))) and toint(InboundNatPoolsCount_s) == 0 + | where toint(LbRulesCount_s) != 0 or toint(InboundNatRulesCount_s) != 0 or toint(OutboundRulesCount_s) != 0 + | distinct InstanceId_s, InstanceName_s, SkuName_s, TimeGenerated + | summarize FirstUnusedDate = min(TimeGenerated) by InstanceId_s, InstanceName_s, SkuName_s + | join kind=leftouter ( + $consumptionTableName + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on InstanceId_s + | summarize CostsSinceUnused = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > FirstUnusedDate) by InstanceName_s, FirstUnusedDate, SkuName_s +"@ + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $deploymentDate + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = $result.SkuName_s + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + $additionalInfoDictionary["savingsAmount"] = [double] $result.Last30DaysCost + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Network/loadBalancers" + Impact = "Medium" + RecommendationType = "Saving" + RecommendationSubType = "UnusedStandardLoadBalancers" + RecommendationSubTypeId = "f1ed3bb2-3cb5-41e6-ba38-7001d5ff87f5" + RecommendationDescription = "Standard Load Balancers with rules defined and without a backend pool incur in unnecessary costs" + RecommendationAction = "Delete the Load Balancer" + InstanceId = $result.InstanceId_s + InstanceName = $result.InstanceName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "unusedstdloadbalancers-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + + +# Execute the Operational Excellence recommendation query against Log Analytics + +$baseQuery = @" + $lbsTableName + | where TimeGenerated > ago(1d) + | where (toint(BackendPoolsCount_s) == 0 or BackendIPCount_s == 0 or isempty(BackendIPCount_s)) and toint(InboundNatPoolsCount_s) == 0 + | distinct InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, Tags_s, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days 2) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Operational Excellence query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$workspaceTenantId/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = $result.SkuName_s + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "OperationalExcellence" + ImpactedArea = "Microsoft.Network/loadBalancers" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "UnusedLoadBalancers" + RecommendationSubTypeId = "48619512-f4e6-4241-9c85-16f7c987950c" + RecommendationDescription = "Load Balancers without a backend pool are useless" + RecommendationAction = "Delete the Load Balancer" + InstanceId = $result.InstanceId_s + InstanceName = $result.InstanceName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "unusedloadbalancers-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +if ($recommendationsErrors -gt 0) +{ + throw "Some of the recommendations queries failed. Please, review the job logs for additional information." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1 new file mode 100644 index 000000000..3165bc592 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1 @@ -0,0 +1,459 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$deploymentDate = Get-AutomationVariable -Name "AzureOptimization_DeploymentDate" # yyyy-MM-dd format +$deploymentDate = $deploymentDate.Replace('"', "") + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$deallocatedIntervalDays = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendationLongDeallocatedVmsIntervalDays") +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGManagedDisk','ARGVirtualMachine','AzureConsumption','ARGResourceContainers')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$vmsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVirtualMachine' }).LogAnalyticsSuffix + "_CL" +$disksTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGManagedDisk' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $vmsTableName, $disksTableName, $subscriptionsTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = $deallocatedIntervalDays + $consumptionOffsetDaysStart +$offlineInterval = $deallocatedIntervalDays + $consumptionOffsetDays +$billingInterval = 30 + $consumptionOffsetDays + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +$recommendationsErrors = 0 + +Write-Output "Looking for VMs that have been deallocated for more than 30 days..." + +# Execute the recommendation query against Log Analytics + +$baseQuery = @" + let offlineInterval = $($offlineInterval)d; + let billingInterval = $($billingInterval)d; + let billingWindowIntervalEnd = $($consumptionOffsetDays)d; + let billingWindowIntervalStart = $($consumptionOffsetDaysStart)d; + let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(billingInterval) | summarize max(todatetime(Date_s)))); + let stime = etime-offlineInterval; + let BilledVMs = $consumptionTableName + | where todatetime(Date_s) between (stime..etime) + | where ResourceId like 'microsoft.compute/virtualmachines/' or ResourceId like 'microsoft.classiccompute/virtualmachines/' + | extend InstanceId_s = tolower(ResourceId) + | distinct InstanceId_s; + let RunningVMs = $vmsTableName + | where TimeGenerated > ago(billingWindowIntervalStart) and TimeGenerated < ago(billingWindowIntervalEnd) + | where PowerState_s has_any ('running','starting','readyrole') + | distinct InstanceId_s; + let BilledDisks = $consumptionTableName + | where todatetime(Date_s) between (stime..etime) + | where ResourceId like 'microsoft.compute/disks/' + | extend BillingInstanceId = tolower(ResourceId) + | summarize DisksCosts = sum(todouble(CostInBillingCurrency_s)) by BillingInstanceId; + $vmsTableName + | where TimeGenerated > ago(billingWindowIntervalStart) and TimeGenerated < ago(billingWindowIntervalEnd) + | where InstanceId_s !in (RunningVMs) + | join kind=leftouter (BilledVMs) on InstanceId_s + | where isempty(InstanceId_s1) + | project InstanceId_s, VMName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s + | join kind=leftouter ( + $disksTableName + | where TimeGenerated > ago(1d) + | project DiskInstanceId = InstanceId_s, SKU_s, OwnerVMId_s + ) on `$left.InstanceId_s == `$right.OwnerVMId_s + | join kind=leftouter ( + BilledDisks + ) on `$left.DiskInstanceId == `$right.BillingInstanceId + | summarize TotalDisksCosts = sum(DisksCosts) by InstanceId_s, VMName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" + let offlineInterval = $($offlineInterval)d; + $consumptionTableName + | extend ResourceId = tolower(ResourceId) + | where ResourceId =~ '$queryInstanceId' + | where todatetime(Date_s) < now() + | join kind=inner ( + $disksTableName + | extend DiskInstanceId = InstanceId_s + ) + on `$left.ResourceId == `$right.OwnerVMId_s + | summarize DeallocatedSince = max(todatetime(Date_s)) by DiskName_s, DiskSizeGB_s, SKU_s, DiskInstanceId + | join kind=inner + ( + $consumptionTableName + | where todatetime(Date_s) > ago(offlineInterval) + | extend DiskInstanceId = tolower(ResourceId) + | summarize DiskCosts = sum(todouble(CostInBillingCurrency_s)) by DiskInstanceId + ) + on DiskInstanceId + | project DeallocatedSince, DiskName_s, DiskSizeGB_s, SKU_s, MonthlyCosts = DiskCosts +"@ + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $deploymentDate + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["LongDeallocatedThreshold"] = $deallocatedIntervalDays + $additionalInfoDictionary["CostsAmount"] = [double] $result.TotalDisksCosts + $additionalInfoDictionary["savingsAmount"] = [double] $result.TotalDisksCosts + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "Medium" + RecommendationType = "Saving" + RecommendationSubType = "LongDeallocatedVms" + RecommendationSubTypeId = "c320b790-2e58-452a-aa63-7b62c383ad8a" + RecommendationDescription = "Virtual Machine has been deallocated for long with disks still incurring costs" + RecommendationAction = "Delete Virtual Machine or downgrade its disks to Standard HDD SKU" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "longdeallocatedvms-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for VMs that are stopped (not deallocated)..." + +# Execute the recommendation query against Log Analytics + +$baseQuery = @" + $vmsTableName + | where TimeGenerated > ago(1d) + | where PowerState_s has 'stopped' + | project InstanceId_s, VMName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s + | join kind=leftouter ( + $consumptionTableName + | where TimeGenerated > ago(1d) and MeterCategory_s == 'Virtual Machines' + | project InstanceId_s=tolower(ResourceId), UnitPrice_s, EffectivePrice_s + | summarize arg_max(todouble(EffectivePrice_s), *) by InstanceId_s + | project InstanceId_s, MonthlyCost=24*todouble(iif(todouble(UnitPrice_s) > 0, todouble(UnitPrice_s), todouble(EffectivePrice_s)))*30 + ) on InstanceId_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" + let LastNonStopped = toscalar($vmsTableName + | where InstanceId_s =~ '$queryInstanceId' + | where TimeGenerated < now() + | where PowerState_s !has 'stopped' + | summarize max(todatetime(StatusDate_s))); + $consumptionTableName + | where ResourceId =~ '$queryInstanceId' + | where todatetime(Date_s) >= LastNonStopped + | where MeterCategory_s == 'Virtual Machines' + | summarize ComputeCostsSinceStopped = sum(todouble(Quantity_s)*todouble(UnitPrice_s)) by MeterSubCategory_s, StoppedSince=LastNonStopped +"@ + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $deploymentDate + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["CostsAmount"] = [double] $result.MonthlyCost + $additionalInfoDictionary["savingsAmount"] = [double] $result.MonthlyCost + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "High" + RecommendationType = "Saving" + RecommendationSubType = "StoppedVms" + RecommendationSubTypeId = "110fea55-a9c3-480d-8248-116f61e139a8" + RecommendationDescription = "Virtual Machine is stopped (not deallocated) and still incurring costs" + RecommendationAction = "Deallocate Virtual Machine" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "stoppedvms-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +if ($recommendationsErrors -gt 0) +{ + throw "Some of the recommendations queries failed. Please, review the job logs for additional information." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1 new file mode 100644 index 000000000..ddc65a5fe --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1 @@ -0,0 +1,795 @@ +$ErrorActionPreference = "Stop" + +function Find-SkuHourlyPrice { + param ( + [object[]] $SKUPriceSheet, + [string] $SKUName + ) + + $skuPriceObject = $null + + if ($SKUPriceSheet) + { + $skuNameParts = $SKUName.Split('_') + + if ($skuNameParts.Count -eq 3) # e.g., Standard_D1_v2 + { + $skuNameFilter = "*" + $skuNameParts[1] + " *" + $skuVersionFilter = "*" + $skuNameParts[2] + $skuPrices = $SKUPriceSheet | Where-Object { $_.MeterName_s -like $skuNameFilter ` + -and $_.MeterName_s -notlike '*Low Priority' -and $_.MeterName_s -notlike '*Expired' ` + -and $_.MeterName_s -like $skuVersionFilter -and $_.MeterSubCategory_s -notlike '*Windows' -and $_.UnitPrice_s -ne 0 } + + if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2) + { + $skuPriceObject = $skuPrices[0] + } + if ($skuPrices.Count -gt 2) # D1-like scenarios + { + $skuFilter = "*" + $skuNameParts[1] + " " + $skuNameParts[2] + "*" + $skuPrices = $skuPrices | Where-Object { $_.MeterName_s -like $skuFilter } + + if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2) + { + $skuPriceObject = $skuPrices[0] + } + } + } + + if ($skuNameParts.Count -eq 2) # e.g., Standard_D1 + { + $skuNameFilter = "*" + $skuNameParts[1] + "*" + + $skuPrices = $SKUPriceSheet | Where-Object { $_.MeterName_s -like $skuNameFilter ` + -and $_.MeterName_s -notlike '*Low Priority' -and $_.MeterName_s -notlike '*Expired' ` + -and $_.MeterName_s -notlike '* v*' -and $_.MeterSubCategory_s -notlike '*Windows' -and $_.UnitPrice_s -ne 0 } + + if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2) + { + $skuPriceObject = $skuPrices[0] + } + if ($skuPrices.Count -gt 2) # D1-like scenarios + { + $skuFilterLeft = "*" + $skuNameParts[1] + "/*" + $skuFilterRight = "*/" + $skuNameParts[1] + "*" + $skuPrices = $skuPrices | Where-Object { $_.MeterName_s -like $skuFilterLeft -or $_.MeterName_s -like $skuFilterRight } + + if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2) + { + $skuPriceObject = $skuPrices[0] + } + } + } + } + + $targetHourlyPrice = [double]::MaxValue + if ($null -ne $skuPriceObject) + { + $targetUnitHours = [int] (Select-String -InputObject $skuPriceObject.UnitOfMeasure_s -Pattern "^\d+").Matches[0].Value + if ($targetUnitHours -gt 0) + { + $targetHourlyPrice = [double] ($skuPriceObject.UnitPrice_s / $targetUnitHours) + } + } + + return $targetHourlyPrice +} + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$deploymentDate = Get-AutomationVariable -Name "AzureOptimization_DeploymentDate" # yyyy-MM-dd format +$deploymentDate = $deploymentDate.Replace('"', "") + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +# percentiles variables +$cpuPercentile = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfPercentileCpu" -ErrorAction SilentlyContinue) +if (-not($cpuPercentile -gt 0)) { + $cpuPercentile = 99 +} +$memoryPercentile = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfPercentileMemory" -ErrorAction SilentlyContinue) +if (-not($memoryPercentile -gt 0)) { + $memoryPercentile = 99 +} + +# perf thresholds variables +$cpuPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdCpuPercentage" -ErrorAction SilentlyContinue) +if (-not($cpuPercentageThreshold -gt 0)) { + $cpuPercentageThreshold = 30 +} +$memoryPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdMemoryPercentage" -ErrorAction SilentlyContinue) +if (-not($memoryPercentageThreshold -gt 0)) { + $memoryPercentageThreshold = 50 +} +$cpuDegradedMaxPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdCpuDegradedMaxPercentage" -ErrorAction SilentlyContinue) +if (-not($cpuDegradedMaxPercentageThreshold -gt 0)) { + $cpuDegradedMaxPercentageThreshold = 95 +} +$cpuDegradedAvgPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdCpuDegradedAvgPercentage" -ErrorAction SilentlyContinue) +if (-not($cpuDegradedAvgPercentageThreshold -gt 0)) { + $cpuDegradedAvgPercentageThreshold = 75 +} +$memoryDegradedPercentageThreshold = [int] (Get-AutomationVariable -Name "AzureOptimization_PerfThresholdMemoryDegradedPercentage" -ErrorAction SilentlyContinue) +if (-not($memoryDegradedPercentageThreshold -gt 0)) { + $memoryDegradedPercentageThreshold = 90 +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$perfDaysBackwards = [int] (Get-AutomationVariable -Name "AzureOptimization_RecommendPerfPeriodInDays" -ErrorAction SilentlyContinue) +if (-not($perfDaysBackwards -gt 0)) { + $perfDaysBackwards = 7 +} + +$perfTimeGrain = Get-AutomationVariable -Name "AzureOptimization_RecommendPerfTimeGrain" -ErrorAction SilentlyContinue +if (-not($perfTimeGrain)) { + $perfTimeGrain = "1h" +} + +$referenceRegion = Get-AutomationVariable -Name "AzureOptimization_ReferenceRegion" + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGVMSS','MonitorMetrics','ARGResourceContainers','AzureConsumption','Pricesheet')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$vmssTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVMSS' }).LogAnalyticsSuffix + "_CL" +$metricsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'MonitorMetrics' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" +$pricesheetTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'Pricesheet' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $vmssTableName, $metricsTableName, $subscriptionsTableName, $pricesheetTableName and $consumptionTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +Write-Output "Getting Virtual Machine SKUs for the $referenceRegion region..." + +$skus = Get-AzComputeResourceSku -Location $referenceRegion | Where-Object { $_.ResourceType -eq "virtualMachines" } + +Write-Output "Getting the current Pricesheet..." + +if ($cloudEnvironment -eq "AzureCloud") +{ + $pricesheetRegion = "EU West" +} + +try +{ + $pricesheetEntries = @() + + $baseQuery = @" + $pricesheetTableName + | where TimeGenerated > ago(14d) + | where MeterCategory_s == 'Virtual Machines' and MeterRegion_s == '$pricesheetRegion' and PriceType_s == 'Consumption' + | distinct MeterName_s, MeterSubCategory_s, MeterCategory_s, MeterRegion_s, UnitPrice_s, UnitOfMeasure_s +"@ + + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days 14) -Wait 600 -IncludeStatistics + $pricesheetEntries = [System.Linq.Enumerable]::ToArray($queryResults.Results) + + Write-Output "Query finished with $($pricesheetEntries.Count) results." + Write-Output "Query statistics: $($queryResults.Statistics.query)" +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + Write-Output "Consumption pricesheet not available, will estimate savings based in cores count..." +} + +$skuPricesFound = @{} + +$recommendationsErrors = 0 + +Write-Output "Looking for underutilized Scale Sets, with less than $cpuPercentageThreshold% CPU and $memoryPercentageThreshold% RAM usage..." + +$baseQuery = @" + let billingInterval = 30d; + let perfInterval = $($perfDaysBackwards)d; + let cpuPercentileValue = $cpuPercentile; + let memoryPercentileValue = $memoryPercentile; + let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(30d) | summarize max(todatetime(Date_s)))); + let stime = etime-billingInterval; + + let BilledVMs = $consumptionTableName + | where todatetime(Date_s) between (stime..etime) and ResourceId contains 'virtualmachinescalesets' + | extend VMConsumedQuantity = iif(ResourceId contains 'virtualmachinescalesets' and MeterCategory_s == 'Virtual Machines', todouble(Quantity_s), 0.0) + | extend VMPrice = iif(ResourceId contains 'virtualmachinescalesets' and MeterCategory_s == 'Virtual Machines', todouble(EffectivePrice_s), 0.0) + | extend FinalCost = VMPrice * VMConsumedQuantity + | extend InstanceId_s = tolower(ResourceId) + | summarize Last30DaysCost = sum(FinalCost), Last30DaysQuantity = sum(VMConsumedQuantity) by InstanceId_s; + + let MemoryPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where MetricNames_s == "Available Memory Bytes" and AggregationType_s == "Minimum" + | extend MemoryAvailableMBs = todouble(MetricValue_s)/1024/1024 + | project TimeGenerated, MemoryAvailableMBs, InstanceId_s=ResourceId + | join kind=inner ( + $vmssTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, MemoryMB_s + ) on InstanceId_s + | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 + | summarize PMemoryPercentage = percentile(MemoryPercentage, memoryPercentileValue) by InstanceId_s; + + let ProcessorPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where MetricNames_s == "Percentage CPU" and AggregationType_s == 'Maximum' + | extend InstanceId_s = ResourceId + | summarize PCPUPercentage = percentile(todouble(MetricValue_s), cpuPercentileValue) by InstanceId_s; + + $vmssTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, VMSSName_s, ResourceGroupName_s, SubscriptionGuid_g, Cloud_s, TenantGuid_g, VMSSSize_s, NicCount_s, DataDiskCount_s, Capacity_s, Tags_s + | join kind=inner ( BilledVMs ) on InstanceId_s + | join kind=leftouter ( MemoryPerf ) on InstanceId_s + | join kind=leftouter ( ProcessorPerf ) on InstanceId_s + | project InstanceId_s, VMSSName = VMSSName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, VMSSSize_s, NicCount_s, DataDiskCount_s, Capacity_s, PMemoryPercentage, PCPUPercentage, Tags_s, Last30DaysCost, Last30DaysQuantity + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionId + | where isnotempty(PMemoryPercentage) and isnotempty(PCPUPercentage) and PMemoryPercentage < $memoryPercentageThreshold and PCPUPercentage < $cpuPercentageThreshold +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + + $targetSku = $null + $currentSku = $skus | Where-Object { $_.Name -eq $result.VMSSSize_s } + + $currentSkuvCPUs = [int]($currentSku.Capabilities | Where-Object { $_.Name -eq 'vCPUsAvailable' }).Value + + $memoryNeeded = [double]($currentSku.Capabilities | Where-Object { $_.Name -eq 'MemoryGB' }).Value * ([double] $result.PMemoryPercentage / 100) + $cpuNeeded = [double]$currentSkuvCPUs * ([double] $result.PCPUPercentage / 100) + $currentPremiumIO = [bool] ($currentSku.Capabilities | Where-Object { $_.Name -eq 'PremiumIO' }).Value + $currentCpuArch = ($currentSku.Capabilities | Where-Object { $_.Name -eq 'CpuArchitectureType' }).Value + + if ($null -eq $skuPricesFound[$currentSku.Name]) + { + $skuPricesFound[$currentSku.Name] = Find-SkuHourlyPrice -SKUName $currentSku.Name -SKUPriceSheet $pricesheetEntries + } + + $targetSkuCandidates = @() + + foreach ($sku in $skus) + { + $targetSkuCandidate = $null + + $skuCPUs = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'vCPUsAvailable' }).Value + $skuMemory = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MemoryGB' }).Value + $skuMaxDataDisks = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxDataDiskCount' }).Value + $skuMaxNICs = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxNetworkInterfaces' }).Value + $skuPremiumIO = [bool] ($sku.Capabilities | Where-Object { $_.Name -eq 'PremiumIO' }).Value + $skuCpuArch = ($sku.Capabilities | Where-Object { $_.Name -eq 'CpuArchitectureType' }).Value + + if ($currentSku.Name -ne $sku.Name -and -not($sku.Name -like "*Promo*") -and [double]$skuCPUs -ge $cpuNeeded -and [double]$skuMemory -ge $memoryNeeded ` + -and $skuMaxDataDisks -ge [int] $result.DataDiskCount_s -and $skuMaxNICs -ge [int] $result.NicCount_s ` + -and ($currentPremiumIO -eq $false -or $skuPremiumIO -eq $currentPremiumIO) -and $skuCpuArch -eq $currentCpuArch) + { + if ($null -eq $skuPricesFound[$sku.Name]) + { + $skuPricesFound[$sku.Name] = Find-SkuHourlyPrice -SKUName $sku.Name -SKUPriceSheet $pricesheetEntries + } + + if ($skuPricesFound[$currentSku.Name] -eq 0 -or $skuPricesFound[$sku.Name] -lt $skuPricesFound[$currentSku.Name]) + { + $targetSkuCandidate = New-Object PSObject -Property @{ + Name = $sku.Name + HourlyPrice = $skuPricesFound[$sku.Name] + vCPUsAvailable = $skuCPUs + MemoryGB = $skuMemory + } + + $targetSkuCandidates += $targetSkuCandidate + } + } + } + + $targetSku = $targetSkuCandidates | Sort-Object -Property HourlyPrice,MemoryGB,vCPUsAvailable | Select-Object -First 1 + + if ($null -ne $targetSku) + { + $queryInstanceId = $result.InstanceId_s + $queryText = @" + let billingInterval = 30d; + let armId = `'$queryInstanceId`'; + let gInt = $perfTimeGrain; + let MemoryPerf = $metricsTableName + | where TimeGenerated > ago(billingInterval) + | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) + | where ResourceId == armId + | where MetricNames_s == 'Available Memory Bytes' and AggregationType_s == 'Minimum' + | extend MemoryAvailableMBs = todouble(MetricValue_s)/1024/1024 + | project CollectedDate, MemoryAvailableMBs, InstanceId_s=ResourceId + | join kind=inner ( + $vmssTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, MemoryMB_s + ) on InstanceId_s + | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 + | summarize percentile(MemoryPercentage, $memoryPercentile) by bin(CollectedDate, gInt); + let ProcessorPerf = $metricsTableName + | where TimeGenerated > ago(billingInterval) + | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) + | where ResourceId == armId + | where MetricNames_s == 'Percentage CPU' and AggregationType_s == 'Maximum' + | extend ProcessorPercentage = todouble(MetricValue_s) + | summarize percentile(ProcessorPercentage, $cpuPercentile) by bin(CollectedDate, gInt); + MemoryPerf + | join kind=inner (ProcessorPerf) on CollectedDate + | render timechart +"@ + + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $datetime.AddDays(-30).ToString("yyyy-MM-dd") + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["SupportsDataDisksCount"] = "true" + $additionalInfoDictionary["SupportsNICCount"] = "true" + $additionalInfoDictionary["BelowCPUThreshold"] = "true" + $additionalInfoDictionary["BelowMemoryThreshold"] = "true" + $additionalInfoDictionary["currentSku"] = "$($result.VMSSSize_s)" + $additionalInfoDictionary["InstanceCount"] = [int] $result.Capacity_s + $additionalInfoDictionary["targetSku"] = "$($targetSku.Name)" + $additionalInfoDictionary["DataDiskCount"] = "$($result.DataDiskCount_s)" + $additionalInfoDictionary["NicCount"] = "$($result.NicCount_s)" + $additionalInfoDictionary["MetricCPUPercentage"] = "$($result.PCPUPercentage)" + $additionalInfoDictionary["MetricMemoryPercentage"] = "$($result.PMemoryPercentage)" + + $fitScore = 4 # needs disk IOPS and throughput analysis to improve score + + $fitScore = [Math]::max(0.0, $fitScore) + + $savingCoefficient = [double] $currentSkuvCPUs / [double] $targetSku.vCPUsAvailable + + if ($targetSku -and $null -eq $skuPricesFound[$targetSku.Name]) + { + $skuPricesFound[$targetSku.Name] = Find-SkuHourlyPrice -SKUName $targetSku.Name -SKUPriceSheet $pricesheetEntries + } + + $targetSkuSavingsMonthly = $result.Last30DaysCost - ($result.Last30DaysCost / $savingCoefficient) + + $tentativeTargetSkuSavingsMonthly = -1 + + if ($targetSku -and $skuPricesFound[$targetSku.Name] -lt [double]::MaxValue) + { + $targetSkuPrice = $skuPricesFound[$targetSku.Name] + + if ($null -eq $skuPricesFound[$currentSku.Name]) + { + $skuPricesFound[$currentSku.Name] = Find-SkuHourlyPrice -SKUName $currentSku.Name -SKUPriceSheet $pricesheetEntries + } + + if ($skuPricesFound[$currentSku.Name] -lt [double]::MaxValue) + { + $currentSkuPrice = $skuPricesFound[$currentSku.Name] + $tentativeTargetSkuSavingsMonthly = ($currentSkuPrice * [double] $result.Last30DaysQuantity) - ($targetSkuPrice * [double] $result.Last30DaysQuantity) + } + else + { + $tentativeTargetSkuSavingsMonthly = $result.Last30DaysCost - ($targetSkuPrice * [double] $result.Last30DaysQuantity) + } + } + + if ($tentativeTargetSkuSavingsMonthly -ge 0) + { + $targetSkuSavingsMonthly = $tentativeTargetSkuSavingsMonthly + } + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + if ($targetSkuSavingsMonthly -eq [double]::PositiveInfinity) + { + $targetSkuSavingsMonthly = [double] $result.Last30DaysCost / 2 + } + + $additionalInfoDictionary["savingsAmount"] = [double] $targetSkuSavingsMonthly + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Compute/virtualMachineScaleSets" + Impact = "High" + RecommendationType = "Saving" + RecommendationSubType = "UnderusedVMSS" + RecommendationSubTypeId = "a4955cc9-533d-46a2-8625-5c4ebd1c30d5" + RecommendationDescription = "VM Scale Set has been underutilized" + RecommendationAction = "Resize VM Scale Set to lower SKU or scale it in" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMSSName + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroup + SubscriptionGuid = $result.SubscriptionId + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation + } +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "vmss-underutilized-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for performance constrained Scale Sets, with more than $cpuDegradedMaxPercentageThreshold% Max. CPU, $cpuDegradedAvgPercentageThreshold% Avg. CPU and $memoryDegradedPercentageThreshold% RAM usage..." + +$baseQuery = @" + let perfInterval = $($perfDaysBackwards)d; + + let MemoryPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where MetricNames_s == "Available Memory Bytes" and AggregationType_s == "Minimum" + | extend MemoryAvailableMBs = todouble(MetricValue_s)/1024/1024 + | project TimeGenerated, MemoryAvailableMBs, InstanceId_s=ResourceId + | join kind=inner ( + $vmssTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, MemoryMB_s + ) on InstanceId_s + | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 + | summarize PMemoryPercentage = avg(MemoryPercentage) by InstanceId_s; + + let ProcessorMaxPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where MetricNames_s == "Percentage CPU" and AggregationType_s == 'Maximum' + | extend InstanceId_s = ResourceId + | summarize PCPUMaxPercentage = avg(todouble(MetricValue_s)) by InstanceId_s; + + let ProcessorAvgPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | where MetricNames_s == "Percentage CPU" and AggregationType_s == 'Average' + | extend InstanceId_s = ResourceId + | summarize PCPUAvgPercentage = avg(todouble(MetricValue_s)) by InstanceId_s; + + $vmssTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, VMSSName_s, ResourceGroupName_s, SubscriptionGuid_g, Cloud_s, TenantGuid_g, VMSSSize_s, NicCount_s, DataDiskCount_s, Capacity_s, Tags_s + | join kind=leftouter ( MemoryPerf ) on InstanceId_s + | join kind=leftouter ( ProcessorMaxPerf ) on InstanceId_s + | join kind=leftouter ( ProcessorAvgPerf ) on InstanceId_s + | project InstanceId_s, VMSSName = VMSSName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, VMSSSize_s, NicCount_s, DataDiskCount_s, Capacity_s, PMemoryPercentage, PCPUMaxPercentage, PCPUAvgPercentage, Tags_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionId + | where isnotempty(PMemoryPercentage) and isnotempty(PCPUAvgPercentage) and isnotempty(PCPUMaxPercentage) and (PMemoryPercentage > $memoryDegradedPercentageThreshold or (PCPUMaxPercentage > $cpuDegradedMaxPercentageThreshold and PCPUAvgPercentage > $cpuDegradedAvgPercentageThreshold)) +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" + let perfInterval = $($perfDaysBackwards)d; + let armId = `'$queryInstanceId`'; + let gInt = $perfTimeGrain; + let MemoryPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) + | where ResourceId == armId + | where MetricNames_s == 'Available Memory Bytes' and AggregationType_s == 'Minimum' + | extend MemoryAvailableMBs = todouble(MetricValue_s)/1024/1024 + | project CollectedDate, MemoryAvailableMBs, InstanceId_s=ResourceId + | join kind=inner ( + $vmssTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, MemoryMB_s + ) on InstanceId_s + | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 + | summarize avg(MemoryPercentage) by bin(CollectedDate, gInt); + let ProcessorMaxPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) + | where ResourceId == armId + | where MetricNames_s == 'Percentage CPU' and AggregationType_s == 'Maximum' + | extend ProcessorMaxPercentage = todouble(MetricValue_s) + | summarize percentile(ProcessorMaxPercentage, $cpuPercentile) by bin(CollectedDate, gInt); + let ProcessorAvgPerf = $metricsTableName + | where TimeGenerated > ago(perfInterval) + | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z')) + | where ResourceId == armId + | where MetricNames_s == 'Percentage CPU' and AggregationType_s == 'Average' + | extend ProcessorAvgPercentage = todouble(MetricValue_s) + | summarize percentile(ProcessorAvgPercentage, $cpuPercentile) by bin(CollectedDate, gInt); + MemoryPerf + | join kind=inner (ProcessorMaxPerf) on CollectedDate + | join kind=inner (ProcessorAvgPerf) on CollectedDate + | render timechart +"@ + + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $datetime.AddDays(-30).ToString("yyyy-MM-dd") + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = "$($result.VMSSSize_s)" + $additionalInfoDictionary["InstanceCount"] = [int] $result.Capacity_s + $additionalInfoDictionary["MetricCPUAvgPercentage"] = "$($result.PCPUAvgPercentage)" + $additionalInfoDictionary["MetricCPUMaxPercentage"] = "$($result.PCPUMaxPercentage)" + $additionalInfoDictionary["MetricMemoryPercentage"] = "$($result.PMemoryPercentage)" + + $fitScore = 3 # needs disk IOPS and throughput analysis to improve score + + if ([double] $result.PCPUMaxPercentage -gt [double] $cpuDegradedMaxPercentageThreshold -and [double] $result.PCPUAvgPercentage -gt [double] $cpuDegradedAvgPercentageThreshold) + { + $fitScore = 4 + } + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Performance" + ImpactedArea = "Microsoft.Compute/virtualMachineScaleSets" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "PerfConstrainedVMSS" + RecommendationSubTypeId = "20a40c62-e5c8-4cc3-9fc2-f4ac75013182" + RecommendationDescription = "VM Scale Set performance has been constrained by lack of resources" + RecommendationAction = "Resize VM Scale Set to higher SKU or scale it out" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMSSName + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroup + SubscriptionGuid = $result.SubscriptionId + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "vmss-perfconstrained-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +if ($recommendationsErrors -gt 0) +{ + throw "Some of the recommendations queries failed. Please, review the job logs for additional information." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1 new file mode 100644 index 000000000..8e7b36368 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1 @@ -0,0 +1,1474 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGVirtualMachine','ARGUnmanagedDisk','ARGAvailabilitySet','ARGResourceContainers','ARGVMSS')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$availSetTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGAvailabilitySet' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" +$vmsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVirtualMachine' }).LogAnalyticsSuffix + "_CL" +$vhdsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGUnmanagedDisk' }).LogAnalyticsSuffix + "_CL" +$vmssTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVMSS' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $availSetTableName, $vmsTableName, $vmssTableName, $vhdsTableName and $subscriptionsTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 1 + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +$recommendationsErrors = 0 + +Write-Output "Looking for Availability Sets with a low fault domain count..." + +# Execute the recommendation query against Log Analytics + +$baseQuery = @" + $availSetTableName + | where TimeGenerated > ago(1d) and toint(FaultDomains_s) < 3 and toint(FaultDomains_s) < todouble(VmCount_s)/2 + | project TimeGenerated, InstanceId_s, InstanceName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s, FaultDomains_s, VmCount_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["FaultDomainCount"] = $result.FaultDomains_s + $additionalInfoDictionary["VMCount"] = $result.VmCount_s + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "AvailSetLowFaultDomainCount" + RecommendationSubTypeId = "255de20b-d5e4-4be5-9695-620b4a905774" + RecommendationDescription = "Availability Sets should have a fault domain count of 3 or equal or greater than half of the Virtual Machines count" + RecommendationAction = "Increase the fault domain count of your Availability Set" + InstanceId = $result.InstanceId_s + InstanceName = $result.InstanceName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "availsetsfaultdomaincount-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for Availability Sets with a low update domain count..." + +$baseQuery = @" + $availSetTableName + | where TimeGenerated > ago(1d) and toint(UpdateDomains_s) < todouble(VmCount_s)/2 + | project TimeGenerated, InstanceId_s, InstanceName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s, UpdateDomains_s, VmCount_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["UpdateDomainCount"] = $result.UpdateDomains_s + $additionalInfoDictionary["VMCount"] = $result.VmCount_s + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "AvailSetLowUpdateDomainCount" + RecommendationSubTypeId = "9764e285-2eca-46c5-b49e-649c039cf0cf" + RecommendationDescription = "Availability Sets should have an update domain count equal or greater than half of the Virtual Machines count" + RecommendationAction = "Increase the update domain count of your Availability Set" + InstanceId = $result.InstanceId_s + InstanceName = $result.InstanceName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "availsetsupdatedomaincount-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for Availability Sets with VMs sharing storage accounts..." + +$baseQuery = @" + $vhdsTableName + | where TimeGenerated > ago(1d) + | extend StorageAccountName = tostring(split(InstanceId_s, '/')[0]) + | distinct TimeGenerated, StorageAccountName, OwnerVMId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s + | join kind=inner ( + $vmsTableName + | where TimeGenerated > ago(1d) and isnotempty(AvailabilitySetId_s) + | distinct VMName_s, InstanceId_s, AvailabilitySetId_s, Cloud_s, Tags_s + ) on `$left.OwnerVMId_s == `$right.InstanceId_s + | extend AvailabilitySetName = tostring(split(AvailabilitySetId_s,'/')[8]) + | summarize TimeGenerated = any(TimeGenerated), Tags_s=any(Tags_s), VMCount = count() by AvailabilitySetName, AvailabilitySetId_s, StorageAccountName, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s + | where VMCount > 1 + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics -ErrorAction Continue + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.AvailabilitySetId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["SharedStorageAccountName"] = $result.StorageAccountName + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "AvailSetSharedStorageAccount" + RecommendationSubTypeId = "e530029f-9b6a-413a-99ed-81af54502bb9" + RecommendationDescription = "Virtual Machines in unmanaged Availability Sets should not share the same Storage Account" + RecommendationAction = "Migrate Virtual Machines disks to Managed Disks or keep the disks in a dedicated Storage Account per VM" + InstanceId = $result.AvailabilitySetId_s + InstanceName = $result.AvailabilitySetName + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "availsetsharedsa-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for Storage Accounts with multiple VMs..." + +$baseQuery = @" + $vhdsTableName + | where TimeGenerated > ago(1d) + | extend StorageAccountName = tostring(split(InstanceId_s, '/')[0]) + | distinct TimeGenerated, StorageAccountName, OwnerVMId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, Cloud_s + | join kind=inner ( + $vmsTableName + | where TimeGenerated > ago(1d) + | distinct InstanceId_s, Tags_s + ) on `$left.OwnerVMId_s == `$right.InstanceId_s + | summarize TimeGenerated = any(TimeGenerated), Tags_s=any(Tags_s), VMCount = count() by StorageAccountName, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, Cloud_s + | where VMCount > 1 + | extend StorageAccountId = strcat('/subscriptions/', SubscriptionGuid_g, '/resourcegroups/', ResourceGroupName_s, '/providers/microsoft.storage/storageaccounts/', StorageAccountName) + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics -ErrorAction Continue + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.StorageAccountId + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["VirtualMachineCount"] = $result.VMCount + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "StorageAccountsMultipleVMs" + RecommendationSubTypeId = "b70f44fa-5ef9-4180-b2f9-9cc6be07ab3e" + RecommendationDescription = "Virtual Machines with unmanaged disks should not share the same Storage Account" + RecommendationAction = "Migrate Virtual Machines disks to Managed Disks or keep the disks in a dedicated Storage Account per VM" + InstanceId = $result.StorageAccountId + InstanceName = $result.StorageAccountName + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "storageaccountsmultiplevms-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for VMs with no Availability Set..." + +$baseQuery = @" + $vmsTableName + | where TimeGenerated > ago(1d) and isempty(AvailabilitySetId_s) and isempty(Zones_s) and Tags_s !has 'databricks-instance-name' + | project TimeGenerated, VMName_s, InstanceId_s, Tags_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "VMsNoAvailSet" + RecommendationSubTypeId = "998b50d8-e654-417b-ab20-a31cb11629c0" + RecommendationDescription = "Virtual Machines should be placed in an Availability Set together with other instances with the same role" + RecommendationAction = "Add VM to an Availability Set together with other VMs of the same role" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "vmsnoavailset-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for VMs alone in an Availability Set..." + +$baseQuery = @" + $vmsTableName + | where TimeGenerated > ago(1d) and isnotempty(AvailabilitySetId_s) and isempty(Zones_s) + | distinct TimeGenerated, VMName_s, InstanceId_s, AvailabilitySetId_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Cloud_s, Tags_s + | summarize any(TimeGenerated, VMName_s, InstanceId_s, Tags_s), VMCount = count() by AvailabilitySetId_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Cloud_s + | where VMCount == 1 + | project TimeGenerated = any_TimeGenerated, VMName_s = any_VMName_s, InstanceId_s = any_InstanceId_s, Tags_s = any_Tags_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "VMsSingleInAvailSet" + RecommendationSubTypeId = "fe577af5-dfa2-413a-82a9-f183196c1f49" + RecommendationDescription = "Virtual Machines should not be the only instance in an Availability Set" + RecommendationAction = "Add more VMs of the same role to the Availability Set" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "vmssingleinavailset-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for VMs with disks in multiple Storage Accounts..." + +$baseQuery = @" + $vhdsTableName + | where TimeGenerated > ago(1d) + | extend StorageAccountName = tostring(split(InstanceId_s, '/')[0]) + | distinct TimeGenerated, StorageAccountName, OwnerVMId_s + | summarize TimeGenerated = any(TimeGenerated), StorageAcccountCount = count() by OwnerVMId_s + | where StorageAcccountCount > 1 + | join kind=inner ( + $vmsTableName + | where TimeGenerated > ago(1d) + | distinct VMName_s, InstanceId_s, Cloud_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Tags_s + ) on `$left.OwnerVMId_s == `$right.InstanceId_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics -ErrorAction Continue + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["StorageAccountsUsed"] = $result.StorageAcccountCount + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "DisksMultipleStorageAccounts" + RecommendationSubTypeId = "024049e7-f63a-4e1c-b620-f011aafbc576" + RecommendationDescription = "Each Virtual Machine should have its unmanaged disks stored in a single Storage Account for higher availability and manageability" + RecommendationAction = "Migrate Virtual Machines disks to Managed Disks or move VHDs to the same Storage Account" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "disksmultiplesa-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for VMs using unmanaged disks..." + +$baseQuery = @" + $vmsTableName + | where TimeGenerated > ago(1d) and UsesManagedDisks_s == 'false' + | distinct InstanceId_s, VMName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, DeploymentModel_s, Tags_s, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["DeploymentModel"] = $result.DeploymentModel_s + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "UnmanagedDisks" + RecommendationSubTypeId = "b576a069-b1f2-43a6-9134-5ee75376402a" + RecommendationDescription = "Virtual Machines should use Managed Disks for higher availability and manageability" + RecommendationAction = "Migrate Virtual Machines disks to Managed Disks" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "unmanageddisks-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for Resource Groups with VMs not in multiple AZs..." + +$baseQuery = @" + let VMsInZones = materialize($vmsTableName + | where TimeGenerated > ago(1d) and isempty(AvailabilitySetId_s) and isnotempty(Zones_s)); + VMsInZones + | distinct ResourceGroupName_s, Zones_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s + | summarize ZonesCount=count() by ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s + | where ZonesCount < 3 + | join kind=inner ( + VMsInZones + | where PowerState_s has 'running' + | distinct VMName_s, ResourceGroupName_s, SubscriptionGuid_g + | summarize VMCount=count() by ResourceGroupName_s, SubscriptionGuid_g + ) on ResourceGroupName_s and SubscriptionGuid_g + | where VMCount == 1 or VMCount > ZonesCount + | project-away SubscriptionGuid_g1, ResourceGroupName_s1 + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g + | extend InstanceId = strcat('/subscriptions/', SubscriptionGuid_g, '/resourcegroups/', ResourceGroupName_s) +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["ZonesCount"] = $result.ZonesCount + $additionalInfoDictionary["VMsCount"] = $result.VMCount + + $fitScore = 4 # a resource group may contain VMs from multiple applications which may lead to false negatives + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachines" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "VMsMultipleAZs" + RecommendationSubTypeId = "1a77887c-7375-434e-af19-c2543171e0b8" + RecommendationDescription = "Virtual Machines should be placed in multiple Availability Zones" + RecommendationAction = "Distribute Virtual Machines instances of the same role in multiple Availability Zones" + InstanceId = $result.InstanceId + InstanceName = $result.ResourceGroupName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "vmsmultipleazs-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for VMSS not in multiple AZs..." + +$baseQuery = @" + $vmssTableName + | where TimeGenerated > ago(1d) + | where (isempty(Zones_s) and toint(Capacity_s) > 1) or (array_length(split(Zones_s, ' ')) != 3 and toint(Capacity_s) > 2) + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["Zones"] = $result.Zones_s + $additionalInfoDictionary["VMSSCapacity"] = $result.Capacity_s + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachineScaleSets" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "VMSSMultipleAZs" + RecommendationSubTypeId = "47e5457c-b345-4372-b536-8887fa8f0298" + RecommendationDescription = "Virtual Machine Scale Sets should be placed in multiple Availability Zones" + RecommendationAction = "Reprovision the Scale Set leveraging enough Availability Zones" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMSSName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "vmssmultipleazs-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for VMSS using unmanaged disks..." + +$baseQuery = @" + $vmssTableName + | where TimeGenerated > ago(1d) and UsesManagedDisks_s == 'false' + | distinct InstanceId_s, VMSSName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Tags_s, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "HighAvailability" + ImpactedArea = "Microsoft.Compute/virtualMachineScaleSets" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "UnmanagedDisksVMSS" + RecommendationSubTypeId = "1bf03c4a-c402-4e6c-bf20-051b18af30e2" + RecommendationDescription = "Virtual Machine Scale Sets should use Managed Disks for higher availability and manageability" + RecommendationAction = "Migrate Virtual Machine Scale Sets disks to Managed Disks" + InstanceId = $result.InstanceId_s + InstanceName = $result.VMSSName_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "unmanageddisksvmss-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +if ($recommendationsErrors -gt 0) +{ + throw "Some of the recommendations queries failed. Please, review the job logs for additional information." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1 new file mode 100644 index 000000000..5ccd80633 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1 @@ -0,0 +1,1327 @@ +$ErrorActionPreference = "Stop" + +# Collect generic and recommendation-specific variables + +$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" +} + +$workspaceId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceId" +$workspaceName = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceName" +$workspaceRG = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceRG" +$workspaceSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceSubId" +$workspaceTenantId = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsWorkspaceTenantId" + +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RecommendationsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "recommendationsexports" +} + +$deploymentDate = Get-AutomationVariable -Name "AzureOptimization_DeploymentDate" # yyyy-MM-dd format +$deploymentDate = $deploymentDate.Replace('"', "") + +$lognamePrefix = Get-AutomationVariable -Name "AzureOptimization_LogAnalyticsLogPrefix" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($lognamePrefix)) +{ + $lognamePrefix = "AzureOptimization" +} + +$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" +} + +$subnetMaxUsedThresholdVar = Get-AutomationVariable -Name "AzureOptimization_RecommendationVNetSubnetMaxUsedPercentageThreshold" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($subnetMaxUsedThresholdVar) -or $subnetMaxUsedThresholdVar -eq 0) +{ + $subnetMaxUsedThreshold = 80 +} +else +{ + $subnetMaxUsedThreshold = [int] $subnetMaxUsedThresholdVar +} + +$subnetMinUsedThresholdVar = Get-AutomationVariable -Name "AzureOptimization_RecommendationVNetSubnetMinUsedPercentageThreshold" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($subnetMinUsedThresholdVar) -or $subnetMinUsedThresholdVar -eq 0) +{ + $subnetMinUsedThreshold = 5 +} +else +{ + $subnetMinUsedThreshold = [int] $subnetMinUsedThresholdVar +} + +# must be a comma-separated, single-quote enclosed list of subnet names, e.g., 'gatewaysubnet','azurebastionsubnet' +$subnetFreeExclusions = Get-AutomationVariable -Name "AzureOptimization_RecommendationVNetSubnetUsedPercentageExclusions" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($subnetFreeExclusions)) +{ + $subnetFreeExclusions = "'gatewaysubnet'" +} + +$subnetMinAgeVar = Get-AutomationVariable -Name "AzureOptimization_RecommendationVNetSubnetEmptyMinAgeInDays" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($subnetMinAgeVar) -or $subnetMinAgeVar -eq 0) +{ + $subnetMinAge = 30 +} +else +{ + $subnetMinAge = [int] $subnetMinAgeVar +} + +$consumptionOffsetDays = [int] (Get-AutomationVariable -Name "AzureOptimization_ConsumptionOffsetDays") +$consumptionOffsetDaysStart = $consumptionOffsetDays + 1 + +$SqlTimeout = 120 +$LogAnalyticsIngestControlTable = "LogAnalyticsIngestControl" + +# Authenticate against Azure + +"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 + } +} + +Write-Output "Finding tables where recommendations will be generated from..." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = "SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGNetworkInterface','ARGVirtualNetwork','ARGResourceContainers', 'ARGNSGRule', 'ARGPublicIP','AzureConsumption')" + + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $controlRows = New-Object System.Data.DataTable + $sqlAdapter.Fill($controlRows) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +$nicsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGNetworkInterface' }).LogAnalyticsSuffix + "_CL" +$vNetsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVirtualNetwork' }).LogAnalyticsSuffix + "_CL" +$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + "_CL" +$nsgRulesTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGNSGRule' }).LogAnalyticsSuffix + "_CL" +$publicIpsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGPublicIP' }).LogAnalyticsSuffix + "_CL" +$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + "_CL" + +Write-Output "Will run query against tables $nicsTableName, $nsgRulesTableName, $publicIpsTableName, $subscriptionsTableName, $consumptionTableName and $vNetsTableName" + +$Conn.Close() +$Conn.Dispose() + +$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart + +# Grab a context reference to the Storage Account where the recommendations file will be stored + +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink + +if ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId) +{ + Select-AzSubscription -SubscriptionId $workspaceSubscriptionId +} + +$recommendationsErrors = 0 + +Write-Output "Looking for subnets with free IP space less than $subnetMaxUsedThreshold%, excluding $subnetFreeExclusions..." + +$baseQuery = @" + $vNetsTableName + | where TimeGenerated > ago(1d) + | where SubnetName_s !in ($subnetFreeExclusions) + | extend FreeIPs = toint(SubnetTotalPrefixIPs_s) - toint(SubnetUsedIPs_s) + | extend UsedIPPercentage = (todouble(SubnetUsedIPs_s) / todouble(SubnetTotalPrefixIPs_s)) * 100 + | where UsedIPPercentage >= $subnetMaxUsedThreshold + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/subnets" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["subnetName"] = $result.SubnetName_s + $additionalInfoDictionary["subnetPrefix"] = $result.SubnetPrefix_s + $additionalInfoDictionary["subnetTotalIPs"] = $result.SubnetTotalPrefixIPs_s + $additionalInfoDictionary["subnetFreeIPs"] = $result.FreeIPs + $additionalInfoDictionary["subnetUsedIPPercentage"] = $result.UsedIPPercentage + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "OperationalExcellence" + ImpactedArea = "Microsoft.Network/virtualNetworks" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "HighSubnetIPSpaceUsage" + RecommendationSubTypeId = "5292525b-5095-4e52-803e-e17192f1d099" + RecommendationDescription = "Subnets with a high IP space usage may constrain operations" + RecommendationAction = "Move network devices to a subnet with a larger address space" + InstanceId = $result.InstanceId_s + InstanceName = "$($result.VNetName_s)/$($result.SubnetName_s)" + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "subnetshighspaceusage-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for subnets with used IP space less than $subnetMinUsedThreshold%..." + +$baseQuery = @" + $vNetsTableName + | where TimeGenerated > ago(1d) + | where SubnetName_s !in ($subnetFreeExclusions) + | extend FreeIPs = toint(SubnetTotalPrefixIPs_s) - toint(SubnetUsedIPs_s) + | extend UsedIPPercentage = (todouble(SubnetUsedIPs_s) / todouble(SubnetTotalPrefixIPs_s)) * 100 + | where UsedIPPercentage > 0 and UsedIPPercentage <= $subnetMinUsedThreshold + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/subnets" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["subnetName"] = $result.SubnetName_s + $additionalInfoDictionary["subnetPrefix"] = $result.SubnetPrefix_s + $additionalInfoDictionary["subnetTotalIPs"] = $result.SubnetTotalPrefixIPs_s + $additionalInfoDictionary["subnetUsedIPs_s"] = $result.SubnetUsedIPs_s + $additionalInfoDictionary["subnetUsedIPPercentage"] = $result.UsedIPPercentage + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "OperationalExcellence" + ImpactedArea = "Microsoft.Network/virtualNetworks" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "LowSubnetIPSpaceUsage" + RecommendationSubTypeId = "0f27b41c-869a-4563-86e9-d1c94232ba81" + RecommendationDescription = "Subnets with a low IP space usage are a waste of virtual network address space" + RecommendationAction = "Move network devices to a subnet with a smaller address space" + InstanceId = $result.InstanceId_s + InstanceName = "$($result.VNetName_s)/$($result.SubnetName_s)" + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "subnetslowspaceusage-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for subnets without any device..." + +$baseQuery = @" + $vNetsTableName + | where TimeGenerated > ago(1d) + | where toint(SubnetUsedIPs_s) == 0 and toint(SubnetDelegationsCount_s) == 0 + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/subnets" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["subnetName"] = $result.SubnetName_s + $additionalInfoDictionary["subnetPrefix"] = $result.SubnetPrefix_s + $additionalInfoDictionary["subnetTotalIPs"] = $result.SubnetTotalPrefixIPs_s + $additionalInfoDictionary["subnetUsedIPs_s"] = $result.SubnetUsedIPs_s + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "OperationalExcellence" + ImpactedArea = "Microsoft.Network/virtualNetworks" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "NoSubnetIPSpaceUsage" + RecommendationSubTypeId = "343bbfb7-5bec-4711-8353-398454d42b7b" + RecommendationDescription = "Subnets without any IP usage are a waste of virtual network address space" + RecommendationAction = "Delete the subnet to reclaim address space" + InstanceId = $result.InstanceId_s + InstanceName = "$($result.VNetName_s)/$($result.SubnetName_s)" + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "subnetsnospaceusage-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for orphaned NICs..." + +$baseQuery = @" + $nicsTableName + | where TimeGenerated > ago(1d) + | where isempty(OwnerVMId_s) and isempty(OwnerPEId_s) + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.InstanceId_s + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["privateIpAddress"] = $result.PrivateIPAddress_s + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "OperationalExcellence" + ImpactedArea = "Microsoft.Network/networkInterfaces" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "OrphanedNIC" + RecommendationSubTypeId = "4c5c2d0c-b6a4-4c59-bc18-6fff6c1f5b23" + RecommendationDescription = "Orphaned Network Interfaces (without owner VM or PE) unnecessarily consume IP address space" + RecommendationAction = "Delete the NIC to reclaim address space" + InstanceId = $result.InstanceId_s + InstanceName = $result.Name_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "orphanednics-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for NSG rules referring empty or removed subnets..." + +$baseQuery = @" + let MinimumSubnetAge = $($subnetMinAge)d; + let SubnetsToday = materialize( $vNetsTableName + | where TimeGenerated > ago(1d) + | extend SubnetId = tolower(strcat(InstanceId_s, '/subnets/', SubnetName_s)) + | distinct SubnetId, SubnetPrefix_s, SubnetUsedIPs_s, SubnetDelegationsCount_s ); + let SubnetsBefore = materialize( $vNetsTableName + | where TimeGenerated < ago(1d) + | extend SubnetId = tolower(strcat(InstanceId_s, '/subnets/', SubnetName_s)) + | summarize ExistsSince = min(todatetime(StatusDate_s)) by SubnetId, SubnetPrefix_s ); + let SubnetsExistingLongEnoughIds = SubnetsBefore | where ExistsSince < ago(MinimumSubnetAge) | distinct SubnetId; + let EmptySubnets = SubnetsToday | where SubnetId in (SubnetsExistingLongEnoughIds) and toint(SubnetUsedIPs_s) == 0 and toint(SubnetDelegationsCount_s) == 0; + let SubnetsTodayIds = SubnetsToday | distinct SubnetId; + let SubnetsTodayPrefixes = SubnetsToday | distinct SubnetPrefix_s; + let RemovedSubnets = SubnetsBefore | where SubnetId !in (SubnetsTodayIds) and SubnetPrefix_s !in (SubnetsTodayPrefixes); + let NSGRules = materialize($nsgRulesTableName + | where TimeGenerated > ago(1d) + | extend SourceAddresses = split(RuleSourceAddresses_s,',') + | mvexpand SourceAddresses + | extend SourceAddress = tostring(SourceAddresses) + | extend DestinationAddresses = split(RuleDestinationAddresses_s,',') + | mvexpand DestinationAddresses + | extend DestinationAddress = tostring(DestinationAddresses) + | project NSGId = InstanceId_s, RuleName_s, DestinationAddress, SourceAddress, SubscriptionGuid_g, Cloud_s, TenantGuid_g, ResourceGroupName_s, NSGName = NSGName_s, Tags_s); + let EmptySubnetsAsSource = EmptySubnets + | join kind=inner ( NSGRules ) on `$left.SubnetPrefix_s == `$right.SourceAddress + | extend SubnetState = 'empty'; + let EmptySubnetsAsDestination = EmptySubnets + | join kind=inner ( NSGRules ) on `$left.SubnetPrefix_s == `$right.DestinationAddress + | extend SubnetState = 'empty'; + let RemovedSubnetsAsSource = RemovedSubnets + | join kind=inner ( NSGRules ) on `$left.SubnetPrefix_s == `$right.SourceAddress + | extend SubnetState = 'unexisting'; + let RemovedSubnetsAsDestination = RemovedSubnets + | join kind=inner ( NSGRules ) on `$left.SubnetPrefix_s == `$right.DestinationAddress + | extend SubnetState = 'unexisting'; + EmptySubnetsAsSource + | union EmptySubnetsAsDestination + | union RemovedSubnetsAsSource + | union RemovedSubnetsAsDestination + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g + | where isnotempty(SubnetPrefix_s) + | distinct NSGId, NSGName, RuleName_s, SubscriptionGuid_g, SubscriptionName, ResourceGroupName_s, TenantGuid_g, Cloud_s, SubnetId, SubnetPrefix_s, SubnetState, Tags_s +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.NSGId + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["subnetId"] = $result.SubnetId + $additionalInfoDictionary["subnetPrefix"] = $result.SubnetPrefix_s + $additionalInfoDictionary["subnetState"] = $result.SubnetState + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Security" + ImpactedArea = "Microsoft.Network/networkSecurityGroups" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "NSGRuleForEmptyOrUnexistingSubnet" + RecommendationSubTypeId = "b5491cde-f76c-4423-8c4c-89e3558ff2f2" + RecommendationDescription = "NSG rules referring to empty or unexisting subnets" + RecommendationAction = "Update or remove the NSG rule to improve your network security posture" + InstanceId = $result.NSGId + InstanceName = "$($result.NSGName)/$($result.RuleName_s)" + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "nsgrules-emptyunexistingsubnets-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for NSG rules referring orphan or removed NICs..." + +$baseQuery = @" + let NICsToday = materialize( $nicsTableName + | where TimeGenerated > ago(1d) + | extend NICId = tolower(InstanceId_s) + | distinct NICId, PrivateIPAddress_s, PublicIPId_s, OwnerVMId_s, OwnerPEId_s ); + let NICsBefore = $nicsTableName + | where TimeGenerated < ago(1d) + | extend NICId = tolower(InstanceId_s) + | distinct NICId, PrivateIPAddress_s, PublicIPId_s; + let OrphanNICs = NICsToday + | where isempty(OwnerVMId_s) and isempty(OwnerPEId_s) + | extend PublicIPId_s = tolower(PublicIPId_s) + | join kind=leftouter ( + $publicIpsTableName + | where TimeGenerated > ago(1d) + | project PublicIPId_s = tolower(InstanceId_s), PublicIPAddress = IPAddress + ) on PublicIPId_s; + let NICsTodayIds = NICsToday | distinct NICId; + let NICsTodayIPs = NICsToday | distinct PrivateIPAddress_s; + let RemovedNICs = NICsBefore + | where NICId !in (NICsTodayIds) and PrivateIPAddress_s !in (NICsTodayIPs) + | extend PublicIPId_s = tolower(PublicIPId_s) + | join kind=leftouter ( + $publicIpsTableName + | where TimeGenerated < ago(1d) + | project PublicIPId_s = tolower(InstanceId_s), PublicIPAddress = IPAddress + ) on PublicIPId_s; + let NSGRules = materialize($nsgRulesTableName + | where TimeGenerated > ago(1d) + | extend SourceAddresses = split(RuleSourceAddresses_s,',') + | mvexpand SourceAddresses + | extend SourceAddress = replace('/32','',tostring(SourceAddresses)) + | extend DestinationAddresses = split(RuleDestinationAddresses_s,',') + | mvexpand DestinationAddresses + | extend DestinationAddress = replace('/32','',tostring(DestinationAddresses)) + | project NSGId = InstanceId_s, RuleName_s, DestinationAddress, SourceAddress, SubscriptionGuid_g, Cloud_s, TenantGuid_g, ResourceGroupName_s, NSGName = NSGName_s, Tags_s); + let OrphanNICsAsPrivateSource = OrphanNICs + | join kind=inner ( NSGRules ) on `$left.PrivateIPAddress_s == `$right.SourceAddress + | extend NICState = 'orphan', IPAddress = PrivateIPAddress_s; + let OrphanNICsAsPublicSource = OrphanNICs + | join kind=inner ( NSGRules ) on `$left.PublicIPAddress == `$right.SourceAddress + | extend NICState = 'orphan', IPAddress = PublicIPAddress; + let OrphanNICsAsPrivateDestination = OrphanNICs + | join kind=inner ( NSGRules ) on `$left.PrivateIPAddress_s == `$right.DestinationAddress + | extend NICState = 'orphan', IPAddress = PrivateIPAddress_s; + let OrphanNICsAsPublicDestination = OrphanNICs + | join kind=inner ( NSGRules ) on `$left.PublicIPAddress == `$right.DestinationAddress + | extend NICState = 'orphan', IPAddress = PublicIPAddress; + let RemovedNICsAsPrivateSource = RemovedNICs + | join kind=inner ( NSGRules ) on `$left.PrivateIPAddress_s == `$right.SourceAddress + | extend NICState = 'unexisting', IPAddress = PrivateIPAddress_s; + let RemovedNICsAsPublicSource = RemovedNICs + | join kind=inner ( NSGRules ) on `$left.PublicIPAddress == `$right.SourceAddress + | extend NICState = 'unexisting', IPAddress = PublicIPAddress; + let RemovedNICsAsPrivateDestination = RemovedNICs + | join kind=inner ( NSGRules ) on `$left.PrivateIPAddress_s == `$right.DestinationAddress + | extend NICState = 'unexisting', IPAddress = PrivateIPAddress_s; + let RemovedNICsAsPublicDestination = RemovedNICs + | join kind=inner ( NSGRules ) on `$left.PublicIPAddress == `$right.DestinationAddress + | extend NICState = 'unexisting', IPAddress = PublicIPAddress; + OrphanNICsAsPrivateSource + | union OrphanNICsAsPublicSource + | union OrphanNICsAsPrivateDestination + | union OrphanNICsAsPublicDestination + | union RemovedNICsAsPrivateSource + | union RemovedNICsAsPublicSource + | union RemovedNICsAsPrivateDestination + | union RemovedNICsAsPublicDestination + | where isnotempty(IPAddress) + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g + | distinct NSGId, NSGName, RuleName_s, SubscriptionGuid_g, SubscriptionName, ResourceGroupName_s, TenantGuid_g, Cloud_s, NICId, IPAddress, NICState, Tags_s +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.NSGId + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["nicId"] = $result.NICId + $additionalInfoDictionary["ipAddress"] = $result.IPAddress + $additionalInfoDictionary["nicState"] = $result.NICState + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Security" + ImpactedArea = "Microsoft.Network/networkSecurityGroups" + Impact = "Medium" + RecommendationType = "BestPractices" + RecommendationSubType = "NSGRuleForOrphanOrUnexistingNIC" + RecommendationSubTypeId = "3dc1d1f8-19ef-4572-9c9d-78d62831f55a" + RecommendationDescription = "NSG rules referring to orphan or unexisting NICs" + RecommendationAction = "Update or remove the NSG rule to improve your network security posture" + InstanceId = $result.NSGId + InstanceName = "$($result.NSGName)/$($result.RuleName_s)" + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "nsgrules-orphanunexistingnics-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for NSG rules referring orphan or removed Public IPs..." + +$baseQuery = @" + let PIPsToday = materialize( $publicIpsTableName + | where TimeGenerated > ago(1d) + | extend PublicIPId = tolower(InstanceId_s) + | distinct PublicIPId, AssociatedResourceId_s, AllocationMethod_s, IPAddress ); + let PIPsBefore = materialize( $publicIpsTableName + | where TimeGenerated < ago(1d) + | extend PublicIPId = tolower(InstanceId_s) + | distinct PublicIPId, IPAddress ); + let OrphanStaticPIPs = PIPsToday + | where isempty(AssociatedResourceId_s) and AllocationMethod_s == 'static'; + let OrphanDynamicPIPIDs = PIPsToday + | where isempty(AssociatedResourceId_s) and AllocationMethod_s == 'dynamic' + | distinct PublicIPId; + let PIPsTodayIds = PIPsToday | distinct PublicIPId; + let PIPsTodayIPs = PIPsToday | distinct IPAddress; + let OrphanDynamicPIPs = PIPsBefore + | where PublicIPId in (OrphanDynamicPIPIDs) and isnotempty(IPAddress) and IPAddress !in (PIPsTodayIPs); + let RemovedPIPs = PIPsBefore + | where PublicIPId !in (PIPsTodayIds) and isnotempty(IPAddress) and IPAddress !in (PIPsTodayIPs); + let NSGRules = materialize( $nsgRulesTableName + | where TimeGenerated > ago(1d) + | extend SourceAddresses = split(RuleSourceAddresses_s,',') + | mvexpand SourceAddresses + | extend SourceAddress = replace('/32','',tostring(SourceAddresses)) + | extend DestinationAddresses = split(RuleDestinationAddresses_s,',') + | mvexpand DestinationAddresses + | extend DestinationAddress = replace('/32','',tostring(DestinationAddresses)) + | project NSGId = InstanceId_s, RuleName_s, DestinationAddress, SourceAddress, SubscriptionGuid_g, Cloud_s, TenantGuid_g, ResourceGroupName_s, NSGName = NSGName_s, Tags_s); + let OrphanStaticPIPsAsSource = OrphanStaticPIPs + | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.SourceAddress + | extend PIPState = 'orphan'; + let OrphanStaticPIPsAsDestination = OrphanStaticPIPs + | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.DestinationAddress + | extend PIPState = 'orphan'; + let OrphanDynamicPIPsAsSource = OrphanDynamicPIPs + | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.SourceAddress + | extend PIPState = 'orphan'; + let OrphanDynamicPIPsAsDestination = OrphanDynamicPIPs + | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.DestinationAddress + | extend PIPState = 'orphan'; + let RemovedPIPsAsSource = RemovedPIPs + | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.SourceAddress + | extend PIPState = 'unexisting'; + let RemovedPIPsAsDestination = RemovedPIPs + | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.DestinationAddress + | extend PIPState = 'unexisting'; + OrphanStaticPIPsAsSource + | union OrphanDynamicPIPsAsSource + | union OrphanStaticPIPsAsDestination + | union OrphanDynamicPIPsAsDestination + | union RemovedPIPsAsSource + | union RemovedPIPsAsDestination + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g + | distinct NSGId, NSGName, RuleName_s, SubscriptionGuid_g, SubscriptionName, ResourceGroupName_s, TenantGuid_g, Cloud_s, PublicIPId, IPAddress, PIPState, AllocationMethod_s, Tags_s +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + switch ($result.Cloud_s) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + + $queryInstanceId = $result.NSGId + $detailsURL = "https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["publicIPId"] = $result.PublicIPId + $additionalInfoDictionary["ipAddress"] = $result.IPAddress + $additionalInfoDictionary["publicIPState"] = $result.PIPState + $additionalInfoDictionary["allocationMethod"] = $result.AllocationMethod_s + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Security" + ImpactedArea = "Microsoft.Network/networkSecurityGroups" + Impact = "High" + RecommendationType = "BestPractices" + RecommendationSubType = "NSGRuleForOrphanOrUnexistingPublicIP" + RecommendationSubTypeId = "fe40cbe7-bdee-4cce-b072-cf25e1247b7a" + RecommendationDescription = "NSG rules referring to orphan or unexisting Public IPs" + RecommendationAction = "Update or remove the NSG rule to improve your network security posture" + InstanceId = $result.NSGId + InstanceName = "$($result.NSGName)/$($result.RuleName_s)" + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "nsgrules-orphanunexistingpublicips-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +Write-Output "Looking for orphaned Public IPs..." + +$baseQuery = @" + let interval = 30d; + let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(30d) | summarize max(todatetime(Date_s)))); + let stime = etime-interval; + $publicIpsTableName + | where TimeGenerated > ago(1d) and isempty(AssociatedResourceId_s) + | distinct Name_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, AllocationMethod_s, Tags_s, Cloud_s + | join kind=leftouter ( + $consumptionTableName + | where todatetime(Date_s) between (stime..etime) + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on InstanceId_s + | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by Name_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, AllocationMethod_s, Tags_s, Cloud_s + | join kind=leftouter ( + $subscriptionsTableName + | where TimeGenerated > ago(1d) + | where ContainerType_s =~ 'microsoft.resources/subscriptions' + | project SubscriptionGuid_g, SubscriptionName = ContainerName_s + ) on SubscriptionGuid_g +"@ + +try +{ + $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics + if ($queryResults) + { + $results = [System.Linq.Enumerable]::ToArray($queryResults.Results) + } +} +catch +{ + Write-Warning -Message "Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery" + Write-Warning -Message $error[0] + $recommendationsErrors++ +} + +Write-Output "Query finished with $($results.Count) results." + +Write-Output "Query statistics: $($queryResults.Statistics.query)" + +# Build the recommendations objects + +$recommendations = @() +$datetime = (get-date).ToUniversalTime() +$timestamp = $datetime.ToString("yyyy-MM-ddTHH:mm:00.000Z") + +foreach ($result in $results) +{ + $queryInstanceId = $result.InstanceId_s + $queryText = @" + $publicIpsTableName + | where InstanceId_s == '$queryInstanceId' and isempty(AssociatedResourceId_s) + | distinct InstanceId_s, Name_s, AllocationMethod_s, SkuName_s, TimeGenerated + | summarize LastAttachedDate = min(TimeGenerated) by InstanceId_s, Name_s, AllocationMethod_s, SkuName_s + | join kind=leftouter ( + $consumptionTableName + | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s + ) on InstanceId_s + | summarize CostsSinceDetached = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > LastAttachedDate) by Name_s, LastAttachedDate, AllocationMethod_s, SkuName_s +"@ + $encodedQuery = [System.Uri]::EscapeDataString($queryText) + $detailsQueryStart = $deploymentDate + $detailsQueryEnd = $datetime.AddDays(8).ToString("yyyy-MM-dd") + switch ($cloudEnvironment) + { + "AzureCloud" { $azureTld = "com" } + "AzureChinaCloud" { $azureTld = "cn" } + "AzureUSGovernment" { $azureTld = "us" } + default { $azureTld = "com" } + } + $detailsURL = "https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z" + + $additionalInfoDictionary = @{} + + $additionalInfoDictionary["currentSku"] = $result.SkuName_s + $additionalInfoDictionary["allocationMethod"] = $result.AllocationMethod_s + $additionalInfoDictionary["CostsAmount"] = [double] $result.Last30DaysCost + $additionalInfoDictionary["savingsAmount"] = [double] $result.Last30DaysCost + + $fitScore = 5 + + $tags = @{} + + if (-not([string]::IsNullOrEmpty($result.Tags_s))) + { + $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';') + foreach ($tagPairString in $tagPairs) + { + $tagPair = $tagPairString.Split('=') + if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1]))) + { + $tagName = $tagPair[0].Trim() + $tagValue = $tagPair[1].Trim() + $tags[$tagName] = $tagValue + } + } + } + + $recommendation = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $result.Cloud_s + Category = "Cost" + ImpactedArea = "Microsoft.Network/publicIPAddresses" + Impact = "Low" + RecommendationType = "Saving" + RecommendationSubType = "OrphanedPublicIP" + RecommendationSubTypeId = "3125883f-8b9f-4bde-a0ff-6c739858c6e1" + RecommendationDescription = "Orphaned Public IP (without owner resource) incur in unnecessary costs" + RecommendationAction = "Delete the Public IP or change its configuration to dynamic allocation" + InstanceId = $result.InstanceId_s + InstanceName = $result.Name_s + AdditionalInfo = $additionalInfoDictionary + ResourceGroup = $result.ResourceGroupName_s + SubscriptionGuid = $result.SubscriptionGuid_g + SubscriptionName = $result.SubscriptionName + TenantGuid = $result.TenantGuid_g + FitScore = $fitScore + Tags = $tags + DetailsURL = $detailsURL + } + + $recommendations += $recommendation +} + +# Export the recommendations as JSON to blob storage + +$fileDate = $datetime.ToString("yyyy-MM-dd") +$jsonExportPath = "orphanedpublicips-$fileDate.json" +$recommendations | ConvertTo-Json | Out-File $jsonExportPath + +$jsonBlobName = $jsonExportPath +$jsonProperties = @{"ContentType" = "application/json"}; +Set-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Uploaded $jsonBlobName to Blob Storage..." + +Remove-Item -Path $jsonExportPath -Force + +$now = (Get-Date).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'") +Write-Output "[$now] Removed $jsonExportPath from local disk..." + +if ($recommendationsErrors -gt 0) +{ + throw "Some of the recommendations queries failed. Please, review the job logs for additional information." +} \ No newline at end of file diff --git a/docs/deploy/optimization-engine/0.4/runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1 new file mode 100644 index 000000000..5d58fcf54 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1 @@ -0,0 +1,223 @@ +param( + [Parameter(Mandatory = $false)] + [bool] $Simulate = $true +) + +$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)) +{ + $sqldatabase = "azureoptimization" +} +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RemediationLogsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "remediationlogs" +} + +$minFitScore = [double] (Get-AutomationVariable -Name "AzureOptimization_RemediateRightSizeMinFitScore" -ErrorAction SilentlyContinue) +if (-not($minFitScore -gt 0.0)) { + $minFitScore = 5.0 +} + +$minWeeksInARow = [int] (Get-AutomationVariable -Name "AzureOptimization_RemediateRightSizeMinWeeksInARow" -ErrorAction SilentlyContinue) +if (-not($minWeeksInARow -gt 0)) { + $minWeeksInARow = 4 +} + +$tagsFilter = Get-AutomationVariable -Name "AzureOptimization_RemediateRightSizeTagsFilter" -ErrorAction SilentlyContinue +# example: '[ { "tagName": "a", "tagValue": "b" }, { "tagName": "c", "tagValue": "d" } ]' +if (-not($tagsFilter)) { + $tagsFilter = '{}' +} +$tagsFilter = $tagsFilter | ConvertFrom-Json + +$rightSizeRecommendationId = Get-AutomationVariable -Name "AzureOptimization_RecommendationAdvisorCostRightSizeId" -ErrorAction SilentlyContinue +if (-not($rightSizeRecommendationId)) { + $rightSizeRecommendationId = 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974' +} + +$SqlTimeout = 0 +$recommendationsTable = "Recommendations" + +"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 + } +} + +# get reference to storage sink +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context + +Write-Output "Querying for right-size recommendations with fit score >= $minFitScore made consecutively for the last $minWeeksInARow weeks." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = @" + SELECT InstanceId, Cloud, TenantGuid, JSON_VALUE(AdditionalInfo, '`$.currentSku') AS CurrentSKU, JSON_VALUE(AdditionalInfo, '`$.targetSku') AS TargetSKU, COUNT(InstanceId) + FROM [dbo].[$recommendationsTable] + WHERE RecommendationSubTypeId = '$rightSizeRecommendationId' AND FitScore >= $minFitScore AND GeneratedDate >= GETDATE()-(7*$minWeeksInARow) + GROUP BY InstanceId, Cloud, TenantGuid, JSON_VALUE(AdditionalInfo, '`$.currentSku'), JSON_VALUE(AdditionalInfo, '`$.targetSku') + HAVING COUNT(InstanceId) >= $minWeeksInARow +"@ + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $vmsToRightSize = New-Object System.Data.DataTable + $sqlAdapter.Fill($vmsToRightSize) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +Write-Output "Found $($vmsToRightSize.Rows.Count) remediation opportunities." + +$Conn.Close() +$Conn.Dispose() + +$logEntries = @() + +$datetime = (get-date).ToUniversalTime() +$hour = $datetime.Hour +$min = $datetime.Minute +$timestamp = $datetime.ToString("yyyy-MM-ddT$($hour):$($min):00.000Z") + +$ctx = Get-AzContext + +foreach ($vm in $vmsToRightSize.Rows) +{ + $isEligible = $false + $logDetails = $null + if ([string]::IsNullOrEmpty($tagsFilter)) + { + $isEligible = $true + } + else + { + $vmTags = Get-AzTag -ResourceId $vm.InstanceId -ErrorAction SilentlyContinue + if ($vmTags) + { + foreach ($tagFilter in $tagsFilter) + { + if ($vmTags.Properties.TagsProperty.($tagFilter.tagName) -eq $tagFilter.tagValue) + { + $isEligible = $true + } + else + { + $isEligible = $false + break + } + } + } + } + + $subscriptionId = $vm.InstanceId.Split("/")[2] + $resourceGroup = $vm.InstanceId.Split("/")[4] + $instanceName = $vm.InstanceId.Split("/")[8] + + if ($isEligible) + { + Write-Output "Downsizing (SIMULATE=$Simulate) $($vm.InstanceId) to $($vm.TargetSKU)..." + if (-not($Simulate) -and $ctx.Environment.Name -eq $vm.Cloud -and $ctx.Tenant.Id -eq $vm.TenantGuid) + { + if ($ctx.Subscription.Id -ne $subscriptionId) + { + Select-AzSubscription -SubscriptionId $subscriptionId | Out-Null + $ctx = Get-AzContext + } + $vmObj = Get-AzVM -ResourceGroupName $resourceGroup -VMName $instanceName -ErrorAction SilentlyContinue + if ($vmObj) + { + $vmObj.HardwareProfile.VmSize = $vm.TargetSKU + Update-AzVM -VM $vmObj -ResourceGroupName $resourceGroup + } + else + { + Write-Output "Skipping as VM was already removed." + } + } + else + { + Write-Output "Did not apply remediation." + } + } + + $logDetails = @{ + IsEligible = $isEligible + CurrentSku = $vm.CurrentSKU + TargetSku = $vm.TargetSKU + } + + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $vm.Cloud + TenantGuid = $vm.TenantGuid + SubscriptionGuid = $subscriptionId + ResourceGroupName = $resourceGroup.ToLower() + InstanceName = $instanceName.ToLower() + InstanceId = $vm.InstanceId.ToLower() + Simulate = $Simulate + LogDetails = $logDetails | ConvertTo-Json -Compress + RecommendationSubTypeId = $rightSizeRecommendationId + } + + $logEntries += $logentry +} + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-rightsizefiltered.csv" + +$logEntries | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force diff --git a/docs/deploy/optimization-engine/0.4/runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1 new file mode 100644 index 000000000..3fc61eb5d --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1 @@ -0,0 +1,285 @@ +param( + [Parameter(Mandatory = $false)] + [bool] $Simulate = $true +) + +$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)) +{ + $sqldatabase = "azureoptimization" +} +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RemediationLogsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "remediationlogs" +} + +$minFitScore = [double] (Get-AutomationVariable -Name "AzureOptimization_RemediateLongDeallocatedVMsMinFitScore" -ErrorAction SilentlyContinue) +if (-not($minFitScore -gt 0.0)) { + $minFitScore = 5.0 +} + +$minWeeksInARow = [int] (Get-AutomationVariable -Name "AzureOptimization_RemediateLongDeallocatedVMsMinWeeksInARow" -ErrorAction SilentlyContinue) +if (-not($minWeeksInARow -gt 0)) { + $minWeeksInARow = 4 +} + +$tagsFilter = Get-AutomationVariable -Name "AzureOptimization_RemediateLongDeallocatedVMsTagsFilter" -ErrorAction SilentlyContinue +# example: '[ { "tagName": "a", "tagValue": "b" }, { "tagName": "c", "tagValue": "d" } ]' +if (-not($tagsFilter)) { + $tagsFilter = '{}' +} +$tagsFilter = $tagsFilter | ConvertFrom-Json + +$recommendationId = Get-AutomationVariable -Name "AzureOptimization_RecommendationLongDeallocatedVMsId" -ErrorAction SilentlyContinue +if (-not($recommendationId)) { + $recommendationId = 'c320b790-2e58-452a-aa63-7b62c383ad8a' +} + +$SqlTimeout = 0 +$recommendationsTable = "Recommendations" + +"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 + } +} + +# get reference to storage sink +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context + +Write-Output "Querying for long-deallocated recommendations with fit score >= $minFitScore made consecutively for the last $minWeeksInARow weeks." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = @" + SELECT InstanceId, Cloud, TenantGuid, COUNT(InstanceId) + FROM [dbo].[$recommendationsTable] + WHERE RecommendationSubTypeId = '$recommendationId' AND FitScore >= $minFitScore AND GeneratedDate >= GETDATE()-(7*$minWeeksInARow) + GROUP BY InstanceId, Cloud, TenantGuid + HAVING COUNT(InstanceId) >= $minWeeksInARow +"@ + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $deallocatedVMs = New-Object System.Data.DataTable + $sqlAdapter.Fill($deallocatedVMs) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +Write-Output "Found $($deallocatedVMs.Rows.Count) remediation opportunities." + +$Conn.Close() +$Conn.Dispose() + +$logEntries = @() + +$datetime = (get-date).ToUniversalTime() +$hour = $datetime.Hour +$min = $datetime.Minute +$timestamp = $datetime.ToString("yyyy-MM-ddT$($hour):$($min):00.000Z") + +$ctx = Get-AzContext + +foreach ($vm in $deallocatedVMs.Rows) +{ + $isEligible = $false + $logDetails = $null + if ([string]::IsNullOrEmpty($tagsFilter)) + { + $isEligible = $true + } + else + { + $vmTags = Get-AzTag -ResourceId $vm.InstanceId -ErrorAction SilentlyContinue + if ($vmTags) + { + foreach ($tagFilter in $tagsFilter) + { + if ($vmTags.Properties.TagsProperty.($tagFilter.tagName) -eq $tagFilter.tagValue) + { + $isEligible = $true + } + else + { + $isEligible = $false + break + } + } + } + } + + $subscriptionId = $vm.InstanceId.Split("/")[2] + $resourceGroup = $vm.InstanceId.Split("/")[4] + $instanceName = $vm.InstanceId.Split("/")[8] + + if ($isEligible) + { + $vmState = "Unknown" + $hasManagedDisks = $false + $osDiskSkuName = "Unknown" + $dataDisksSkuNames = "Unknown" + + Write-Output "Downsizing (SIMULATE=$Simulate) $($vm.InstanceId) disks to Standard_LRS..." + if ($ctx.Environment.Name -eq $vm.Cloud -and $ctx.Tenant.Id -eq $vm.TenantGuid) + { + if ($ctx.Subscription.Id -ne $subscriptionId) + { + Select-AzSubscription -SubscriptionId $subscriptionId | Out-Null + $ctx = Get-AzContext + } + $vmObj = Get-AzVM -ResourceGroupName $resourceGroup -VMName $instanceName -Status -ErrorAction SilentlyContinue + if ($vmObj.PowerState -eq 'VM deallocated') + { + $vmState = "Deallocated" + $osDiskId = $vmObj.StorageProfile.OsDisk.ManagedDisk.Id + $dataDiskIds = $vmObj.StorageProfile.DataDisks.ManagedDisk.Id + if ($osDiskId) + { + $hasManagedDisks = $true + $disk = Get-AzDisk -ResourceGroupName $osDiskId.Split("/")[4] -DiskName $osDiskId.Split("/")[8] + $osDiskSkuName = $disk.Sku.Name + if (-not($Simulate) -and $disk.Sku.Name -ne 'Standard_LRS') + { + $disk.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new('Standard_LRS') + $disk | Update-AzDisk | Out-Null + } + else + { + Write-Output "Skipping as OS disk is already HDD." + } + foreach ($dataDiskId in $dataDiskIds) + { + $disk = Get-AzDisk -ResourceGroupName $dataDiskId.Split("/")[4] -DiskName $dataDiskId.Split("/")[8] + if ($dataDisksSkuNames -eq 'Unknown') + { + $dataDisksSkuNames = $disk.Sku.Name + } + else + { + if ($dataDisksSkuNames -notlike "*$($disk.Sku.Name)*") + { + $dataDisksSkuNames += ",$($disk.Sku.Name)" + } + } + + if (-not($Simulate) -and $disk.Sku.Name -ne 'Standard_LRS') + { + $disk.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new('Standard_LRS') + $disk | Update-AzDisk | Out-Null + } + else + { + Write-Output "Skipping as Data disk is already HDD." + } + } + } + else + { + Write-Output "Skipping as disks are not Managed Disks." + $hasManagedDisks = $false + } + } + else + { + if ($vmObj) + { + Write-Output "Skipping as VM is not deallocated." + $vmState = "Running" + } + else + { + Write-Output "Skipping as VM was already removed." + $vmState = "Removed" + } + } + } + else + { + Write-Output "Could not apply remediation as VM is in another cloud/tenant." + } + } + + $logDetails = @{ + IsEligible = $isEligible + VMState = $vmState + HasManagedDisks = $hasManagedDisks + OsDiskSkuName = $osDiskSkuName + DataDisksSkuName = $dataDisksSkuNames + } + + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $vm.Cloud + TenantGuid = $vm.TenantGuid + SubscriptionGuid = $subscriptionId + ResourceGroupName = $resourceGroup.ToLower() + InstanceName = $instanceName.ToLower() + InstanceId = $vm.InstanceId.ToLower() + Simulate = $Simulate + LogDetails = $logDetails | ConvertTo-Json -Compress + RecommendationSubTypeId = $recommendationId + } + + $logEntries += $logentry +} + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-longdeallocatedvmsfiltered.csv" + +$logEntries | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force diff --git a/docs/deploy/optimization-engine/0.4/runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1 b/docs/deploy/optimization-engine/0.4/runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1 new file mode 100644 index 000000000..68ed719d6 --- /dev/null +++ b/docs/deploy/optimization-engine/0.4/runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1 @@ -0,0 +1,264 @@ +param( + [Parameter(Mandatory = $false)] + [bool] $Simulate = $true +) + +$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)) +{ + $sqldatabase = "azureoptimization" +} +$storageAccountSink = Get-AutomationVariable -Name "AzureOptimization_StorageSink" +$storageAccountSinkRG = Get-AutomationVariable -Name "AzureOptimization_StorageSinkRG" +$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name "AzureOptimization_StorageSinkSubId" +$storageAccountSinkContainer = Get-AutomationVariable -Name "AzureOptimization_RemediationLogsContainer" -ErrorAction SilentlyContinue +if ([string]::IsNullOrEmpty($storageAccountSinkContainer)) { + $storageAccountSinkContainer = "remediationlogs" +} + +$minFitScore = [double] (Get-AutomationVariable -Name "AzureOptimization_RemediateUnattachedDisksMinFitScore" -ErrorAction SilentlyContinue) +if (-not($minFitScore -gt 0.0)) { + $minFitScore = 5.0 +} + +$minWeeksInARow = [int] (Get-AutomationVariable -Name "AzureOptimization_RemediateUnattachedDisksMinWeeksInARow" -ErrorAction SilentlyContinue) +if (-not($minWeeksInARow -gt 0)) { + $minWeeksInARow = 4 +} + +$tagsFilter = Get-AutomationVariable -Name "AzureOptimization_RemediateUnattachedDisksTagsFilter" -ErrorAction SilentlyContinue +# example: '[ { "tagName": "a", "tagValue": "b" }, { "tagName": "c", "tagValue": "d" } ]' +if (-not($tagsFilter)) { + $tagsFilter = '{}' +} +$tagsFilter = $tagsFilter | ConvertFrom-Json + +$remediationAction = Get-AutomationVariable -Name "AzureOptimization_RemediateUnattachedDisksAction" -ErrorAction SilentlyContinue # Delete / Downsize +if (-not($remediationAction)) { + $remediationAction = "Delete" +} + +$recommendationId = Get-AutomationVariable -Name "AzureOptimization_RecommendationUnattachedDisksId" -ErrorAction SilentlyContinue +if (-not($recommendationId)) { + $recommendationId = 'c84d5e86-e2d6-4d62-be7c-cecfbd73b0db' +} + +$SqlTimeout = 0 +$recommendationsTable = "Recommendations" + +"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 + } +} + +# get reference to storage sink +Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId +$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context + +Write-Output "Querying for unattached disks recommendations with fit score >= $minFitScore made consecutively for the last $minWeeksInARow weeks." + +$tries = 0 +$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;") + $Conn.Open() + $Cmd=new-object system.Data.SqlClient.SqlCommand + $Cmd.Connection = $Conn + $Cmd.CommandTimeout = $SqlTimeout + $Cmd.CommandText = @" + SELECT InstanceId, Cloud, TenantGuid, COUNT(InstanceId) + FROM [dbo].[$recommendationsTable] + WHERE RecommendationSubTypeId = '$recommendationId' AND FitScore >= $minFitScore AND GeneratedDate >= GETDATE()-(7*$minWeeksInARow) + GROUP BY InstanceId, Cloud, TenantGuid + HAVING COUNT(InstanceId) >= $minWeeksInARow +"@ + $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter + $sqlAdapter.SelectCommand = $Cmd + $unattachedDisks = New-Object System.Data.DataTable + $sqlAdapter.Fill($unattachedDisks) | Out-Null + $connectionSuccess = $true + } + catch { + Write-Output "Failed to contact SQL at try $tries." + Write-Output $Error[0] + Start-Sleep -Seconds ($tries * 20) + } +} while (-not($connectionSuccess) -and $tries -lt 3) + +if (-not($connectionSuccess)) +{ + throw "Could not establish connection to SQL." +} + +Write-Output "Found $($unattachedDisks.Rows.Count) remediation opportunities." + +$Conn.Close() +$Conn.Dispose() + +$logEntries = @() + +$datetime = (get-date).ToUniversalTime() +$hour = $datetime.Hour +$min = $datetime.Minute +$timestamp = $datetime.ToString("yyyy-MM-ddT$($hour):$($min):00.000Z") + +$ctx = Get-AzContext + +foreach ($disk in $unattachedDisks.Rows) +{ + $isEligible = $false + $logDetails = $null + if ([string]::IsNullOrEmpty($tagsFilter)) + { + $isEligible = $true + } + else + { + $diskTags = Get-AzTag -ResourceId $disk.InstanceId -ErrorAction SilentlyContinue + if ($diskTags) + { + foreach ($tagFilter in $tagsFilter) + { + if ($diskTags.Properties.TagsProperty.($tagFilter.tagName) -eq $tagFilter.tagValue) + { + $isEligible = $true + } + else + { + $isEligible = $false + break + } + } + } + } + + $subscriptionId = $disk.InstanceId.Split("/")[2] + $resourceGroup = $disk.InstanceId.Split("/")[4] + $instanceName = $disk.InstanceId.Split("/")[8] + + if ($isEligible) + { + $diskState = "Unknown" + $currentSku = "Unknown" + + Write-Output "Performing $remediationAction action (SIMULATE=$Simulate) on $($disk.InstanceId) disk..." + if ($ctx.Environment.Name -eq $disk.Cloud -and $ctx.Tenant.Id -eq $disk.TenantGuid) + { + if ($ctx.Subscription.Id -ne $subscriptionId) + { + Select-AzSubscription -SubscriptionId $subscriptionId | Out-Null + $ctx = Get-AzContext + } + $diskObj = Get-AzDisk -ResourceGroupName $resourceGroup -DiskName $instanceName -ErrorAction SilentlyContinue + if (-not($diskObj.ManagedBy)) + { + $diskState = "Unattached" + $currentSku = $diskObj.Sku.Name + if ($remediationAction -eq "Downsize") + { + if (-not($Simulate) -and $diskObj.Sku.Name -ne 'Standard_LRS') + { + $diskObj.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new('Standard_LRS') + $diskObj | Update-AzDisk | Out-Null + } + else + { + Write-Output "Skipping as disk is already HDD." + } + } + elseif ($remediationAction -eq "Delete") + { + if (-not($Simulate)) + { + Remove-AzDisk -ResourceGroupName $resourceGroup -DiskName $instanceName -Force | Out-Null + } + } + else + { + Write-Output "Skipping as action is not supported." + } + } + else + { + if ($diskObj) + { + Write-Output "Skipping as disk is not unattached." + $diskState = "Attached" + } + else + { + Write-Output "Skipping as disk was already removed." + $diskState = "Removed" + } + } + } + else + { + Write-Output "Could not apply remediation as disk is in another cloud/tenant." + } + } + + $logDetails = @{ + IsEligible = $isEligible + RemediationAction = $remediationAction + DiskState = $diskState + CurrentSku = $currentSku + } + + $logentry = New-Object PSObject -Property @{ + Timestamp = $timestamp + Cloud = $disk.Cloud + TenantGuid = $disk.TenantGuid + SubscriptionGuid = $subscriptionId + ResourceGroupName = $resourceGroup.ToLower() + InstanceName = $instanceName.ToLower() + InstanceId = $disk.InstanceId.ToLower() + Simulate = $Simulate + LogDetails = $logDetails | ConvertTo-Json -Compress + RecommendationSubTypeId = $recommendationId + } + + $logEntries += $logentry +} + +$today = $datetime.ToString("yyyyMMdd") +$csvExportPath = "$today-unattacheddisksfiltered.csv" + +$logEntries | Export-Csv -Path $csvExportPath -NoTypeInformation + +$csvBlobName = $csvExportPath + +$csvProperties = @{"ContentType" = "text/csv"}; + +Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force diff --git a/docs/deploy/optimization-workbook-0.4.json b/docs/deploy/optimization-workbook-0.4.json new file mode 100644 index 000000000..fdbd337d3 --- /dev/null +++ b/docs/deploy/optimization-workbook-0.4.json @@ -0,0 +1,11403 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.24.24.22086", + "templateHash": "18432665164063758796" + } + }, + "parameters": { + "displayName": { + "type": "string", + "defaultValue": "Cost optimization", + "metadata": { + "description": "Optional. Display name for the workbook used in the Gallery. Must be unique in the resource group." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location of the resources. Default: Same as deployment. See https://aka.ms/azureregions." + } + }, + "description": { + "type": "string", + "defaultValue": "Reports to help you optimize your cost.", + "metadata": { + "description": "Optional. Workbook description." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Tags for all resources." + } + }, + "enableDefaultTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable telemetry to track anonymous module usage trends, monitor for bugs, and improve future releases." + } + } + }, + "variables": { + "$fxv#0": { + "version": "Notebook/1.0", + "items": [ + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "ca40468d-4518-43bf-ac6e-0a11d7331e12", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Overview", + "subTarget": "Welcome", + "style": "link" + }, + { + "id": "f280fc2a-f42a-42a4-ad4b-be37ab3e8b48", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Rate optimization", + "subTarget": "RateOptimization", + "style": "link" + }, + { + "id": "26b3c7ef-1a00-4a3f-a773-677f00db9343", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "Usage optimization", + "subTarget": "UsageOptimization", + "style": "link" + } + ] + }, + "name": "links - MainTabs" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "28fdc6e9-2946-4016-8e75-b812ff8f853d", + "cellValue": "SelectedRateOptimizationTab", + "linkTarget": "parameter", + "linkLabel": "Compute", + "subTarget": "Compute", + "style": "link" + }, + { + "id": "4e0a0d2d-1d61-4d04-a35d-93e38d1bac29", + "cellValue": "SelectedRateOptimizationTab", + "linkTarget": "parameter", + "linkLabel": "Storage", + "subTarget": "Storage", + "style": "link" + }, + { + "id": "22d04714-50f4-4d72-baec-e8ccddddc7f3", + "cellValue": "SelectedRateOptimizationTab", + "linkTarget": "parameter", + "linkLabel": "Networking", + "subTarget": "Networking", + "style": "link" + }, + { + "id": "eaedbb0e-e895-4940-80ad-f743c3ab1041", + "cellValue": "SelectedRateOptimizationTab", + "linkTarget": "parameter", + "linkLabel": "Top 10 services", + "subTarget": "Top10Services", + "style": "link" + } + ] + }, + "name": "links - UsageOptimization tabs" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "51aa3a9b-14e0-4c22-a60d-abdbf8813f00", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "label": " Subscription", + "value": [ + "value::all" + ] + }, + { + "id": "f342a111-002a-47fd-807f-0d4ccac0618a", + "version": "KqlParameterItem/1.0", + "name": "ResourceGroup", + "label": "Resource Group", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "resources\r\n| distinct resourceGroup", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + { + "id": "2336f06b-ddaa-4a9e-b72f-a2bec1ea84a9", + "version": "KqlParameterItem/1.0", + "name": "SingleSubHidden", + "type": 1, + "isRequired": true, + "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Hidden Subscription" + }, + { + "id": "d6776ffe-e4f6-4c08-8f9e-a2fe2b3b6634", + "version": "KqlParameterItem/1.0", + "name": "TagName", + "type": 2, + "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Name" + }, + { + "id": "f73dc4a1-ef8b-45c5-a30b-a11bb077a3cc", + "version": "KqlParameterItem/1.0", + "name": "TagValue", + "type": 2, + "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Value" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "75", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "UsageOptimization" + }, + "name": "parameters - Filters" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadFromTemplateId": "", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "37ceb1c3-3930-4689-a90b-22f26e42bd81", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "label": " Subscription" + }, + { + "id": "08f5fe68-c2e3-4882-9300-b3e33f572dfe", + "version": "KqlParameterItem/1.0", + "name": "ResourceGroup", + "label": "Resource Group", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "resources\r\n| distinct resourceGroup", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + { + "id": "4fea3013-df84-4930-a453-8a6bd0375130", + "version": "KqlParameterItem/1.0", + "name": "SingleSubHidden", + "type": 1, + "isRequired": true, + "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Hidden Subscription" + }, + { + "id": "8412f39d-ee67-4979-b887-47463b8848c2", + "version": "KqlParameterItem/1.0", + "name": "TagName", + "type": 2, + "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Name" + }, + { + "id": "50c68f38-13a0-4aff-a259-4426c83b7cc0", + "version": "KqlParameterItem/1.0", + "name": "TagValue", + "type": 2, + "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Value" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "75", + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "CostInformation" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "Welcome" + } + ], + "name": "parameters - Filters" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "eae8a0d2-14e6-4cd1-a2d2-fd6b207cf517", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "eae8a0d2-14e6-4cd1-a2d2-fd6b207cf517", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Azure App Service", + "subTarget": "webapp", + "style": "link" + }, + { + "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Azure Kubernetes Service", + "subTarget": "AKS", + "style": "link" + }, + { + "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Azure Synapse", + "subTarget": "Synapse", + "preText": "VM", + "style": "link" + }, + { + "id": "820d600c-8ab3-4622-ba5a-52f60574d111", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Monitoring", + "subTarget": "Monitoring", + "style": "link" + } + ] + }, + "name": "links - Storage" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Synapse\r\nA Synapse Workspace is considered unused if it doesn't have any SQL pools attached to it\r\n", + "style": "upsell" + }, + "name": "Synapse" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| where type =~ 'Microsoft.Synapse/workspaces'\r\n| join kind=leftouter (\r\n Resources\r\n | where type =~ 'Microsoft.Synapse/workspaces/sqlPools'\r\n | extend SynapseWorkspaceResourceId = substring(id, 0, indexof(id, '/sqlPools/'))\r\n | summarize sqlpoolCount = count() by SynapseWorkspaceResourceId\r\n) on $left.id == $right.SynapseWorkspaceResourceId\r\n| join kind=leftouter (\r\n Resources\r\n | where type =~ 'Microsoft.Synapse/workspaces/bigDataPools'\r\n | extend SynapseWorkspaceResourceId = substring(id, 0, indexof(id, '/bigDataPools/'))\r\n | summarize bigdatapoolCount = count() by SynapseWorkspaceResourceId\r\n) on $left.id == $right.SynapseWorkspaceResourceId\r\n| where (isnull(sqlpoolCount) or sqlpoolCount == 0) and (isnull(bigdatapoolCount) or bigdatapoolCount == 0)\r\n| project id, resourceGroup, subscriptionId, location\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Unused Synapase workspace", + "noDataMessage": "All of your Synapse workspaces have SQL pools.", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + }, + { + "columnMatch": "storageaccount", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "insights", + "linkIsContextBlade": true, + "showIcon": true + } + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + } + ] + } + }, + "name": "Get-Synapse1" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "Synapse" + }, + "name": "SynapseGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Azure Kubernetes Service\r\n- Enable cluster autoscaler to automatically adjust the number of agent nodes in response to resource constraints\r\n\r\n- Consider using Azure Spot VMs for workloads that can handle interruptions, early terminations, or evictions. For example, workloads such as batch processing jobs, development and testing environments, and large compute workloads may be good candidates to be scheduled on a spot node pool.\r\n\r\n- Utilize the Horizontal pod autoscaler to adjust the number of pods in a deployment depending on CPU utilization or other select metrics.\r\n\r\n- Use the Start/Stop feature in Azure Kubernetes Services (AKS).\r\n\r\n", + "style": "upsell" + }, + "name": "Azure Kubernetes Service" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "\tresources\r\n | where resourceGroup in ({ResourceGroup})\r\n\t| where type == \"microsoft.containerservice/managedclusters\"\r\n\t| extend AKSname=name,location=location,Sku=tostring(sku.name),Tier=tostring(sku.tier),AgentPoolProfiles=properties.agentPoolProfiles\r\n | project id,AKSname,resourceGroup,subscriptionId,Sku,Tier,AgentPoolProfiles,location\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id\r\n\t| mvexpand AgentPoolProfiles\r\n\t| extend ProfileName = tostring(AgentPoolProfiles.name) ,mode=AgentPoolProfiles.mode,AutoScaleEnabled = AgentPoolProfiles.enableAutoScaling ,SpotVM=AgentPoolProfiles.scaleSetPriority, VMSize=tostring(AgentPoolProfiles.vmSize),minCount=tostring(AgentPoolProfiles.minCount),maxCount=tostring(AgentPoolProfiles.maxCount) , nodeCount=tostring(AgentPoolProfiles.['count'])\r\n | project id,ProfileName,Sku,Tier,mode,AutoScaleEnabled,SpotVM, VMSize,nodeCount,minCount,maxCount,location,resourceGroup,subscriptionId,AKSname\r\n \r\n", + "size": 0, + "noDataMessage": "You have no AKS clusters!", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "AKS Name", + "formatter": 1 + }, + { + "columnMatch": "id", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "Insights", + "showIcon": true + } + }, + { + "columnMatch": "mode", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "System", + "representation": "Gear", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "User", + "representation": "Person", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "AutoScaleEnabled", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "true", + "representation": "success", + "text": "Enabled" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "disabled", + "text": "Disabled" + } + ] + } + }, + { + "columnMatch": "SpotVM", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "is Empty", + "representation": "2", + "text": "{0}{1}Not Spot VM" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "AKSname", + "formatter": 5 + } + ], + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "AKSname" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "id", + "label": "ID" + }, + { + "columnId": "ProfileName", + "label": "Profile Name" + }, + { + "columnId": "Sku", + "label": "SKU" + }, + { + "columnId": "Tier", + "label": "SKU Tier" + }, + { + "columnId": "mode", + "label": "Mode" + }, + { + "columnId": "AutoScaleEnabled", + "label": "Autoscale enabled?" + }, + { + "columnId": "SpotVM", + "label": "Spot VM?" + }, + { + "columnId": "VMSize", + "label": "VM SKU" + }, + { + "columnId": "nodeCount", + "label": "Number of nodes" + }, + { + "columnId": "minCount", + "label": "Minimum nodes" + }, + { + "columnId": "maxCount", + "label": "Maximum nodes" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + }, + { + "columnId": "AKSname", + "label": "AKS Name" + } + ] + } + }, + "name": "Get-All-AKS" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "AKS" + }, + "name": "AKSGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Azure App Service\r\n## Save with Premium v3 reserved instances\r\nWhen you commit to an Azure App Service Premium v3 reserved instance you can save money. The reservation discount is applied automatically to the number of running instances that match the reservation scope and attributes - you don't need to assign a reservation to a specific instance to get the discounts.\r\n\r\n## Determine the right reserved instance size before you buy\r\nBefore you buy a reservation, you should determine the size of the Premium v3 reserved instance that you need. The following sections will help you determine the right Premium v3 reserved instance size.\r\n\r\n## Use Autoscale appropriately\r\nAutoscale can be used to provision resources for when they're needed or on demand, which allows you to minimize costs when your environment is idle.\r\n", + "style": "upsell" + }, + "name": "Azure App Service" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'Microsoft.Web/sites'\r\n| extend WebAppRG=resourceGroup, WebAppName=name, AppServicePlan=tostring(properties.serverFarmId), SKU=tostring(properties.sku), Type=kind, Status=tostring(properties.state), WebAppLocation=location, SubscriptionName=subscriptionId\r\n| project id,WebAppName, Type, Status, WebAppLocation, AppServicePlan, WebAppRG,SubscriptionName\r\n| order by id asc\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "Never" + }, + "name": "query - WebFunctionStatus" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type == \"microsoft.web/serverfarms\" and sku.tier !~ 'Free'\r\n| extend planId=tolower(tostring(id)),skuname = tostring(sku.name) , skutier = tostring(sku.tier), workers=tostring(properties.numberOfWorkers),webRG=resourceGroup,maxworkers=tostring(properties.maximumNumberOfWorkers), Sites=tostring(properties.numberOfSites), SubscriptionName=subscriptionId\r\n| project planId, name, skuname, skutier, workers, maxworkers, webRG, Sites, SubscriptionName\r\n| join kind=leftouter (resources | where type ==\"microsoft.insights/autoscalesettings\" | project planId=tolower(tostring(properties.targetResourceUri)), PredictiveAutoscale=properties.predictiveAutoscalePolicy.scaleMode, AutoScaleProfiles=properties.profiles,resourceGroup) on planId\r\n", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "Never" + }, + "name": "query - AppServiceplandetails" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\",\"mergeType\":\"inner\",\"leftTable\":\"query - AppServiceplandetails\",\"rightTable\":\"query - WebFunctionStatus\",\"leftColumn\":\"planId\",\"rightColumn\":\"AppServicePlan\"}],\"projectRename\":[{\"originalName\":\"[query - AppServiceplandetails].type\",\"mergedName\":\"type\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].tenantId\",\"mergedName\":\"tenantId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].kind\",\"mergedName\":\"kind\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].location\",\"mergedName\":\"location\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].managedBy\",\"mergedName\":\"managedBy\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].sku\",\"mergedName\":\"sku\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].plan\",\"mergedName\":\"plan\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].properties\",\"mergedName\":\"properties\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].tags\",\"mergedName\":\"tags\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].identity\",\"mergedName\":\"identity\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].zones\",\"mergedName\":\"zones\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].extendedLocation\",\"mergedName\":\"extendedLocation\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].planId\",\"mergedName\":\"planId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - WebFunctionStatus].id\",\"mergedName\":\"id\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].name\",\"mergedName\":\"name\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].Status\",\"mergedName\":\"Status\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].Type\",\"mergedName\":\"Type\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].skuname\",\"mergedName\":\"skuname\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].skutier\",\"mergedName\":\"skutier\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].PredictiveAutoscale\",\"mergedName\":\"PredictiveAutoscale\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].AutoScaleProfiles\",\"mergedName\":\"AutoScaleProfiles\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].workers\",\"mergedName\":\"workers\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].maxworkers\",\"mergedName\":\"maxworkers\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].webRG\",\"mergedName\":\"webRG\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].planId1\",\"mergedName\":\"planId1\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].resourceGroup\",\"mergedName\":\"resourceGroup\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].WebAppName\",\"mergedName\":\"WebAppName\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].WebAppLocation\",\"mergedName\":\"WebAppLocation\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].AppServicePlan\",\"mergedName\":\"AppServicePlan\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].WebAppRG\",\"mergedName\":\"WebAppRG\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].Sites\",\"mergedName\":\"Sites\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - WebFunctionStatus].SubscriptionName\"},{\"originalName\":\"[query - WebFunctionStatus].id1\"},{\"originalName\":\"[query - AppServiceplandetails].resourceGroup1\"}]}", + "size": 0, + "title": "Web Apps", + "noDataMessage": "You have no WebApps!", + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Name", + "formatter": 1 + }, + { + "columnMatch": "SubscriptionName", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "name", + "formatter": 5 + }, + { + "columnMatch": "Status", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "Running", + "representation": "success", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Stopped", + "representation": "disabled", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "webRG", + "formatter": 5 + }, + { + "columnMatch": "planId1", + "formatter": 5 + }, + { + "columnMatch": "resourceGroup", + "formatter": 5 + }, + { + "columnMatch": "WebAppName", + "formatter": 5 + }, + { + "columnMatch": "AppServicePlan", + "formatter": 5 + }, + { + "columnMatch": "WebAppRG", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 1 + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "name" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "planId", + "label": "Plan ID" + }, + { + "columnId": "id", + "label": "ID" + }, + { + "columnId": "name", + "label": "Name" + }, + { + "columnId": "skuname", + "label": "SKU" + }, + { + "columnId": "skutier", + "label": "SKU Tier" + }, + { + "columnId": "PredictiveAutoscale", + "label": "Autoscale Enabled?" + }, + { + "columnId": "AutoScaleProfiles", + "label": "Autoscale Profile" + }, + { + "columnId": "workers", + "label": "Workers" + }, + { + "columnId": "maxworkers", + "label": "Max. Workers" + }, + { + "columnId": "webRG", + "label": "Application Resource Group" + }, + { + "columnId": "WebAppName", + "label": "Application Name" + }, + { + "columnId": "WebAppLocation", + "label": "Application Location" + }, + { + "columnId": "AppServicePlan", + "label": "App Service Plan" + }, + { + "columnId": "WebAppRG", + "label": "Application Resource Group" + } + ] + } + }, + "name": "Get-Idle-WebApp" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type == \"microsoft.web/serverfarms\" and properties.numberOfSites == \"0\"\r\n| extend id, planId=tolower(tostring(id)),skuname = tostring(sku.name) , skutier = tostring(sku.tier), workers=tostring(properties.numberOfWorkers),webRG=resourceGroup,maxworkers=tostring(properties.maximumNumberOfWorkers), Sites=tostring(properties.numberOfSites), SubscriptionName=subscriptionId\r\n| project id, planId, name, skuname, skutier, workers, maxworkers, webRG, Sites, SubscriptionName\r\n| join kind=leftouter (resources | where type ==\"microsoft.insights/autoscalesettings\" | project planId=tolower(tostring(properties.targetResourceUri)), PredictiveAutoscale=properties.predictiveAutoscalePolicy.scaleMode, AutoScaleProfiles=properties.profiles,resourceGroup) on planId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n ) on id", + "size": 0, + "noDataMessage": "All of your App Service's plan have at least one website.", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "name", + "formatter": 5 + }, + { + "columnMatch": "maxworkers", + "formatter": 5 + }, + { + "columnMatch": "SubscriptionName", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "planId1", + "formatter": 5 + }, + { + "columnMatch": "PredictiveAutoscale", + "formatter": 5 + }, + { + "columnMatch": "AutoScaleProfiles", + "formatter": 5 + }, + { + "columnMatch": "resourceGroup", + "formatter": 5 + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "planId", + "label": "App Service Plan " + }, + { + "columnId": "name", + "label": "SKU Name" + }, + { + "columnId": "skuname", + "label": "SKU Name" + }, + { + "columnId": "skutier", + "label": "SKU Tier" + }, + { + "columnId": "workers", + "label": "Number of Workers " + }, + { + "columnId": "maxworkers", + "label": "Number of websites" + }, + { + "columnId": "webRG", + "label": "Resource Group " + }, + { + "columnId": "Sites", + "label": "Number of websites" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + } + ] + } + }, + "name": "query - IdleServicePlans" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "webapp" + }, + "name": "WebAppGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Log Analytics workspace\r\nA [Log Analytics workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-workspace-overview) is a unique environment for log data from Azure Monitor and other Azure services, such as Microsoft Sentinel and Microsoft Defender for Cloud. Each workspace has its own data repository and configuration but might combine data from multiple services. The following advices could be of help in cost optimization:\r\n\r\n1. Adopt [commitment tiers](https://learn.microsoft.com/azure/azure-monitor/logs/cost-logs#commitment-tiers) where applicable.\r\n2. Adopt [Azure Monitor Logs dedicated cluster](https://learn.microsoft.com/azure/azure-monitor/logs/cost-logs#dedicated-clusters) if a single workspace does not ingest enough data as per the minimum commitment tier (100 GB/day) or if it is possible to aggregate ingestion costs from more than one workspace in the same region.\r\n3. Convert the free tier based workspace to **Pay-as-you-go** model and add them to an Azure Monitor Logs dedicated cluster where possible.", + "style": "upsell" + }, + "name": "MonitoringRecommendations" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type =~ 'microsoft.operationalinsights/workspaces'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend \r\n state = trim(' ', tostring(properties.provisioningState)),\r\n sku = trim(' ', tostring(properties.sku.name)),\r\n skuUpdate = trim(' ', tostring(properties.sku.lastSkuUpdate)),\r\n retentionDays = toint(properties.retentionInDays),\r\n dailyquotaGB = trim(' ', tostring(properties.workspaceCapping.dailyQuotaGb))\r\n| extend dailyquotaGB = iif(dailyquotaGB !=-1.0, dailyquotaGB,\"--\")\r\n| project id, resourceGroup, location, retentionDays, dailyquotaGB, sku, subscriptionId\r\n| join kind = inner (\r\n resources\r\n | where type =~ 'microsoft.operationalinsights/workspaces'\r\n | where resourceGroup in ({ResourceGroup})\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags[tagName])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | summarize arg_max(tagName, tagValue) by id\r\n) on id\r\n| extend resourceGroup = tostring(split(id,'/providers/')[0])\r\n| project-away id1", + "size": 0, + "title": "Log Analytics Workspaces", + "showRefreshButton": true, + "exportMultipleValues": true, + "exportedParameters": [ + { + "fieldName": "id", + "parameterName": "selectedWorkspaceId", + "parameterType": 1 + } + ], + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "id", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "insights", + "linkIsContextBlade": true, + "showIcon": true + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": "Resource", + "linkIsContextBlade": true, + "showIcon": true + } + }, + { + "columnMatch": "retentionDays", + "formatter": 4, + "formatOptions": { + "min": 1, + "max": 730, + "palette": "blue", + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "dailyquotaGB", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "sku", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "lacluster", + "representation": "green", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "free", + "representation": "gray", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "capacityreservation", + "representation": "green", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "red", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": "Resource", + "linkIsContextBlade": true, + "showIcon": true + } + }, + { + "columnMatch": "tagName", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "is Empty", + "representation": "Blank", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Tags", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "tagValue", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "is Empty", + "representation": "Blank", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Tags", + "text": "{0}{1}" + } + ] + } + } + ], + "rowLimit": 10000, + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Workspace" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "retentionDays", + "label": "Retention (days)" + }, + { + "columnId": "dailyquotaGB", + "label": "Daily Cap (GB)" + }, + { + "columnId": "sku", + "label": "Pricing Tier" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + }, + { + "columnId": "tagName", + "label": "Tag Name" + }, + { + "columnId": "tagValue", + "label": "Tag Value" + } + ] + }, + "sortBy": [] + }, + "name": "logAnalyticsWorkspaces", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 1, + "content": { + "json": "💡_Select one or more workspaces from the list above to see daily ingestion trend_" + }, + "conditionalVisibility": { + "parameterName": "selectedWorkspaceId", + "comparison": "isEqualTo" + }, + "name": "text - 3", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "d9c04e61-453f-4f85-8d7e-1a34037d836b", + "version": "KqlParameterItem/1.0", + "name": "selectedWorkspaces", + "type": 5, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "where type =~ 'microsoft.operationalinsights/workspaces'\r\n| where id in ({selectedWorkspaceId})", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 2592000000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null + }, + { + "id": "2108523c-fb80-49b3-9ff1-ea5e5eca2091", + "version": "KqlParameterItem/1.0", + "name": "TimeRange", + "label": "Time range", + "type": 4, + "isRequired": true, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 172800000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 1209600000 + }, + { + "durationMs": 2592000000 + } + ] + }, + "timeContext": { + "durationMs": 2592000000 + }, + "value": { + "durationMs": 2592000000 + } + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "conditionalVisibility": { + "parameterName": "_", + "comparison": "isEqualTo", + "value": "_" + }, + "name": "parameters - 2" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Usage\r\n| where StartTime >= startofday({TimeRange:start}) and EndTime < startofday(now())\r\n| where IsBillable == true\r\n| project Quantity, ResourceUri, TimeGenerated\r\n| summarize BillableDataGB = sum(Quantity / 1024.) by bin(TimeGenerated, 1d)\r\n| project TimeGenerated, BillableDataGB", + "size": 0, + "aggregation": 5, + "title": "Total Daily Ingestion for selected workspaces - Trend by {TimeRange:label}", + "timeContextFromParameter": "TimeRange", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "crossComponentResources": [ + "{selectedWorkspaces}" + ], + "visualization": "barchart", + "chartSettings": { + "seriesLabelSettings": [ + { + "seriesName": "BillableDataGB", + "label": "Ingested data" + } + ], + "ySettings": { + "numberFormatSettings": { + "unit": 39, + "options": { + "style": "decimal", + "useGrouping": true, + "maximumFractionDigits": 2 + } + } + } + } + }, + "conditionalVisibility": { + "parameterName": "selectedWorkspaceId", + "comparison": "isNotEqualTo" + }, + "name": "dailyIngestionTrend", + "styleSettings": { + "showBorder": true + } + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "Monitoring" + }, + "name": "MonitoringGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| where properties.impactedField has \"Workspaces\"\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Impact=properties.impact,resourceGroup,subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Recommendation=tostring(properties.shortDescription.problem), Impact=properties.impact,resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| where Category == 'Cost' \r\n| where SubCategory has \"Workspaces\"\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Monitoring", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "stableId", + "formatter": 5 + }, + { + "columnMatch": "recommendationTypeId", + "formatter": 5 + }, + { + "columnMatch": "maxCpuP95", + "formatter": 5 + }, + { + "columnMatch": "excludeRecomm", + "formatter": 5 + }, + { + "columnMatch": "lowCpuThreshold", + "formatter": 5 + }, + { + "columnMatch": "AdditionaInfo", + "formatter": 5, + "formatOptions": { + "customColumnWidthSetting": "19ch" + } + }, + { + "columnMatch": "isActive1", + "formatter": 5 + }, + { + "columnMatch": "excludeProperty", + "formatter": 5 + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "AffectedResource", + "label": "Affected Resource" + }, + { + "columnId": "Category", + "label": "Recommendation Category" + }, + { + "columnId": "SubCategory", + "label": "Affected Resource Type" + }, + { + "columnId": "Recommendation", + "label": "Recommendation" + }, + { + "columnId": "Impact", + "label": "Impact" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "Get-AdvisorRecommendations-Monitoring" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where type has \"microsoft.operationalinsights/workspaces\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "query - tags - list all network resources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Monitoring\",\"rightTable\":\"query - tags - list all network resources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].AffectedResource\",\"mergedName\":\"Affected Resource\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].Category\",\"mergedName\":\"Recommendation Category\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].SubCategory\",\"mergedName\":\"Affected Resource Type\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].subscriptionId\",\"mergedName\":\"Subscription ID\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].id\",\"mergedName\":\"id\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].stableId\",\"mergedName\":\"stableId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].recommendationTypeId\",\"mergedName\":\"recommendationTypeId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].maxCpuP95\",\"mergedName\":\"maxCpuP95\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].excludeRecomm\",\"mergedName\":\"excludeRecomm\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].lowCpuThreshold\",\"mergedName\":\"lowCpuThreshold\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].AdditionaInfo\",\"mergedName\":\"AdditionaInfo\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].isActive1\",\"mergedName\":\"isActive1\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].excludeProperty\",\"mergedName\":\"excludeProperty\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[query - tags - list all network resources].id\",\"mergedName\":\"id1\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeProperty\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].isActive1\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].AdditionaInfo\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].lowCpuThreshold\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeRecomm\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].maxCpuP95\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].recommendationTypeId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].stableId\"},{\"originalName\":\"[query - tags - list all network resources].id\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].id\"}]}", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Monitoring", + "noDataMessageStyle": 3, + "queryType": 7 + }, + "showPin": false, + "name": "query - Merge - Monitoring Advisor recommendations" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "Monitoring" + }, + "name": "AdvisorGroupMonitoring" + } + ] + }, + "name": "group - 0 " + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "UsageOptimization" + }, + { + "parameterName": "SelectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "Top10Services" + } + ], + "name": "group - Top10Services" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadFromTemplateId": "", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "7a720abf-5b4a-4fb1-adaf-2383e70f625d", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "label": " Subscription" + }, + { + "id": "a29babbc-5092-46c5-b03b-932c90aa61c9", + "version": "KqlParameterItem/1.0", + "name": "ResourceGroup", + "label": "Resource Group", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "resources\r\n| distinct resourceGroup", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + { + "id": "4b9c84b6-14ab-4663-b8b7-8bf0c351bbb5", + "version": "KqlParameterItem/1.0", + "name": "SingleSubHidden", + "type": 1, + "isRequired": true, + "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Hidden Subscription" + }, + { + "id": "6637e003-5323-4c6d-9990-426388c833e9", + "version": "KqlParameterItem/1.0", + "name": "TagName", + "type": 2, + "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Name" + }, + { + "id": "d390e2b5-aa2f-494b-bbb8-0b18c8de9063", + "version": "KqlParameterItem/1.0", + "name": "TagValue", + "type": 2, + "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Value" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "75", + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "CostInformation" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "Welcome" + } + ], + "name": "parameters - Filters" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "ae7eb928-8873-46f8-a3ff-77f45c207fb3", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "title": "Networking cost optimization recommendations", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "ae7eb928-8873-46f8-a3ff-77f45c207fb3", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "e5d97e9d-97e6-45f2-871c-376799213b6a", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Azure Firewall", + "subTarget": "firewall", + "style": "link" + }, + { + "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Application Gateway", + "subTarget": "appGateway", + "preText": "VM", + "style": "link" + }, + { + "id": "61595d5e-9f25-4919-95a6-1462739f4657", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Load Balancer", + "subTarget": "loadBalancer", + "style": "link" + }, + { + "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Public IP Address", + "subTarget": "publicIP", + "style": "link" + }, + { + "id": "79e7a97a-1413-41e8-b4c6-ebd1d0a45e2e", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Virtual Network Gateway", + "subTarget": "vpnGw", + "style": "link" + }, + { + "id": "5655ef75-a5ec-4f4b-badf-a99191a0493f", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "NAT Gateway", + "subTarget": "natgw", + "style": "link" + }, + { + "id": "68a77162-06c2-4648-83e0-f8f41c4fbda7", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Express Route", + "subTarget": "ER", + "style": "link" + }, + { + "id": "5dd4cb39-5aa1-4de9-bc4c-338e15b8d389", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Private DNS & Private Endpoint", + "subTarget": "privatedns", + "style": "link" + }, + { + "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Advisor recommendations", + "subTarget": "advisorNetworking", + "style": "link" + } + ] + }, + "name": "links - Networking" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Application Gateways\r\nReview Application Gateways which include backend pools with no targets. Resources listed with 2 red signs are considered idle.", + "style": "upsell" + }, + "name": "Recommendations for Application Gateways" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type =~ 'Microsoft.Network/applicationGateways' and resourceGroup in ({ResourceGroup})\r\n| extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier= tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools,resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id, name, SKUName, SKUTier, SKUCapacity,resourceGroup,subscriptionId\r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Network/applicationGateways' and resourceGroup in ({ResourceGroup})\r\n | mvexpand backendPools = properties.backendAddressPools\r\n | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations)\r\n | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses)\r\n | extend backendPoolName = backendPools.properties.backendAddressPools.name\r\n | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by id\r\n) on id\r\n| project-away id1\r\n| where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount))\r\n| order by id asc\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Application gateways with empty backend pools", + "noDataMessage": "You don't have any Application Gateways with empty backendpools", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "SKUCapacity", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "backendIPCount", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "0", + "representation": "disabled", + "text": "No Backend IPs" + }, + { + "operator": ">", + "thresholdValue": "0", + "representation": "success", + "text": "Backend IP configured" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "backendAddressesCount", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "0", + "representation": "disabled", + "text": "No Backend targets" + }, + { + "operator": ">", + "thresholdValue": "0", + "representation": "success", + "text": "Backend targets available" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "id1", + "formatter": 5 + }, + { + "columnMatch": "Recommendation", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "No Backend targets", + "representation": "redBright", + "text": "No Backend targets" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "green", + "text": "Backend targets enabled" + } + ] + } + }, + { + "columnMatch": "backendPoolIPTarget", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": ">", + "thresholdValue": "0", + "representation": "success", + "text": "" + }, + { + "operator": "==", + "thresholdValue": "0", + "representation": "disabled", + "text": "" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "backendPoolVMTarget", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "is Empty", + "representation": "disabled", + "text": "" + }, + { + "operator": ">", + "thresholdValue": "0", + "representation": "success", + "text": "" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "Recommednation", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "No Backend targets", + "representation": "redBright", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "green", + "text": "{0}{1}" + } + ] + } + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "ID" + }, + { + "columnId": "name", + "label": "Name" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "SKUCapacity", + "label": "Capacity" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + }, + { + "columnId": "backendIPCount", + "label": "Has backend pool for IPs?" + }, + { + "columnId": "backendAddressesCount", + "label": "Has backend pool for VMs?" + }, + { + "columnId": "id1", + "label": "ResourceID" + } + ] + } + }, + "name": "Get-Idle-AppGW" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "appGateway" + }, + "name": "NetworkingAppGateway" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Load Balancers\r\nReview Load balancers with no backend pools, and remove them if not needed.", + "style": "upsell" + }, + "name": "Recommendations for Load Balancers" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where resourceGroup in ({ResourceGroup})\r\n| extend resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup), SKUName=tostring(sku.name),SKUTier=tostring(sku.tier),location,backendAddressPools = properties.backendAddressPools\r\n| where type =~ 'microsoft.network/loadbalancers' and array_length(backendAddressPools) == 0 and sku.name!='Basic'\r\n| order by id asc\r\n| project id,name, SKUName,SKUTier,backendAddressPools, location,resourceGroup, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Load Balancers with empty backend pools", + "noDataMessage": "You don't have any Load Balancers with empty backendpools", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "backendAddressPools", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "0", + "representation": "disabled", + "text": "Empty Backend Pool" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "name", + "label": "Name" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "backendAddressPools", + "label": "Has backend pool?" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + }, + { + "columnId": "id1", + "label": "ResourceID" + } + ] + } + }, + "name": "Get-Idle-LB" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "loadBalancer" + }, + "name": "LoadBalancerGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Public IP Addresses\r\nReview unattached Public IP addresses, as they may represent additional cost.\r\n
This query will also show Public IPs attached to Idle network cards.\r\n", + "style": "upsell" + }, + "name": "Recommendations for PIP" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'Microsoft.Network/publicIPAddresses' and isempty(properties.ipConfiguration) and isempty(properties.natGateway) and properties.publicIPAllocationMethod =~ 'Static'\r\n| extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, Location=location ,resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId\r\n| union (\r\n Resources \r\n | where type =~ 'microsoft.network/networkinterfaces' and isempty(properties.virtualMachine) and isnull(properties.privateEndpoint) and isnotempty(properties.ipConfigurations) \r\n | extend IPconfig = properties.ipConfigurations \r\n | mv-expand IPconfig \r\n | extend PublicIpId= tostring(IPconfig.properties.publicIPAddress.id)\r\n | project PublicIpId\r\n | join ( \r\n resources \r\n | where type =~ 'Microsoft.Network/publicIPAddresses'\r\n | extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, resourceGroup, Location=location \r\n ) on PublicIpId\r\n | project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId\r\n)\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | extend PublicIpId=id\r\n | distinct PublicIpId\r\n )\r\n on PublicIpId\r\n", + "size": 0, + "title": "Unattached Public IPs", + "noDataMessage": "You have no unattached Public IPs", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "PublicIpId1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "PublicIpId", + "label": "ID" + }, + { + "columnId": "IPName", + "label": "Name" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "AllocationMethod", + "label": "Allocation Method" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + }, + { + "columnId": "PublicIpId1", + "label": "Resource ID" + } + ] + } + }, + "name": "Get-Idle-PIP" + }, + { + "type": 1, + "content": { + "json": "# Routing Preference\r\n\r\nAzure routing preference enables you to choose how your traffic routes between Azure and the Internet. You can choose to route traffic either via the Microsoft network or via the ISP network (public internet). By default, traffic is routed via the Microsoft global network for all Azure services.\r\n\r\nRouting preference choices include:\r\n\r\n- **Microsoft Network**: Both ingress and egress traffic stays bulk of the travel on the Microsoft global network. This routing is also known as cold potato routing. This option has a higher ingress/egress cost.\r\n\r\n- **Public Internet (ISP network)**: The new routing choice Internet routing minimizes travel on the Microsoft global network and uses the transit ISP network to route your traffic. This routing is also known as hot potato routing.\r\n\r\nFor more information about routing preference, see [What is routing preference?](https://learn.microsoft.com/azure/virtual-network/ip-services/ip-services-overview#routing-preference).\r\n\r\n", + "style": "upsell" + }, + "name": "text - 3" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'Microsoft.Network/publicIPAddresses' and isnotempty(properties.ipConfiguration)\r\n| where tostring(properties.ipTags)== \"[]\"\r\n| extend PublicIpId=id, RoutingMethod=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, Location=location ,resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project PublicIpId,IPName, RoutingMethod,SKUName, resourceGroup, Location, AllocationMethod, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | extend PublicIpId=id\r\n | distinct PublicIpId\r\n )\r\n on PublicIpId", + "size": 0, + "title": "Public IP Addresses ", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "RoutingMethod", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "info", + "text": "Microsoft Network" + } + ] + } + }, + { + "columnMatch": "PublicIpId1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "PublicIpId", + "label": "ID" + }, + { + "columnId": "IPName", + "label": "Name" + }, + { + "columnId": "RoutingMethod", + "label": "Routing Method" + }, + { + "columnId": "SKUName", + "label": "SKU Name" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "AllocationMethod", + "label": "Allocation Method" + }, + { + "columnId": "subscriptionId", + "label": "SubscriptionId" + }, + { + "columnId": "PublicIpId1", + "label": "Resource ID" + } + ] + } + }, + "name": "Query-PIP-RoutingPreference" + }, + { + "type": 1, + "content": { + "json": "# DDoS IP Protection\r\nIf you need to protect fewer than 15 public IP resources, the IP Protection tier is the more cost-effective option. However, if you have more than 15 public IP resources to protect, then the Network Protection tier becomes more cost-effective. \r\n\r\nThis query will surface all Public IP (PIP) addressess with the DDoS Protection enabled. If there are more than 15 Public IP Addresses with DDoS protection in the same virtual network, then it is cheaper to enable DDoS Network protection.\r\n\r\nThe Network Protection tier also provides additional features, including:\r\n\r\n- DDoS Protection Rapid Response (DRR)\r\n- Cost protection guarantees\r\n- Web Application Firewall (WAF) discounts\r\n\r\nFor more information about DDoS protection, see [Which Azure DDoS Protection tier should I choose?](https://learn.microsoft.com/azure/ddos-protection/ddos-faq?source=recommendations#which-azure-ddos-protection-tier-should-i-choose-).", + "style": "upsell" + }, + "name": "text - 5" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.network/publicipaddresses\"\r\n| project ddosProtection=tostring(properties.ddosSettings), name\r\n| where ddosProtection has \"Enabled\"\r\n| count\r\n| project TotalIpsProtected = Count\r\n| extend CheckIpsProtected = iff(TotalIpsProtected >= 15,\"Enable Network Protection tier\", \"Enable PIP DDoS Protection\")", + "size": 0, + "title": "Public IP Addresses DDoS Protection", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "RoutingMethod", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "info", + "text": "Microsoft Network" + } + ] + } + }, + { + "columnMatch": "PublicIpId1", + "formatter": 5 + } + ] + } + }, + "name": "Query-PIP-DDoSProtection" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "publicIP" + }, + "name": "PIPGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Virtual Network Gateways\r\nReview idle Virtual Network Gateways that have no connections defined, as they may represent additional cost.\r\n", + "style": "upsell" + }, + "name": "Recommendations for idle virtualNetworkGateways" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.network/virtualnetworkgateways\"\r\n| extend resourceGroup =strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id, GWName=name,resourceGroup,location,subscriptionId\r\n| join kind = leftouter(\r\n resources\r\n | where type == \"microsoft.network/connections\"\r\n | extend id = tostring(properties.virtualNetworkGateway1.id)\r\n | project id\r\n | union (\r\n resources\r\n | where type == \"microsoft.network/connections\"\r\n | extend id = tostring(properties.virtualNetworkGateway2.id)\r\n | project id\r\n )\r\n) on id\r\n| where isempty(id1)\r\n| project id, GWName,resourceGroup,location,subscriptionId,status=id", + "size": 0, + "title": "Idle Virtual Network Gateways", + "noDataMessage": "No Idle Virtual Network Gateways found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "status", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Error-Connection not configured" + } + ] + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "GWName", + "label": "VPN Gateway Name" + }, + { + "columnId": "status", + "label": "Is connected?" + } + ] + } + }, + "name": "query - Idle Virtual Network gateways" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "vpnGw" + }, + "name": "VPNGW Group" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| where properties.impactedField has \"Network\"\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Impact=properties.impact,resourceGroup,subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Recommendation=tostring(properties.shortDescription.problem), Impact=properties.impact,resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| where Category == 'Cost' \r\n| where SubCategory has \"Network\"\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Networking", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "stableId", + "formatter": 5 + }, + { + "columnMatch": "recommendationTypeId", + "formatter": 5 + }, + { + "columnMatch": "maxCpuP95", + "formatter": 5 + }, + { + "columnMatch": "excludeRecomm", + "formatter": 5 + }, + { + "columnMatch": "lowCpuThreshold", + "formatter": 5 + }, + { + "columnMatch": "AdditionaInfo", + "formatter": 5, + "formatOptions": { + "customColumnWidthSetting": "19ch" + } + }, + { + "columnMatch": "isActive1", + "formatter": 5 + }, + { + "columnMatch": "excludeProperty", + "formatter": 5 + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "AffectedResource", + "label": "Affected Resource" + }, + { + "columnId": "Category", + "label": "Recommendation Category" + }, + { + "columnId": "SubCategory", + "label": "Affected Resource Type" + }, + { + "columnId": "Recommendation", + "label": "Recommendation" + }, + { + "columnId": "Impact", + "label": "Impact" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "Get-AdvisorRecommendations-Networking" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where type has \"Microsoft.Network\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "query - tags - list all network resources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Networking\",\"rightTable\":\"query - tags - list all network resources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Networking].AffectedResource\",\"mergedName\":\"AffectedResource\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].Category\",\"mergedName\":\"Category\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].SubCategory\",\"mergedName\":\"SubCategory\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].resourceGroup\",\"mergedName\":\"resourceGroup\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeProperty\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].isActive1\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].AdditionaInfo\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].lowCpuThreshold\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeRecomm\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].maxCpuP95\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].recommendationTypeId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].stableId\"},{\"originalName\":\"[query - tags - list all network resources].id\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].id\"}]}", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Networking", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 7 + }, + "showPin": false, + "name": "query - Merge - Network Advisor recommendations" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "advisorNetworking" + }, + "name": "AdvisorGroupNetworking" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for NAT Gateways\r\nReview idle NAT Gateways that have no subnet defined, as they may represent additional cost.\r\n", + "style": "upsell" + }, + "name": "Recommendations for idle virtualNetworkGateways" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.network/natgateways\" and isnull(properties.subnets)\r\n| project id, GWName=name, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), Location=location ,resourceGroup=tostring(strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)),subnet=tostring(properties.subnet), subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Idle NAT Gateways", + "noDataMessage": "No idle NAT gateways found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subnet", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Not associated." + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "GWName", + "label": "NAT Gateway Name" + }, + { + "columnId": "SKUName", + "label": "SKU Name" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subnet", + "label": "Subnet" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + } + ] + } + }, + "name": "query - Idle NAT gateways" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "natgw" + }, + "name": "NATGW Group" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Private DNS\r\nReview private DNS without [Virtual Network Links](https://learn.microsoft.com/azure/dns/private-dns-virtual-network-links).\r\n", + "style": "upsell" + }, + "name": "Recommendations for idle private dns" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.network/privatednszones\" and properties.numberOfVirtualNetworkLinks == 0\r\n| project id, PrivateDNSName=name, NumberOfRecordSets=tostring(properties.numberOfRecordSets),resourceGroup=tostring(strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)),vNets=tostring(properties.properties.numberOfVirtualNetworkLinks), subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Idle private DNS ", + "noDataMessage": "No idle private DNS found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "vNets", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "0", + "representation": "2", + "text": "Not associated to any vNET" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Not associated to any vNET" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + }, + { + "columnMatch": "subnet", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Not associated." + } + ] + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "PrivateDNSName", + "label": "Private DNS name" + }, + { + "columnId": "NumberOfRecordSets", + "label": "Number of DNS records" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "vNets", + "label": "vNETs associated" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + } + ] + } + }, + "name": "query - Idle private DNS" + }, + { + "type": 1, + "content": { + "json": "# Recommendations for Private endpoints\r\nReview [Private Endpoints](https://learn.microsoft.com/azure/private-link/private-endpoint-overview) that are not connected to any resource.", + "style": "upsell" + }, + "name": "Recommendations for idle private endpoints" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type =~ \"microsoft.network/privateendpoints\"\r\n| extend connection = iff(array_length(properties.manualPrivateLinkServiceConnections) > 0, properties.manualPrivateLinkServiceConnections[0], properties.privateLinkServiceConnections[0])\r\n| extend subnetId = properties.subnet.id\r\n| extend subnetIdSplit = split(subnetId, \"/\")\r\n| extend vnetId = strcat_array(array_slice(subnetIdSplit,0,8), \"/\")\r\n| extend serviceId = tostring(connection.properties.privateLinkServiceId)\r\n| extend serviceIdSplit = split(serviceId, \"/\")\r\n| extend serviceName = tostring(serviceIdSplit[8])\r\n| extend serviceTypeEnum = iff(isnotnull(serviceIdSplit[6]), tolower(strcat(serviceIdSplit[6], \"/\", serviceIdSplit[7])), \"microsoft.network/privatelinkservices\")\r\n| extend stateEnum = tostring(connection.properties.privateLinkServiceConnectionState.status)\r\n| extend stateDescription = tostring(connection.properties.privateLinkServiceConnectionState.description)\r\n| extend groupIds = tostring(connection.properties.groupIds[0])\r\n| where stateEnum == \"Disconnected\"\r\n| extend Details = pack_all()\r\n| project id, PrivateDNSName=name, stateEnum, stateDescription, resourceGroup=tostring(strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)),serviceName, serviceTypeEnum, groupIds, vnetId, subnetId,subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Idle private endpoints", + "noDataMessage": "No idle private endpoints found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "serviceTypeEnum", + "formatter": 16, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "vnetId", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "subnetId", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "PrivateDNSName", + "label": "Private Endpoint name" + }, + { + "columnId": "stateEnum", + "label": "State" + }, + { + "columnId": "stateDescription", + "label": "State description" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "serviceName", + "label": "Resource Name" + }, + { + "columnId": "serviceTypeEnum", + "label": "Service Type" + }, + { + "columnId": "groupIds", + "label": "Resource Sub-type" + }, + { + "columnId": "vnetId", + "label": "Subnet" + }, + { + "columnId": "subnetId", + "label": "Subscription" + } + ] + } + }, + "name": "query - Idle private endpoint" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "privatedns" + }, + "name": "Private DNS and Private Endpoints Group" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Azure Firewall\r\n\r\n## Azure Firewall Premium SKU\r\nThis table identifies Azure Firewalls with Premium SKU and evaluates whether the associated policy incorporates premium-only features or not. If a Premium SKU Firewall lacks a policy with premium features, such as TLS or intrusion detection it will be shown here. To learn more about Azure Firewall skus, check this [SKU comparison table](https://learn.microsoft.com/azure/firewall/choose-firewall-sku). ", + "style": "upsell" + }, + "name": "Recommendations for premium Firewall" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'Microsoft.Network/azureFirewalls' and properties.sku.tier==\"Premium\"\r\n| project FWID=id, firewallName = name, SkuTier = tostring(properties.sku.tier), resourceGroup, location\r\n| join kind=inner (\r\n resources\r\n | where type =~ 'microsoft.network/firewallpolicies'\r\n | mv-expand properties.firewalls\r\n | extend intrusionDetection = tostring(properties.intrusionDetection contains \"Alert\" or properties.intrusionDetection contains \"Deny\"), transportSecurity = tostring(properties.transportSecurity contains \"keyVaultSecretId\")\r\n | extend FWID=tostring(properties_firewalls.id)\r\n | where intrusionDetection == \"False\" and transportSecurity == \"False\"\r\n | project PolicyName = name, PolicySKU=tostring(properties.sku.tier), intrusionDetection, transportSecurity, FWID\r\n) on FWID", + "size": 0, + "title": "Azure Firewall Premium", + "noDataMessage": "No Azure Firewall Premium found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "firewallName", + "formatter": 5 + }, + { + "columnMatch": "FWID1", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "status", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Error-Connection not configured" + } + ] + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "FWID", + "label": "Firewall Name" + }, + { + "columnId": "firewallName", + "label": "FWName" + }, + { + "columnId": "SkuTier", + "label": "SKU" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "PolicyName", + "label": "Policy Name" + }, + { + "columnId": "PolicySKU", + "label": "Policy SKU" + }, + { + "columnId": "intrusionDetection", + "label": "Is Intrusion Detection enabled?" + }, + { + "columnId": "transportSecurity", + "label": "Is TLS enabled?" + } + ] + } + }, + "name": "query - Optimize Premium AZ Firewall" + }, + { + "type": 1, + "content": { + "json": "## Avoid multiple Firewall instances in the same region\r\nOptimize the use of Azure Firewall by having a central instance of Azure Firewall in the hub virtual network or Virtual WAN secure hub and share the same firewall across many spoke virtual networks that are connected to the same hub from the same region. Ensure there's no unexpected cross-region traffic as part of the hub-spoke topology nor multiple Azure firewall instances deployed to the same region. To learn more about Azure Firewall design principles, check [Azure Well-Architected Framework review - Azure Firewall](https://learn.microsoft.com/azure/well-architected/service-guides/azure-firewall#cost-optimization).", + "style": "upsell" + }, + "name": "text - 3" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'Microsoft.Network/azureFirewalls'\r\n| mv-expand properties.ipConfigurations\r\n| project FWID=id, firewallName = name, SkuTier = tostring(properties.sku.tier), FWRG=resourceGroup, FWLocation=location, SubnetID=tostring(properties_ipConfigurations.properties.subnet.id)\r\n| join (\r\nresources\r\n| where type =~ 'Microsoft.Network/virtualNetworks' \r\n| mv-expand properties.subnets\r\n| where properties_subnets.id has 'AzureFirewallSubnet'\r\n| extend SubnetID=tostring(properties_subnets.id), SubnetName=name, SubnetLocation=location, SubnetRG=resourceGroup) on SubnetID\r\n| project FWID, FWRG,FWLocation, SubnetID,SubnetName, SubnetRG, SubnetLocation\r\n", + "size": 0, + "title": "Azure Firewall per location", + "noDataMessage": "No Firewall deployed", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "SubnetName", + "formatter": 5 + }, + { + "columnMatch": "firewallName", + "formatter": 5 + }, + { + "columnMatch": "FWID1", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "status", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Error-Connection not configured" + } + ] + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "FWID", + "label": "Firewall Name" + }, + { + "columnId": "FWRG", + "label": "Firewall Resource Group" + }, + { + "columnId": "FWLocation", + "label": "Firewall Location" + }, + { + "columnId": "SubnetID", + "label": "Vnet / Subnet Name" + }, + { + "columnId": "SubnetName", + "label": "Subnet extended Name" + }, + { + "columnId": "SubnetRG", + "label": "Subnet Resource Group" + }, + { + "columnId": "SubnetLocation", + "label": "Subnet Location" + } + ] + } + }, + "name": "query - Firewall per Location" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "firewall" + }, + "name": "Firewall Group" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Express Route\r\n\r\n## Express Route Gateways without a completed circuit (ISP has not completed the connection)\r\nThis table identifies Express Route circutis that have not been completed. ", + "style": "upsell" + }, + "name": "Recommendations for premium Firewall" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type =~ 'Microsoft.Network/expressRouteCircuits' and properties.serviceProviderProvisioningState == \"NotProvisioned\"\r\n| extend ServiceLocation=tostring(properties.serviceProviderProperties.peeringLocation), ServiceProvider=tostring(properties.serviceProviderProperties.serviceProviderName), BandwidthInMbps=tostring(properties.serviceProviderProperties.bandwidthInMbps)\r\n| project ERId=id,ERName = name, ERRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), SKUFamily=tostring(sku.family), ERLocation = location, ServiceLocation, ServiceProvider, BandwidthInMbps\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | extend ERId=id\r\n | distinct ERId\r\n )\r\n on ERId\r\n\r\n", + "size": 0, + "title": "Idle Express Route circuits", + "noDataMessage": "No idle Express Route circuit found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "ERId1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "ERId", + "label": "Express Route ID" + }, + { + "columnId": "ERName", + "label": "E.R. Name" + }, + { + "columnId": "ERRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU Name" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "SKUFamily", + "label": "SKU Family" + }, + { + "columnId": "ERLocation", + "label": "Location" + }, + { + "columnId": "ServiceLocation", + "label": "Service Location" + }, + { + "columnId": "ServiceProvider", + "label": "Service Provider" + }, + { + "columnId": "BandwidthInMbps", + "label": "Bandwidth in Mbps" + } + ] + } + }, + "name": "Idle Express Route circuits" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "ER" + }, + "name": "Express Route Group" + } + ] + }, + "name": "networking - Subscription" + } + ] + }, + "name": "group - 0" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "UsageOptimization" + }, + { + "parameterName": "SelectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "Networking" + } + ], + "name": "NetworkingGroup", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "title": "Storage cost optimization recommendations", + "loadFromTemplateId": "", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "37ceb1c3-3930-4689-a90b-22f26e42bd81", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "label": " Subscription" + }, + { + "id": "08f5fe68-c2e3-4882-9300-b3e33f572dfe", + "version": "KqlParameterItem/1.0", + "name": "ResourceGroup", + "label": "Resource Group", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "resources\r\n| distinct resourceGroup", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + { + "id": "4fea3013-df84-4930-a453-8a6bd0375130", + "version": "KqlParameterItem/1.0", + "name": "SingleSubHidden", + "type": 1, + "isRequired": true, + "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Hidden Subscription" + }, + { + "id": "8412f39d-ee67-4979-b887-47463b8848c2", + "version": "KqlParameterItem/1.0", + "name": "TagName", + "type": 2, + "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Name" + }, + { + "id": "50c68f38-13a0-4aff-a259-4426c83b7cc0", + "version": "KqlParameterItem/1.0", + "name": "TagValue", + "type": 2, + "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Value" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "75", + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "CostInformation" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "Welcome" + } + ], + "name": "parameters - Filters" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "eae8a0d2-14e6-4cd1-a2d2-fd6b207cf517", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "eae8a0d2-14e6-4cd1-a2d2-fd6b207cf517", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Storage Accounts", + "subTarget": "Storage", + "preText": "VM", + "style": "link" + }, + { + "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Managed Disks", + "subTarget": "Disks", + "style": "link" + }, + { + "id": "86ff248b-1ce4-4194-8cd4-b1e0a9956b5d", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Backup", + "subTarget": "Backup", + "style": "link" + }, + { + "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Advisor recommendations", + "subTarget": "advisorStorage", + "style": "link" + } + ] + }, + "name": "links - Storage" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Idle backups\r\n\r\nReview protected items backup activity to determine if there are items that have not been backed up in the last 90 days. This could either mean that the underlying resource that's being backed up doesn't exist anymore or there's some issue with the resource that's preventing backups from being taken reliably.\r\n", + "style": "upsell" + }, + "name": "text - idleBackup" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "recoveryservicesresources\r\n| where type =~ 'microsoft.recoveryservices/vaults/backupfabrics/protectioncontainers/protecteditems'\r\n| extend vaultId = tostring(properties.vaultId),resourceId = tostring(properties.sourceResourceId),idleBackup= datetime_diff('day', now(), todatetime(properties.lastBackupTime)) > 90, resourceType=tostring(properties.workloadType), protectionState=tostring(properties.protectionState),lastBackupTime=tostring(properties.lastBackupTime), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup),lastBackupDate=todatetime(properties.lastBackupTime)\r\n| where idleBackup != 0\r\n| project resourceId,vaultId,idleBackup,lastBackupDate,resourceType,protectionState,lastBackupTime,location,resourceGroup,subscriptionId\r\n| join kind = inner(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | extend vaultId = id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | project vaultId\r\n )\r\n on vaultId\r\n | project-away vaultId1", + "size": 0, + "title": "Idle backups", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "idleBackup", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": ">", + "thresholdValue": "0", + "representation": "2", + "text": "No backup in the last 90 days" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "" + } + ] + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "labelSettings": [ + { + "columnId": "resourceId", + "label": "Resource ID" + }, + { + "columnId": "idleBackup", + "label": "Backup activity" + }, + { + "columnId": "lastBackupDate", + "label": "Last backup date" + }, + { + "columnId": "resourceType", + "label": "Resource type" + }, + { + "columnId": "protectionState", + "label": "Protection state" + }, + { + "columnId": "lastBackupTime", + "label": "Last backup time" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + } + ] + }, + "sortBy": [] + }, + "name": "query - idleBackups" + }, + { + "type": 1, + "content": { + "json": "## Backup storage redundancy settings\r\n\r\nBy default, when you configure backup for resources, geo-redundant storage (GRS) replication is applied to these backups. While this is the recommended storage replication option as it creates more redundancy for your critical data, you can choose to protect items using locally-redundant storage (LRS) if that meets your backup availability needs for dev-test workloads. Using LRS instead of GRS halves the cost of your backup storage. \r\n\r\n🖱️Click on each vault to see the configured storage replication\r\n", + "style": "upsell" + }, + "name": "text - backupReplication" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| where type == 'microsoft.recoveryservices/vaults'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend skuTier = tostring(sku['tier']), skuName = tostring(sku['name']), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup),redundancySettings = tostring(properties.redundancySettings['standardTierStorageRedundancy'])\r\n| order by id asc\r\n| project id,redundancySettings, resourceGroup, location,subscriptionId, skuTier, skuName\r\n| join kind = innerunique (\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | extend vaultId = id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | project id\r\n)\r\non id\r\n| project-away id1\r\n", + "size": 0, + "title": "Recovery vaults storage replication ", + "exportedParameters": [ + { + "fieldName": "RGVault", + "parameterName": "resourceGroupVault", + "parameterType": 1 + }, + { + "fieldName": "subscriptionId", + "parameterName": "subscriptionId", + "parameterType": 1 + }, + { + "fieldName": "name", + "parameterName": "vaultName", + "parameterType": 1 + } + ], + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "redundancySettings", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "GeoRedundant", + "representation": "Globe", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "ResourceFlat", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "RGVault", + "formatter": 5 + }, + { + "columnMatch": "name", + "formatter": 5 + } + ] + } + }, + "name": "query - backupStorageReplication" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "Backup" + }, + "name": "group - Backup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Storage accounts\r\nGeneral-purpose v2 storage accounts support the latest Azure Storage features and incorporate all of the functionality of general-purpose v1 and Blob storage accounts. General-purpose v2 accounts are recommended for most storage scenarios.\r\n\r\n1. General-purpose v2 accounts deliver the lowest per-gigabyte capacity prices for Azure Storage, as well as industry-competitive transaction prices.\r\n2. General-purpose v2 accounts support default account access tiers of hot or cool and blob level tiering between hot, cool, or archive.\r\n3. General-purpose v2 accounts allows you to also use lifecycle management to optimize your storage cost", + "style": "upsell" + }, + "name": "Storage accounts" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'Microsoft.Storage/StorageAccounts' and kind !='StorageV2' and kind !='FileStorage'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend StorageAccountName=name, SAKind=kind,AccessTier=tostring(properties.accessTier),SKUName=sku.name, SKUTier=sku.tier, Location=location\r\n| order by id asc\r\n| project id,StorageAccountName, SKUName, SKUTier, SAKind,AccessTier, resourceGroup, Location, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Storage accounts which are not v2", + "noDataMessage": "All storage accounts are General-purpose v2", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + }, + { + "columnMatch": "storageaccount", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "insights", + "linkIsContextBlade": true, + "showIcon": true + } + } + ], + "sortBy": [ + { + "itemKey": "$gen_link_id_0", + "sortOrder": 1 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "StorageAccountName", + "label": "Name" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "SAKind", + "label": "Kind" + }, + { + "columnId": "AccessTier", + "label": "Access Tier" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + } + ] + }, + "sortBy": [ + { + "itemKey": "$gen_link_id_0", + "sortOrder": 1 + } + ] + }, + "name": "Get-Storagev1" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "Storage" + }, + "name": "group - StorageAccount" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Unattached Managed Disks\r\n\r\nReview Managed Disks that are not attached to any Virtual machine.\r\n\r\n## Last Modified Date\r\nClick on a cell in the specified row to view the last modified date. This may help identify when the disk became idle.\r\n\r\n", + "style": "upsell" + }, + "name": "text - 3" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "47b2c951-49ef-4352-a52e-fc42fbd77f3c", + "version": "KqlParameterItem/1.0", + "name": "UpdateTime", + "label": "Last update", + "type": 2, + "isRequired": true, + "query": "{\"version\":\"1.0.0\",\"content\":\"[\\r\\n { \\\"value\\\":\\\"0d\\\", \\\"label\\\":\\\"All\\\", \\\"selected\\\":true },\\r\\n { \\\"value\\\":\\\"7d\\\", \\\"label\\\":\\\"> 7 days\\\" },\\r\\n { \\\"value\\\":\\\"14d\\\", \\\"label\\\":\\\"> 14 days\\\" },\\r\\n\\t{ \\\"value\\\":\\\"30d\\\", \\\"label\\\":\\\"> 1 month\\\" },\\r\\n\\t{ \\\"value\\\":\\\"60d\\\", \\\"label\\\":\\\"> 2 months\\\" },\\r\\n\\t{ \\\"value\\\":\\\"90d\\\", \\\"label\\\":\\\"> 3 months\\\" },\\r\\n\\t{ \\\"value\\\":\\\"180d\\\", \\\"label\\\":\\\"> 6 months\\\" },\\r\\n\\t{ \\\"value\\\":\\\"365d\\\", \\\"label\\\":\\\"> 1 year\\\" },\\r\\n\\t{ \\\"value\\\":\\\"730d\\\", \\\"label\\\":\\\"> 2 years\\\" }\\r\\n]\",\"transformers\":null}", + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 8 + }, + { + "version": "KqlParameterItem/1.0", + "name": "DiskSize", + "label": "Disk size", + "type": 2, + "isRequired": true, + "query": "{\"version\":\"1.0.0\",\"content\":\"[\\r\\n { \\\"value\\\":\\\"0\\\", \\\"label\\\":\\\"All\\\", \\\"selected\\\":true },\\r\\n { \\\"value\\\":\\\"64\\\", \\\"label\\\":\\\"> 64 GB\\\" },\\r\\n { \\\"value\\\":\\\"128\\\", \\\"label\\\":\\\"> 128 GB\\\" },\\r\\n\\t{ \\\"value\\\":\\\"256\\\", \\\"label\\\":\\\"> 256 GB\\\" },\\r\\n\\t{ \\\"value\\\":\\\"512\\\", \\\"label\\\":\\\"> 512 GB\\\" },\\r\\n\\t{ \\\"value\\\":\\\"1024\\\", \\\"label\\\":\\\"> 1024 GB\\\" }\\r\\n]\",\"transformers\":null}", + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 8, + "id": "ec556d4a-4306-4da2-8e58-9326a7a5e3ea" + } + ], + "style": "above", + "queryType": 8 + }, + "name": "idle disks parameters" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'microsoft.compute/disks' and managedBy == \"\"\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend diskState = tostring(properties.diskState)\r\n| where managedBy == \"\" and diskState != 'ActiveSAS'\r\nor diskState == 'Unattached' and diskState != 'ActiveSAS' \r\nand tags !contains 'ASR-ReplicaDisk' and tags !contains 'asrseeddisk'\r\n| extend DiskId=id, DiskIDfull=id, DiskName=name, SKUName=sku.name, SKUTier=sku.tier, DiskSizeGB=toint(properties.diskSizeGB), Location=location, TimeCreated= properties.timeCreated, QuickFix=id, SubId=subscriptionId, LastUpdate = properties.LastOwnershipUpdateTime\r\n| where ago({UpdateTime}) > LastUpdate or LastUpdate == ''\r\n| where DiskSizeGB > {DiskSize}\r\n| order by DiskId asc \r\n| project DiskId, DiskIDfull, DiskName, DiskSizeGB, SKUName, SKUTier, resourceGroup, QuickFix, Location, TimeCreated, LastUpdate, subscriptionId,SubId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | extend DiskId = id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct DiskId\r\n )\r\n on DiskId", + "size": 0, + "title": "Unattached disks", + "noDataMessage": "There aren't any unattached disks!", + "noDataMessageStyle": 3, + "exportedParameters": [ + { + "fieldName": "DiskIDfull", + "parameterName": "DiskID" + }, + { + "fieldName": "DiskName", + "parameterName": "DiskName", + "parameterType": 1 + }, + { + "fieldName": "resourceGroup", + "parameterName": "ResourceGroup", + "parameterType": 1 + }, + { + "fieldName": "SubId", + "parameterName": "subscriptionId", + "parameterType": 1 + } + ], + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "DiskIDfull", + "formatter": 5 + }, + { + "columnMatch": "QuickFix", + "formatter": 7, + "formatOptions": { + "linkTarget": "ArmAction", + "linkLabel": "Remove Idle Disk", + "linkIsContextBlade": true, + "templateRunContext": { + "componentIdSource": "column", + "componentId": "DiskId", + "templateUriSource": "static", + "templateUri": "https://raw.githubusercontent.com/sebassem/MS-learn-Workbooks/main/Deploy-Tag.json", + "templateParameters": [ + { + "name": "DiskID", + "source": "static", + "value": "DiskId", + "kind": "stringValue" + } + ], + "titleSource": "static", + "title": "Remove Idle Disk", + "descriptionSource": "static", + "description": "# Description\r\nThis ARM Template will remove the selected disk.\r\n\r\n# Actions:\r\n- Click \"Remove Idle Disk\" to remove the selected item.\r\n- Click View Template to examine the template and parameters used during deployment\r\n\r\n\r\n\r\n", + "runLabelSource": "static", + "runLabel": "Remove Idle Disk" + }, + "armActionContext": { + "path": "/{DiskID}?api-version=2021-04-01", + "headers": [], + "params": [ + { + "key": "DiskID", + "value": "" + } + ], + "httpMethod": "DELETE", + "title": "Remove Idle Disks", + "description": "# Disk Deletion Warning: {DiskName}\r\n\r\n**Attention!**\r\n\r\nThis action will permanently remove the disk with the name **{DiskName}**. Please ensure that this disk is not currently in use and that you are deleting the correct disk.\r\n\r\n**Resource Details:**\r\n\r\n- Disk Name: {DiskName}\r\n- Resource Group: {ResourceGroup}\r\n\r\n### Required RBAC Permissions\r\n\r\nTo perform this action, you need to have **Contributor** permissions on the Resource Group where the disk is located.\r\n\r\nPlease review the information carefully before proceeding with the deletion.\r\n", + "actionName": "Removing Idle Dsk", + "runLabel": "I understand, remove disk {DiskName}" + } + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "rowLimit": 1000, + "labelSettings": [ + { + "columnId": "DiskId", + "label": "Resource ID" + }, + { + "columnId": "DiskName", + "label": "Name" + }, + { + "columnId": "DiskSizeGB", + "label": "Disk Size (GB)" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "QuickFix", + "label": "Delete disk?" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "TimeCreated", + "label": "Time Created" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + } + ] + }, + "sortBy": [] + }, + "customWidth": "80", + "name": "Get-Idle-Disk" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":null,\"headers\":[],\"method\":\"GET\",\"path\":\"/subscriptions/{subscriptionId}/resources?\",\"urlParams\":[{\"key\":\"api-version\",\"value\":\"2021-04-01\"},{\"key\":\"$expand\",\"value\":\"createdTime,changedTime,provisioningState\"},{\"key\":\"$filter\",\"value\":\"name eq '{DiskName}' and resourceGroup eq'{ResourceGroup}'\"}],\"batchDisabled\":false,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.value\",\"columns\":[{\"path\":\"$..id\",\"columnid\":\"id\"},{\"path\":\"$..createdTime\",\"columnid\":\"createdTime\"},{\"path\":\"$..changedTime\",\"columnid\":\"changedTime\"},{\"path\":\"$.name\",\"columnid\":\"name\"}]}}]}", + "size": 0, + "title": "Disk last modified date", + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "formatters": [ + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "createdTime", + "formatter": 5 + }, + { + "columnMatch": "name", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Name" + }, + { + "columnId": "createdTime", + "label": "Created time" + }, + { + "columnId": "changedTime", + "label": "Last change time" + } + ] + } + }, + "customWidth": "20", + "conditionalVisibility": { + "parameterName": "DiskID", + "comparison": "isNotEqualTo", + "value": "" + }, + "name": "IdleDisk date" + } + ] + }, + "name": "Idle Disks Group" + }, + { + "type": 1, + "content": { + "json": "## Old Managed Disks snapshots\r\n\r\nReview Managed Disks snapshots that are older than 30 days\r\n", + "style": "upsell" + }, + "name": "text - 4" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == 'microsoft.compute/snapshots'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend TimeCreated = properties.timeCreated\r\n| extend resourceGroup=strcat(\"/subscriptions/\",subscriptionId,\"/resourceGroups/\",resourceGroup)\r\n| where TimeCreated < ago(30d)\r\n| order by id asc \r\n| project id, resourceGroup, location, TimeCreated ,subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Disk Snapshots with + 30 Days", + "noDataMessage": "No Snapshots with more than 30 days.", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "TimeCreated", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Name" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "TimeCreated", + "label": "Time Created" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + } + ] + } + }, + "name": "Get-Old-Snapshots" + }, + { + "type": 1, + "content": { + "json": "## Managed Disks snapshots using Premium storage\r\n\r\nTo save 60% of cost, we recommend storing your snapshots in Standard Storage, regardless of the storage type of the parent disk. It is the default option for Managed Disks snapshots. Migrate your snapshot from Premium to Standard Storage.\r\n", + "style": "upsell" + }, + "name": "text - 5" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == 'microsoft.compute/snapshots'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend StorageSku = tostring(sku.tier), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup),diskSize=tostring(properties.diskSizeGB)\r\n| where StorageSku == \"Premium\"\r\n| project id,name,StorageSku,diskSize,location,resourceGroup,subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id\r\n", + "size": 0, + "title": "Snapshots using premium storage", + "noDataMessage": "No snapshots are using Premium storage", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource Id" + }, + { + "columnId": "name", + "label": "Name" + }, + { + "columnId": "StorageSku", + "label": "SKU" + }, + { + "columnId": "diskSize", + "label": "Disk Size (GB)" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Id" + } + ] + } + }, + "name": "query - Snapshots using premium storage" + }, + { + "type": 1, + "content": { + "json": "## Orphaned Managed Disks snapshots\r\n\r\nReview snapshots with deleted source disks.\r\n", + "style": "upsell" + }, + "name": "text - 6" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == 'microsoft.compute/snapshots'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend parentDisk = properties.creationData.sourceResourceId, diskSize=tostring(properties.diskSizeGB),resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id,parentDisk,diskSize,location,resourceGroup,subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id\r\n", + "size": 0, + "title": "All Managed Disks snapshots", + "noDataMessage": "No snapshots found", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "labelSettings": [ + { + "columnId": "id", + "label": "Resource Id" + }, + { + "columnId": "parentDisk", + "label": "Parent Disk Resource Id" + }, + { + "columnId": "diskSize", + "label": "Disk size (GB)" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Id" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "IsVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "query - Retrieve all snapshots" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == 'microsoft.compute/disks'\r\n| project id\r\n", + "size": 0, + "title": "All managed disks", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "labelSettings": [ + { + "columnId": "id", + "label": "Resource Id" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "True" + }, + "name": "query - Retrieve all managed disks" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\",\"mergeType\":\"leftanti\",\"leftTable\":\"query - Retrieve all snapshots\",\"rightTable\":\"query - Retrieve all managed disks\",\"leftColumn\":\"parentDisk\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[query - Retrieve all snapshots].id\",\"mergedName\":\"Resource Id\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].parentDisk\",\"mergedName\":\"Parent Disk Resource Id\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].diskSize\",\"mergedName\":\"Disk size (GB)\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].location\",\"mergedName\":\"Location\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].subscriptionId\",\"mergedName\":\"Subscription Id\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].id1\",\"mergedName\":\"id1\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"}]}", + "size": 0, + "title": "Snapshots with deleted source disk", + "noDataMessage": "No orphaned snapshots found", + "noDataMessageStyle": 3, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Parent Disk Resource Id", + "formatter": 5 + }, + { + "columnMatch": "Subscription Id", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "Resource Id", + "label": "Resource Id" + }, + { + "columnId": "Parent Disk Resource Id", + "label": "Parent Disk resource Id" + }, + { + "columnId": "Disk size (GB)", + "label": "Disk size (GB)" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "Resource Group", + "label": "Resource Group" + }, + { + "columnId": "Subscription Id", + "label": "Subscription Id" + } + ] + } + }, + "showPin": false, + "name": "query - orphaned snapshots" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "Disks" + }, + "name": "Managed Disks Group" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=tostring(properties.category), SubCategory=tostring(properties.impactedField), Impact=tostring(properties.impact),resourceGroup,subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| where SubCategory has \"Microsoft.Storage\"\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=tostring(properties.category), SubCategory=tostring(properties.impactedField), Recommendation=tostring(properties.shortDescription.problem), Impact=tostring(properties.impact),resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2\r\n| where resourceGroup in ({ResourceGroup})", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Storage", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "stableId", + "formatter": 5 + }, + { + "columnMatch": "recommendationTypeId", + "formatter": 5 + }, + { + "columnMatch": "maxCpuP95", + "formatter": 5 + }, + { + "columnMatch": "excludeRecomm", + "formatter": 5 + }, + { + "columnMatch": "lowCpuThreshold", + "formatter": 5 + }, + { + "columnMatch": "AdditionaInfo", + "formatter": 5, + "formatOptions": { + "customColumnWidthSetting": "19ch" + } + }, + { + "columnMatch": "isActive1", + "formatter": 5 + }, + { + "columnMatch": "excludeProperty", + "formatter": 5 + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "AffectedResource", + "label": "Affected Resource" + }, + { + "columnId": "Category", + "label": "Recommendation Category" + }, + { + "columnId": "SubCategory", + "label": "Affected Resource Type" + }, + { + "columnId": "Recommendation", + "label": "Recommendation" + }, + { + "columnId": "Impact", + "label": "Impact" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "Get-AdvisorRecommendations-Storage" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where type has \"Microsoft.Storage\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "query - tags - list all storageresources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"e84cba0d-e501-4f55-a761-9126fb305030\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Storage\",\"rightTable\":\"query - tags - list all storageresources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Storage].AffectedResource\",\"mergedName\":\"Affected Resource\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].Category\",\"mergedName\":\"Recommendation Category\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].SubCategory\",\"mergedName\":\"Affected Resource Type\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].subscriptionId\",\"mergedName\":\"Subscription ID\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[query - tags - list all storageresources].id\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].excludeProperty\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].isActive1\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].AdditionaInfo\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].lowCpuThreshold\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].excludeRecomm\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].recommendationTypeId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].maxCpuP95\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].stableId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].id\"}]}", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Storage", + "noDataMessageStyle": 3, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Affected Resource Type", + "formatter": 5 + }, + { + "columnMatch": "Subscription ID", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ] + } + } + }, + "showPin": false, + "name": "query - Merge - Storage Advisor recommendations" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "advisorStorage" + }, + "name": "AdvisorGroupStorage" + } + ] + }, + "name": "group - 0" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "UsageOptimization" + }, + { + "parameterName": "SelectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "Storage" + } + ], + "name": "StorageGroup", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadFromTemplateId": "", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "94bd2bd0-5aa8-4df6-8cf7-603407f4e2d8", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "label": " Subscription" + }, + { + "id": "faa42c49-ab77-42a1-9aaf-d8508b9408af", + "version": "KqlParameterItem/1.0", + "name": "ResourceGroup", + "label": "Resource Group", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "resources\r\n| distinct resourceGroup", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + { + "id": "99a44dfa-30e2-4b2e-80a8-e05d2daab672", + "version": "KqlParameterItem/1.0", + "name": "SingleSubHidden", + "type": 1, + "isRequired": true, + "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Hidden Subscription" + }, + { + "id": "a02c21a6-cd5e-4e02-bb87-00993a06d8e8", + "version": "KqlParameterItem/1.0", + "name": "TagName", + "type": 2, + "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Name" + }, + { + "id": "add52b5b-2e8d-45d3-a304-f6d8f4b205f7", + "version": "KqlParameterItem/1.0", + "name": "TagValue", + "type": 2, + "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Value" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "75", + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "CostInformation" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "Welcome" + } + ], + "name": "parameters - Filters" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "1fc44b9a-2dd3-4b1f-bebd-b89d4ba6dfec", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Virtual machines", + "subTarget": "VM", + "preText": "VM", + "style": "link" + }, + { + "id": "8a2fa734-a30e-404e-bf99-927c1891d4b9", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Virtual machine scale sets", + "subTarget": "VMSS", + "style": "link" + }, + { + "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Advisor recommendations", + "subTarget": "advisorCompute", + "style": "link" + } + ] + }, + "name": "links - Compute" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Virtual Machines\r\n## Stopped virtual machines\r\nA virtual machine in a stopped state is still allocated the resources it was assigned, such as CPU and memory, but the VM itself is powered off. This allows for a quick startup when needed, but you are still billed for the allocated resources.", + "style": "upsell" + }, + "name": "text - StoppedVM" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'microsoft.compute/virtualmachines' and tostring(properties.extended.instanceView.powerState.displayStatus) != 'VM deallocated' and tostring(properties.extended.instanceView.powerState.displayStatus) != 'VM running'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend PowerState=tostring(properties.extended.instanceView.powerState.displayStatus), VMLocation=location, resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| order by id asc\r\n| project id, PowerState, VMLocation, resourceGroup, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id\r\n | project-away id1", + "size": 0, + "title": "Virtual Machines in a Stopped State", + "noDataMessage": "You have no VMs in a stopped state", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ] + } + }, + "name": "Get-StoppedVM" + }, + { + "type": 1, + "content": { + "json": "## Deallocated virtual machines\r\nA virtual machine in a deallocated state is not only powered off, but the underlying host infrastructure is also released, resulting in no charges for the allocated resources while the VM is in this state. However, some Azure resources such as disks and networking continue to incur charges.", + "style": "upsell" + }, + "name": "text - DeallocatedVM" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'microsoft.compute/virtualmachines' and tostring(properties.extended.instanceView.powerState.displayStatus) == 'VM deallocated'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend PowerState=tostring(properties.extended.instanceView.powerState.displayStatus), VMLocation=location, resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| order by id asc\r\n| project id, PowerState, VMLocation, resourceGroup, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id\r\n | project-away id1", + "size": 0, + "title": "Virtual Machines in a deallocated State", + "noDataMessage": "You have no VMs in a deallocated state", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "filter": true + } + }, + "name": "query - vmDeallocatedState" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "VM" + }, + "name": "group - VMs" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=tostring(properties.category), SubCategory=tostring(properties.impactedField), Impact=tostring(properties.impact),subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95,resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=tostring(properties.category), SubCategory=tostring(properties.impactedField), Recommendation=tostring(properties.shortDescription.problem), Impact=tostring(properties.impact),resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=tostring(properties.impact),resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| where Category == 'Cost' \r\n| where SubCategory has \"Microsoft.Compute\" or SubCategory has \"Container\" or SubCategory has \"Web\"\r\n| where SubCategory !has \"Microsoft.Compute/disks\"\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Compute", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "stableId", + "formatter": 5 + }, + { + "columnMatch": "recommendationTypeId", + "formatter": 5 + }, + { + "columnMatch": "maxCpuP95", + "formatter": 5 + }, + { + "columnMatch": "excludeRecomm", + "formatter": 5 + }, + { + "columnMatch": "lowCpuThreshold", + "formatter": 5 + }, + { + "columnMatch": "AdditionaInfo", + "formatter": 5, + "formatOptions": { + "customColumnWidthSetting": "19ch" + } + }, + { + "columnMatch": "isActive1", + "formatter": 5 + }, + { + "columnMatch": "excludeProperty", + "formatter": 5 + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "AffectedResource", + "label": "Affected Resource" + }, + { + "columnId": "Category", + "label": "Recommendation Category" + }, + { + "columnId": "SubCategory", + "label": "Affected Resource Type" + }, + { + "columnId": "Recommendation", + "label": "Recommendation" + }, + { + "columnId": "Impact", + "label": "Impact" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "IsVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "Get-AdvisorRecommendations-Compute" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where (type has \"Microsoft.Compute\" or type has \"Microsoft.ContainerService\" or type has \"serverfarms\") and type !has \"Disks\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "query - tags - list all compute resources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d446799d-b1af-4bca-9d72-84ba2d870039\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Compute\",\"rightTable\":\"query - tags - list all compute resources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Compute].AffectedResource\",\"mergedName\":\"Affected Resource\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].Category\",\"mergedName\":\"Recommendation Category\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].SubCategory\",\"mergedName\":\"Affected Resource Type\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].subscriptionId\",\"mergedName\":\"Subscription ID\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].id\",\"mergedName\":\"id\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].stableId\",\"mergedName\":\"stableId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].recommendationTypeId\",\"mergedName\":\"recommendationTypeId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].maxCpuP95\",\"mergedName\":\"maxCpuP95\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].excludeRecomm\",\"mergedName\":\"excludeRecomm\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].lowCpuThreshold\",\"mergedName\":\"lowCpuThreshold\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].AdditionaInfo\",\"mergedName\":\"AdditionaInfo\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].isActive1\",\"mergedName\":\"isActive1\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].excludeProperty\",\"mergedName\":\"excludeProperty\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[query - tags - list all compute resources].id\",\"mergedName\":\"id1\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d870039\"},{\"originalName\":\"[Get-AdvisorRecommendations-Compute].location\",\"mergedName\":\"location\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Compute", + "noDataMessageStyle": 3, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "Affected Resource Type", + "formatter": 5 + }, + { + "columnMatch": "Resource Group", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Subscription ID", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "stableId", + "formatter": 5 + }, + { + "columnMatch": "recommendationTypeId", + "formatter": 5 + }, + { + "columnMatch": "maxCpuP95", + "formatter": 5 + }, + { + "columnMatch": "excludeRecomm", + "formatter": 5 + }, + { + "columnMatch": "lowCpuThreshold", + "formatter": 5 + }, + { + "columnMatch": "AdditionaInfo", + "formatter": 5 + }, + { + "columnMatch": "isActive1", + "formatter": 5 + }, + { + "columnMatch": "excludeProperty", + "formatter": 5 + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ] + } + } + }, + "showPin": false, + "name": "query - Merge - Compute Advisor recommendations" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "advisorCompute" + }, + "name": "AdvisorGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Virtual Machine Scale Sets\r\n## Save with Azure Spot VMs on Virtual Machine Scale Sets\r\nUsing Azure Spot Virtual Machines on scale sets allows you to take advantage of our unused capacity at a significant cost savings. At any point in time when Azure needs the capacity back, the Azure infrastructure will evict Azure Spot Virtual Machine instances. Therefore, Azure Spot Virtual Machine instances are great for workloads that can handle interruptions like batch processing jobs, dev/test environments, large compute workloads, and more.\r\n\r\n## Spot Priority Mix\r\nAzure allows you to have the flexibility of running a mix of uninterruptible standard VMs and interruptible Spot VMs for Virtual Machine Scale Set deployments. You're able to deploy this Spot Priority Mix using Flexible orchestration to easily balance between high-capacity availability and lower infrastructure costs according to your workload requirements\r\n", + "style": "upsell" + }, + "name": "text - 8" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'microsoft.compute/virtualmachinescalesets'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend SpotVMs=tostring(properties.virtualMachineProfile.priority), SpotPriorityMix=tostring(properties.priorityMixPolicy), SKU=tostring(sku.name), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id, SKU, SpotVMs,SpotPriorityMix,subscriptionId,resourceGroup, location\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "SpotVMs", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "Spot", + "representation": "success", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Not using Spot VMs" + } + ] + } + }, + { + "columnMatch": "SpotPriorityMix", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "is Empty", + "representation": "2", + "text": "Not using Spot Priority Mix" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "ID" + }, + { + "columnId": "SKU", + "label": "SKU" + }, + { + "columnId": "SpotVMs", + "label": "Spot VMs" + }, + { + "columnId": "SpotPriorityMix", + "label": "Spot Priority Mix" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "location", + "label": "Location" + } + ] + } + }, + "name": "query - 9" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "VMSS" + }, + "name": "group - VMSS" + } + ] + }, + "name": "Compute - Subscription" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "UsageOptimization" + }, + { + "parameterName": "SelectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "Compute" + } + ], + "name": "ComputeGroup", + "styleSettings": { + "showBorder": true + } + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "UsageOptimization" + }, + "name": "group - usage optimization" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Commitment-based savings\r\nTo maximize your Azure savings, consider savings plans for flexible usage and reserved instances for persistent needs. Azure Savings plans offer reduced rates with a fixed hourly spend and reserved instances allow pre-purchasing VM base price. Both options provide discounts and adapt to your usage patterns, helping you manage costs effectively. Below is an estimate of how much you can potentially save with 1-Year commitment for each option based on your usage pattern for the last 30 days.​", + "style": "upsell" + }, + "customWidth": "50", + "name": "text - P1YTotalSavings" + }, + { + "type": 1, + "content": { + "json": "## Commitment-based savings\r\nTo maximize your Azure savings, consider savings plans for flexible usage and reserved instances for persistent needs. Savings plans offer reduced rates with a fixed hourly spend, while reserved instances allow pre-purchasing VM base price. Both options provide discounts and adapt to your usage patterns, helping you manage costs effectively. Below is an estimate of how much you can save with 3-Year commitment for each option based on your usage pattern for the last 30 days.​", + "style": "upsell" + }, + "customWidth": "50", + "name": "text - P3YTotalSavings - Copy" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and (properties.shortDescription.solution contains \"Reserved Instance\" or properties.shortDescription.solution contains \"savings plan\")\r\n| extend\r\nrecommendationTypeId = tostring(properties.recommendationTypeId),\r\nreservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId)\r\n| where term == \"P1Y\" and lookbackPeriod == \"Last 30 days\"\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nreservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend reservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nresources=tostring(properties.resourceMetadata.resourceId), \r\nsubscription = tostring(properties.extendedProperties.subId),\r\ntypeOfRecommendation = iif(properties.shortDescription.solution contains \"Reserved Instance\", \"Reservations\", \"Savings plan\")\r\n| where term == \"P1Y\" and lookbackPeriod == \"Last 30 days\"\r\n| summarize bin (sum(savings), 0.01) by typeOfRecommendation,currency\r\n| order by sum_savings desc\r\n", + "size": 0, + "title": "1 year total commitment-based savings", + "noDataMessage": "There are no commitment-based recommendations", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "value::all" + ], + "visualization": "piechart", + "chartSettings": { + "seriesLabelSettings": [ + { + "seriesName": "Reservations", + "label": "Azure Reservations" + }, + { + "seriesName": "Savings plan", + "label": "Azure Savings Plan for Compute" + } + ] + } + }, + "customWidth": "50", + "name": "query - CommitmentBasedSavingsP1Y" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and (properties.shortDescription.solution contains \"Reserved Instance\" or properties.shortDescription.solution contains \"savings plan\")\r\n| extend\r\nrecommendationTypeId = tostring(properties.recommendationTypeId),\r\nreservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId)\r\n| where term == \"P3Y\" and lookbackPeriod == \"Last 30 days\"\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nreservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend reservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nresources=tostring(properties.resourceMetadata.resourceId), \r\nsubscription = tostring(properties.extendedProperties.subId),\r\ntypeOfRecommendation = iif(properties.shortDescription.solution contains \"Reserved Instance\", \"Reservations\", \"Savings plan\")\r\n| where term == \"P3Y\" and lookbackPeriod == \"Last 30 days\"\r\n| summarize bin (sum(savings), 0.01) by typeOfRecommendation,currency\r\n| order by sum_savings desc\r\n", + "size": 0, + "title": "3 years total commitment-based savings", + "noDataMessage": "There are no commitment-based recommendations", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "value::all" + ], + "visualization": "piechart", + "chartSettings": { + "seriesLabelSettings": [ + { + "seriesName": "Reservations", + "label": "Azure Reservations" + }, + { + "seriesName": "Savings plan", + "label": "Azure Savings Plan for Compute" + } + ] + } + }, + "customWidth": "50", + "name": "query - CommitmentBasedSavingsP3Y" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "792df0b2-35da-403d-999d-ff81ea8d4f56", + "cellValue": "selectedRateOptimizationTab", + "linkTarget": "parameter", + "linkLabel": "Azure Hybrid Benefit", + "subTarget": "AHB", + "style": "link" + }, + { + "id": "56eb4166-cb7c-4384-94a9-c5f201e1316d", + "cellValue": "selectedRateOptimizationTab", + "linkTarget": "parameter", + "linkLabel": "Azure Reservations", + "subTarget": "Reservations", + "style": "link" + }, + { + "id": "799d4fc7-5790-467c-84cc-ce4b4cc34a3f", + "cellValue": "selectedRateOptimizationTab", + "linkTarget": "parameter", + "linkLabel": "Azure savings plan for compute", + "subTarget": "SavingsPlan", + "style": "link" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RateOptimization" + }, + "name": "links - rate optimization tabs" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadFromTemplateId": "", + "items": [ + { + "type": 1, + "content": { + "json": "**Reserved instances** can provide a significant discount over on-demand prices. With reserved instances, you can pre-purchase the base costs for your virtual machines. \r\n
Discounts will automatically apply to new or existing VMs that have the same size and region as your reserved instance.
We analyzed your usage over selected Term, look-back period and recommend money-saving reserved instances​.\r\n
This query will only provide you recommendations for single scope reserved instances. *To learn more about Reserved Instances, go to this [link.](https://learn.microsoft.com/azure/cost-management-billing/manage/understand-vm-reservation-charges)*", + "style": "info" + }, + "name": "text - advisorReservationdDisclaimer" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "a1960768-9da4-455d-b6f6-6d43098cff76", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "label": " Subscription", + "value": [ + "value::all" + ] + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "75", + "name": "parameters - Filters" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "2b8ca845-75ba-4f4b-acad-54ee50d66d54", + "version": "KqlParameterItem/1.0", + "name": "LookBackPeriod", + "label": "Look back period", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n {\"value\": \"Last 7 days\"},\r\n {\"value\": \"Last 30 days\"},\r\n {\"value\": \"Last 60 days\"}\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "Last 60 days" + }, + { + "id": "953c9e4c-af03-4fb7-bf30-3f1bfdf09199", + "version": "KqlParameterItem/1.0", + "name": "term", + "label": "Term", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [] + }, + "jsonData": "[[\r\n {\r\n \"value\": \"P1Y\",\r\n \"Selected\": \"true\"\r\n },\r\n {\r\n \"value\": \"P3Y\"\r\n }\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "P3Y" + }, + { + "id": "c46193fe-f1b2-49d1-a9bc-c9f5149f0194", + "version": "KqlParameterItem/1.0", + "name": "resourceType", + "label": "Resource type", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"Reserved Instance\"\r\n| extend reservedResourceType=tostring(properties.extendedProperties.reservedResourceType)\r\n| distinct reservedResourceType", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": [ + "value::all" + ] + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "name": "parameters - reservationsParams" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"Reserved Instance\" \r\n| extend\r\nrecommendationTypeId = tostring(properties.recommendationTypeId),\r\nreservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\" and reservedResourceType in ({resourceType})\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nreservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend reservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nresources=tostring(properties.resourceMetadata.resourceId), \r\nsubscription = tostring(properties.extendedProperties.subId)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\" and reservedResourceType in ({resourceType})\r\n| summarize Subscriptions=dcount(resources), \r\n bin (sum(savings), 0.01) by Recommendation ,reservedResourceType ,lookbackPeriod,scope,term ,currency\r\n| order by sum_savings desc\r\n", + "size": 0, + "title": "Reservations Summary", + "noDataMessage": "No reservations recommendations found!", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "categoricalbar", + "gridSettings": { + "filter": true, + "labelSettings": [ + { + "columnId": "reservedResourceType", + "label": "Resource type" + }, + { + "columnId": "lookbackPeriod", + "label": "Look back period" + }, + { + "columnId": "scope", + "label": "Scope" + }, + { + "columnId": "term", + "label": "Term" + }, + { + "columnId": "currency", + "label": "Currency" + }, + { + "columnId": "sum_savings", + "label": "Total annual savings" + } + ] + }, + "chartSettings": { + "xAxis": "reservedResourceType", + "yAxis": [ + "sum_savings" + ], + "group": "reservedResourceType", + "createOtherGroup": 0, + "showLegend": true, + "ySettings": { + "numberFormatSettings": { + "unit": 0, + "options": { + "style": "decimal", + "useGrouping": true + } + } + } + } + }, + "name": "query - Reservations Summary" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"Reserved Instance\" \r\n| extend\r\nrecommendationTypeId = tostring(properties.recommendationTypeId),\r\nreservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\" and reservedResourceType in ({resourceType})\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nreservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend reservedResourceType=tostring(properties.extendedProperties.reservedResourceType),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nsubscription = tostring(properties.extendedProperties.subId)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\" and reservedResourceType in ({resourceType})\r\n| project Recommendation,reservedResourceType,displaySKU,displayQty,savings,currency,lookbackPeriod,term,region,subscription\r\n| order by savings desc\r\n", + "size": 0, + "title": "Reservations details", + "noDataMessage": "No reservations recommendations found!", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Recommendation", + "formatter": 5 + }, + { + "columnMatch": "reservedResourceType", + "formatter": 5 + }, + { + "columnMatch": "subscription", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscription", + "reservedResourceType" + ], + "expandTopLevel": false + }, + "labelSettings": [ + { + "columnId": "displaySKU", + "label": "SKU" + }, + { + "columnId": "displayQty", + "label": "Quantity" + }, + { + "columnId": "savings", + "label": "Total annual savings" + }, + { + "columnId": "currency", + "label": "Currency" + }, + { + "columnId": "lookbackPeriod", + "label": "Look back period" + }, + { + "columnId": "term", + "label": "Term" + }, + { + "columnId": "region", + "label": "Region" + }, + { + "columnId": "subscription", + "label": "Subscription" + } + ] + }, + "chartSettings": { + "xAxis": "reservedResourceType", + "yAxis": [ + "sum_savings" + ], + "group": "reservedResourceType", + "createOtherGroup": 0, + "showLegend": true, + "ySettings": { + "numberFormatSettings": { + "unit": 0, + "options": { + "style": "decimal", + "useGrouping": true + } + } + } + } + }, + "name": "query - Reservations details" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RateOptimization" + }, + { + "parameterName": "selectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "Reservations" + } + ], + "name": "group - Reservations" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadFromTemplateId": "", + "items": [ + { + "type": 1, + "content": { + "json": "We analyzed your compute usage over the last 30 days and recommend adding a savings plan to increase your savings.
The savings plan unlocks lower prices on select compute services when you commit to spend a fixed hourly amount for 1 or 3 years.
As you use select compute services globally, your usage is covered by the plan at reduced prices. During the times when your usage is above your hourly commitment, you’ll simply be billed at your regular pay-as-you-go prices. With savings automatically applying across compute usage globally, you’ll continue saving even as your usage needs change over time.
Savings plan are more suited for dynamic workloads while accommodating for planned or unplanned changes while reservations are more suited for stable, predictable workloads with no planned changes.
Saving estimates are calculated for individual subscriptions and the usage pattern observed over last 30 days. **Shared scope savings plans are available in purchase experience and can further increase savings.**
\r\nTo learn more about Savings Plan, check out this [link.](https://learn.microsoft.com/azure/cost-management-billing/savings-plan/purchase-recommendations)​", + "style": "info" + }, + "name": "text - advisorSavingsPlanDisclaimer" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "a1960768-9da4-455d-b6f6-6d43098cff76", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "label": " Subscription", + "value": [ + "value::all" + ] + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "75", + "name": "parameters - Filters" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "2b8ca845-75ba-4f4b-acad-54ee50d66d54", + "version": "KqlParameterItem/1.0", + "name": "LookBackPeriod", + "label": "Look back period", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[[\r\n {\"value\": \"Last 7 days\"},\r\n {\"value\": \"Last 30 days\"},\r\n {\"value\": \"Last 60 days\"}\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "Last 30 days" + }, + { + "id": "953c9e4c-af03-4fb7-bf30-3f1bfdf09199", + "version": "KqlParameterItem/1.0", + "name": "term", + "label": "Term", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [] + }, + "jsonData": "[[\r\n {\r\n \"value\": \"P1Y\",\r\n \"Selected\": \"true\"\r\n },\r\n {\r\n \"value\": \"P3Y\"\r\n }\r\n]", + "timeContext": { + "durationMs": 86400000 + }, + "value": "P1Y" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "name": "parameters - savingsPlanParams" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"savings plan\"\r\n| extend recommendationTypeId = tostring(properties.recommendationTypeId),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId),\r\ncommitment = tostring(properties.extendedProperties.commitment)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nstableId = name,\r\ncommitment = tostring(properties.extendedProperties.commitment),\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend lookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\nregion = tostring(properties.extendedProperties.region),\r\nresources=tostring(properties.resourceMetadata.resourceId), \r\nsubscription = tostring(properties.extendedProperties.subId),\r\ncommitment = tostring(properties.extendedProperties.commitment)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| summarize Subscriptions=dcount(resources), \r\n bin (sum(savings), 0.01) by subscription ,commitment ,lookbackPeriod,scope,term ,currency\r\n| order by sum_savings desc\r\n| join (\r\nresourcecontainers\r\n| where type == 'microsoft.resources/subscriptions'\r\n| extend subscription = subscriptionId\r\n| project name,subscription\r\n) on subscription\r\n| project-away subscription1,subscription\r\n", + "size": 0, + "title": "Savings plan Summary", + "noDataMessage": "No savings plan recommendations found!", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "categoricalbar", + "gridSettings": { + "filter": true + }, + "chartSettings": { + "xAxis": "name", + "yAxis": [ + "sum_savings" + ], + "group": "reservedResourceType", + "createOtherGroup": 0, + "showLegend": true, + "ySettings": { + "numberFormatSettings": { + "unit": 0, + "options": { + "style": "decimal", + "useGrouping": true + } + } + } + } + }, + "name": "query - Saving plan Summary" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"savings plan\"\r\n| extend\r\nrecommendationTypeId = tostring(properties.recommendationTypeId),\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nterm=tostring(properties.extendedProperties.term),\r\nstableId = name,\r\nsubscriptionId = tostring(properties.extendedProperties.subId),\r\ncommitment = tostring(properties.extendedProperties.commitment)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| extend subscriptionId,stableId\r\n| join kind = leftouter\r\n(advisorresources \r\n| where type =~ 'microsoft.advisor/configurations'\r\n| where isempty(resourceGroup) == true\r\n| extend\r\nmaxCpuP95 = properties.extendedProperties.MaxCpuP95,\r\nlowCpuThreshold = properties.lowCpuThreshold,\r\nexcludeRecomm = properties.exclude,\r\nlookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\nstableId = name,\r\ncommitment = tostring(properties.extendedProperties.commitment),\r\nsubscriptionId = tostring(properties.extendedProperties.subId))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| extend lookbackPeriod=tostring(strcat(\"Last \",properties.extendedProperties.lookbackPeriod,\" days\")),\r\nscope=tostring(properties.extendedProperties.scope),\r\nterm=tostring(properties.extendedProperties.term),\r\nsavings=todouble(properties.extendedProperties.annualSavingsAmount),\r\nsavingsAmount = todouble(properties.extendedProperties.savingsAmount),\r\nRecommendation=tostring(properties.shortDescription.solution), \r\ncurrency = tostring(properties.extendedProperties.savingsCurrency),\r\ndisplayQty = tostring(properties.extendedProperties.displayQty),\r\ndisplaySKU = tostring(properties.extendedProperties.displaySKU),\r\ncommitment = tostring(properties.extendedProperties.commitment),\r\nregion = tostring(properties.extendedProperties.region),\r\nsubscription = tostring(properties.extendedProperties.subId)\r\n| where term == \"{term}\" and lookbackPeriod == \"{LookBackPeriod}\"\r\n| project Recommendation,savings,commitment,currency,lookbackPeriod,term,subscription\r\n| order by savings desc\r\n| join (\r\nresourcecontainers\r\n| where type == 'microsoft.resources/subscriptions'\r\n| extend subscription = subscriptionId\r\n| project id,name,subscription\r\n) on subscription\r\n| project-away subscription1,subscription\r\n", + "size": 0, + "title": "Savings plan details", + "noDataMessage": "No savings plan recommendations found!", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Recommendation", + "formatter": 5 + }, + { + "columnMatch": "id", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "name", + "formatter": 5 + }, + { + "columnMatch": "reservedResourceType", + "formatter": 5 + }, + { + "columnMatch": "subscription", + "formatter": 5 + } + ], + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "id" + ] + }, + "labelSettings": [ + { + "columnId": "savings", + "label": "Total annual savings" + }, + { + "columnId": "commitment", + "label": "Commitment" + }, + { + "columnId": "currency", + "label": "Currency" + }, + { + "columnId": "lookbackPeriod", + "label": "Look back period" + }, + { + "columnId": "term", + "label": "Term" + } + ] + }, + "chartSettings": { + "xAxis": "reservedResourceType", + "yAxis": [ + "sum_savings" + ], + "group": "reservedResourceType", + "createOtherGroup": 0, + "showLegend": true, + "ySettings": { + "numberFormatSettings": { + "unit": 0, + "options": { + "style": "decimal", + "useGrouping": true + } + } + } + } + }, + "name": "query - Savings plan details" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RateOptimization" + }, + { + "parameterName": "selectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "SavingsPlan" + } + ], + "name": "group - SavingsPlan" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "2b43eb64-bca3-444a-8003-003554236fe7", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "label": " Subscription", + "value": [ + "value::all" + ] + }, + { + "id": "03fbf28a-892d-4b68-929c-3ba5056f4b94", + "version": "KqlParameterItem/1.0", + "name": "ResourceGroup", + "label": "Resource Group", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "resources\r\n| distinct resourceGroup", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "showDefault": false + }, + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + { + "id": "566c43ae-f300-43be-aa0d-61d92ba8da87", + "version": "KqlParameterItem/1.0", + "name": "SingleSubHidden", + "type": 1, + "isRequired": true, + "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Hidden Subscription" + }, + { + "id": "a9df02ed-7100-4130-952f-a3d9d5d364af", + "version": "KqlParameterItem/1.0", + "name": "TagName", + "type": 2, + "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Name" + }, + { + "id": "66406915-1f07-448f-8170-2f3b0dc6dc00", + "version": "KqlParameterItem/1.0", + "name": "TagValue", + "type": 2, + "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Value" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "75", + "conditionalVisibility": { + "parameterName": "selectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - Filters" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "f74bc7f5-2b16-4440-8053-106e040b73b6", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "selectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "loadType": "always", + "loadFromTemplateId": "", + "items": [ + { + "type": 1, + "content": { + "json": "# Azure Hybrid Benefit\r\nFor customers with Software Assurance, Azure Hybrid Benefit for Windows Server allows you to use your on-premises Windows Server licenses to run Windows virtual machines on Azure at a reduced cost. This article discusses how to deploy new VMs with Azure Hybrid Benefit for Windows Server enabled, and how you can update any existing running VMs. For more information about Azure Hybrid Benefit for Windows Server licensing and cost savings, see the [Azure Hybrid Benefit for Windows Server licensing page](https://azure.microsoft.com/pricing/hybrid-use-benefit/)\r\n\r\n", + "style": "upsell" + }, + "name": "Azure Hybrid Benefit" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "0c58188b-5c09-45aa-b738-f7122d0e0a19", + "version": "KqlParameterItem/1.0", + "name": "Location", + "label": "SKU Location", + "type": 1, + "description": "Select the region where the VMs are located. Different Regions might have different SKUs", + "isRequired": true, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project location\r\n| take 1\r\n\r\n", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + { + "id": "3ee18b0f-2f7b-4a6a-9d1c-526505c7eea2", + "version": "KqlParameterItem/1.0", + "name": "SingleSubHidden", + "type": 1, + "isRequired": true, + "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Hidden Subscription" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "name": "SubscriptionPicker" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":null,\"headers\":[],\"method\":\"GET\",\"path\":\"/subscriptions/{SingleSubHidden}/providers/Microsoft.Compute/skus?$filter=location eq '{Location}'\",\"urlParams\":[{\"key\":\"api-version\",\"value\":\"2021-07-01\"}],\"batchDisabled\":false,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.*[?(@.resourceType=='virtualMachines')]\",\"columns\":[{\"path\":\"name\",\"columnid\":\"Name\"},{\"path\":\"capabilities[?(@.name=='vCPUs')].value\",\"columnid\":\"vCPUs\"},{\"path\":\"capabilities[?(@.name=='MemoryGB')].value\",\"columnid\":\"MemoryGB\"},{\"path\":\"capabilities[?(@.name=='MaxNetworkInterfaces')].value\",\"columnid\":\"MaxNetworkInterfaces\"},{\"path\":\"capabilities[?(@.name=='HyperVGenerations')].value\",\"columnid\":\"HyperVGenerations\"},{\"path\":\"capabilities[?(@.name=='vCPUsPerCore')].value\",\"columnid\":\"vCPUsPerCore\"}]}}]}", + "size": 0, + "title": "Get VM vCPU", + "exportParameterName": "ResourceSKU", + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "rowLimit": 5000 + } + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "API-Get_VM_SKU" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "3f12a4b6-b18d-4191-8c1c-6045a7edcb6b", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "VM/VMSS", + "subTarget": "VM", + "style": "link" + }, + { + "id": "78ac1878-4b69-4f32-af1f-a8f095afbed5", + "cellValue": "SelectedTab", + "linkTarget": "parameter", + "linkLabel": "SQL", + "subTarget": "SQL", + "style": "link" + } + ] + }, + "name": "links - 1" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Windows Virtual Machines", + "subTarget": "VM", + "preText": "VM", + "style": "link" + }, + { + "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Linux Virtual Machines", + "subTarget": "LinuxVM", + "style": "link" + }, + { + "id": "79e7a97a-1413-41e8-b4c6-ebd1d0a45e2e", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "VM Scale Set", + "subTarget": "VMSS", + "style": "link" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "VM" + }, + "name": "links - 4" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "title": "Virtual Machines", + "loadType": "always", + "items": [ + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "loadType": "always", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets'\r\n| where tostring(properties.storageProfile.imageReference.publisher ) == \"MicrosoftWindowsServer\" or tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType) == 'Windows' or tostring(properties.storageProfile.imageReference.publisher ) == \"microsoftsqlserver\"\r\n| where tostring(properties.['licenseType']) !has 'Windows' and tostring(properties.virtualMachineProfile.['licenseType']) != 'Windows_Server'\r\n| extend WindowsId=id, VMIDFull=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.storageProfile.imageReference.offer), OsVersion = tostring(properties.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.['licenseType']), VMSSize=tostring(sku.name), QuickFix=id\r\n ) on subscriptionId \r\n| order by type asc \r\n| project WindowsId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OsVersion,LicenseType, subscriptionId, QuickFix, VMIDFull\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId", + "size": 0, + "title": "AHB Disabled", + "noDataMessage": "All of your VMs have AHB enabled.", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "AHB Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines'\r\n| where tostring(properties.storageProfile.imageReference.publisher ) == \"MicrosoftWindowsServer\" or tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType) == 'Windows' or tostring(properties.storageProfile.imageReference.publisher ) == \"microsoftsqlserver\"\r\n| where tostring(properties.['licenseType']) has \"Windows\"\r\n| extend WindowsId=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.storageProfile.imageReference.offer), OsVersion = tostring(properties.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.['licenseType']), VMSSize=tostring(sku.name)\r\n) on subscriptionId \r\n| order by type asc \r\n| project WindowsId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OsVersion,LicenseType, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId", + "size": 0, + "title": "AHB Enabled", + "noDataMessage": "None of your VMs have AHB enabled.", + "noDataMessageStyle": 4, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "VMRG", + "formatter": 0, + "tooltipFormat": { + "tooltip": "test" + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "WindowsAHBEnabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resourcechanges\r\n| where properties.changeType == \"Update\" and properties.targetResourceType == \"microsoft.compute/virtualmachines\"\r\n| mv-expand changes = properties.changes\r\n| mv-expand LicenseChanges=changes.['properties.licenseType']\r\n| extend WindowsId=id\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId\r\n| where isnotnull(LicenseChanges)\r\n| where tostring(LicenseChanges.newValue) has \"Windows\"\r\n| project VMID=properties.targetResourceId, NewLicense=tostring(LicenseChanges.newValue), DateofChange=todatetime(properties.changeAttributes.timestamp)\r\n", + "size": 0, + "title": "VM Latest Change Last 7 days", + "noDataMessage": "AHB was not enabled in the last 7 days.", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "VM Latest Change Last 7 days" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":null,\"headers\":[],\"method\":\"GET\",\"path\":\"/subscriptions/{SingleSubHidden}/providers/Microsoft.Compute/skus?$filter=location eq '{Location}'\",\"urlParams\":[{\"key\":\"api-version\",\"value\":\"2021-07-01\"}],\"batchDisabled\":false,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.*[?(@.resourceType=='virtualMachines')]\",\"columns\":[{\"path\":\"name\",\"columnid\":\"Name\"},{\"path\":\"capabilities[?(@.name=='vCPUs')].value\",\"columnid\":\"vCPUs\"},{\"path\":\"capabilities[?(@.name=='MemoryGB')].value\",\"columnid\":\"MemoryGB\"},{\"path\":\"capabilities[?(@.name=='MaxNetworkInterfaces')].value\",\"columnid\":\"MaxNetworkInterfaces\"},{\"path\":\"capabilities[?(@.name=='HyperVGenerations')].value\",\"columnid\":\"HyperVGenerations\"},{\"path\":\"capabilities[?(@.name=='vCPUsPerCore')].value\",\"columnid\":\"vCPUsPerCore\"}]}}]}", + "size": 0, + "title": "Get VM vCPU", + "exportParameterName": "ResourceSKU", + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "rowLimit": 5000 + } + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "query - Get VM vCPU" + }, + { + "type": 1, + "content": { + "json": "## Windows Azure Hybrid Benefit (AHB) Overview" + }, + "name": "AHB Overview" + }, + { + "type": 1, + "content": { + "json": "Each two-processor license or each set of 16-core licenses, either Datacenter or Standard editions, are entitled to two instances of up to 8 cores, or one instance of up to 16 cores.\r\n\r\nThe virtual machines (VMs) with less than 8 cores are categorized as **Low Priority**, while those with 8 or more cores are classified as **High Priority**. In situations where there are insufficient Azure Hybrid benefit licenses to cover all the VMs in the environment, it is recommended to prioritize the High Priority VMs.", + "style": "info" + }, + "name": "NUmber of Processors", + "styleSettings": { + "margin": "10px", + "showBorder": true + } + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines'\r\n| where tostring(properties.storageProfile.osDisk.osType) == 'Windows'\r\n| extend WindowsId=id\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId\r\n| extend LicenseType = tostring(properties.['licenseType'])\r\n| extend CheckAHBWindows = case(\r\n type == 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets', iif((properties.['licenseType'])\r\n !has 'Windows' and (properties.virtualMachineProfile.['licenseType']) !has 'Windows' , \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not Windows\"\r\n )\r\n) on subscriptionId \r\n| summarize count() by SubscriptionName, CheckAHBWindows\r\n", + "size": 0, + "title": "Summary of Windows VMs with or without AHB per Subscription", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "sortBy": [ + { + "itemKey": "SubscriptionName", + "sortOrder": 1 + } + ], + "labelSettings": [ + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "CheckAHBWindows", + "label": "Is AHB enabled?" + }, + { + "columnId": "count_", + "label": "Number of resources" + } + ] + }, + "sortBy": [ + { + "itemKey": "SubscriptionName", + "sortOrder": 1 + } + ], + "tileSettings": { + "titleContent": { + "columnMatch": "CheckAHBWindows", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" + } + }, + "customWidth": "50", + "name": "Summary of Windows VMs with or without AHB per Subscription" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines'\r\n| where tostring(properties.storageProfile.osDisk.osType) == 'Windows'\r\n| extend WindowsId=id\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId\r\n| extend LicenseType = tostring(properties.['licenseType'])\r\n| extend CheckAHBWindows = case(\r\n type == 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets', iif((properties.['licenseType'])\r\n !has 'Windows' and (properties.virtualMachineProfile.['licenseType']) !has 'Windows' , \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not Windows\"\r\n )\r\n) on subscriptionId \r\n| summarize count() by CheckAHBWindows", + "size": 0, + "title": "Summary of Windows VMs with or without AHB", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart" + }, + "customWidth": "50", + "name": "Summary of Windows VMs with or without AHB" + }, + { + "type": 1, + "content": { + "json": "### Consumed Licenses\r\nTotal number of Windows licenses cores consumed by all Windows virtual machines.\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Consumed Licenses" + }, + { + "type": 1, + "content": { + "json": "### Number of required Cores to enable Windows Azure Hybrid Benefit\r\nNumber of cores required to enable AHB across the entire environment.", + "style": "info" + }, + "customWidth": "50", + "name": "Number of required Cores" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"50d79765-aad4-437e-a90b-8cc7865e7081\",\"mergeType\":\"inner\",\"leftTable\":\"WindowsAHBEnabled\",\"rightTable\":\"query - Get VM vCPU\",\"leftColumn\":\"VMSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[WindowsAHBEnabled].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[WindowsAHBEnabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\">=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores per VM\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"8\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"[\\\"vCPUs\\\"]\"}}]},{\"originalName\":\"[WindowsAHBEnabled].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[WindowsAHBEnabled].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"},{\"originalName\":\"[WindowsAHBEnabled].WindowsId\"},{\"originalName\":\"[WindowsAHBEnabled].VMRG\"},{\"originalName\":\"[WindowsAHBEnabled].VMLocation\"},{\"originalName\":\"[WindowsAHBEnabled].OSType\"},{\"originalName\":\"[WindowsAHBEnabled].OsVersion\"},{\"originalName\":\"[WindowsAHBEnabled].LicenseType\"},{\"originalName\":\"[query - Get VM vCPU].Name\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\"},{\"originalName\":\"[WindowsAHBEnabled].VMSSize\"}]}", + "size": 0, + "title": "Consumed Cores per AHB Priority", + "noDataMessage": "None of your VMs have AHB enabled", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Sev4", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores per VM" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Consumed Cores per AHB Priority" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d8deb22b-a596-43ee-acc4-180849d26130\",\"mergeType\":\"inner\",\"leftTable\":\"WindowsAHBEnabled\",\"rightTable\":\"query - Get VM vCPU\",\"leftColumn\":\"VMSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"ConsumedCores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"8\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"[\\\"vCPUs\\\"]\"}}]},{\"originalName\":\"[WindowsAHBEnabled].WindowsId\",\"mergedName\":\"WindowsId\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[WindowsAHBEnabled].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[WindowsAHBEnabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[WindowsAHBEnabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[WindowsAHBEnabled].VMSSize\",\"mergedName\":\"VMSSize\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[WindowsAHBEnabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[WindowsAHBEnabled].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[WindowsAHBEnabled].OsVersion\",\"mergedName\":\"OsVersion\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[WindowsAHBEnabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[query - Get VM vCPU].Name\",\"mergedName\":\"Name\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[query - Get VM vCPU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[WindowsAHBEnabled].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[WindowsAHBEnabled].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "Consumed Cores per VM", + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "ConsumedCores", + "formatter": 0, + "formatOptions": { + "aggregation": "Sum" + } + } + ] + }, + "tileSettings": { + "titleContent": {}, + "leftContent": { + "columnMatch": "ConsumedCores", + "formatter": 12, + "formatOptions": { + "palette": "blue" + } + }, + "showBorder": false + }, + "graphSettings": { + "type": 0 + }, + "chartSettings": { + "yAxis": [ + "ConsumedCores" + ], + "group": "VMName", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Consumed Cores per VM" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"50d79765-aad4-437e-a90b-8cc7865e7081\",\"mergeType\":\"inner\",\"leftTable\":\"AHB Disabled\",\"rightTable\":\"query - Get VM vCPU\",\"leftColumn\":\"VMSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\">=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores per VM\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"8\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"[\\\"vCPUs\\\"]\"}}]},{\"originalName\":\"[query - 0].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - 0].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"},{\"originalName\":\"[AHB Disabled].WindowsId\",\"mergedName\":\"WindowsId\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].VMSSize\",\"mergedName\":\"VMSSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].OsVersion\",\"mergedName\":\"OsVersion\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].Name\",\"mergedName\":\"Name\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].QuickFix\",\"mergedName\":\"QuickFix\",\"fromId\":\"unknown\"},{\"originalName\":\"[AHB Disabled].VMIDFull\",\"mergedName\":\"VMIDFull\",\"fromId\":\"unknown\"},{\"originalName\":\"[WindowsAHBEnabled].WindowsId\"},{\"originalName\":\"[WindowsAHBEnabled].VMRG\"},{\"originalName\":\"[WindowsAHBEnabled].VMLocation\"},{\"originalName\":\"[WindowsAHBEnabled].OSType\"},{\"originalName\":\"[WindowsAHBEnabled].OsVersion\"},{\"originalName\":\"[WindowsAHBEnabled].LicenseType\"},{\"originalName\":\"[query - Get VM vCPU].Name\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\"},{\"originalName\":\"[WindowsAHBEnabled].VMSSize\"}]}", + "size": 0, + "title": "Cores not enabled per AHB Priority", + "noDataMessage": "All of your VMs have AHB enabled", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Sev4", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores per VM" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Cores NOT enabled per AHB Priority" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", + "version": "KqlParameterItem/1.0", + "name": "AHBEnabled", + "label": "See VMs with AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 + } + }, + { + "id": "f1ac5e53-253c-4afb-8bc5-b1ba2efea3eb", + "version": "KqlParameterItem/1.0", + "name": "AHBDisabled", + "label": "See VMs without AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "value": "Yes" + }, + { + "id": "20a00706-a89b-42aa-8dea-9c44c93e8014", + "version": "KqlParameterItem/1.0", + "name": "LastAHB", + "label": "See VMs AHB enabled in the last 7 days", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "VM AHB Enabled" + }, + { + "type": 1, + "content": { + "json": "List of Windows VMs without Hybrid Benefit groupped by Subscription.", + "style": "info" + }, + "conditionalVisibility": { + "parameterName": "AHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "List of Windows VMs without AHB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"50d79765-aad4-437e-a90b-8cc7865e7081\",\"mergeType\":\"inner\",\"leftTable\":\"AHB Disabled\",\"rightTable\":\"query - Get VM vCPU\",\"leftColumn\":\"VMSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[query - 0].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - 0].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[AHB Disabled].VMName\",\"mergedName\":\"VM Name\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\">=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"static\",\"resultVal\":\"High Priority\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"static\",\"resultVal\":\"Low Priority\"}}]},{\"originalName\":\"[AHB Disabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].QuickFix\",\"mergedName\":\"QuickFix\",\"fromId\":\"unknown\"},{\"originalName\":\"[AHB Disabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].OsVersion\",\"mergedName\":\"OsVersion\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].Name\",\"mergedName\":\"Name\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[AHB Disabled].VMIDFull\",\"mergedName\":\"VMIDFull\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - 0].VMName\"},{\"originalName\":\"[query - 0].VMSSize\"},{\"originalName\":\"[query - Get VM vCPU].Name\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\"},{\"originalName\":\"[AHB Disabled].WindowsId\"},{\"originalName\":\"[AHB Disabled].VMSSize\"}]}", + "size": 0, + "title": "VMs without AHB", + "noDataMessage": "All of your VMs have AHB enabled", + "noDataMessageStyle": 3, + "exportedParameters": [ + { + "fieldName": "VMIDFull", + "parameterName": "WindowsID" + }, + { + "fieldName": "VMRG", + "parameterName": "ResourceGroup", + "parameterType": 1 + }, + { + "fieldName": "VM Name", + "parameterName": "VMName", + "parameterType": 1 + }, + { + "fieldName": "Prioritize AHB?", + "parameterName": "AHBPriority", + "parameterType": 1 + } + ], + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "QuickFix", + "formatter": 7, + "formatOptions": { + "linkTarget": "ArmAction", + "linkLabel": "Apply Hybrid Benefit", + "linkIsContextBlade": true, + "armActionContext": { + "path": "/{WindowsID}?api-version=2023-03-01", + "headers": [], + "params": [], + "body": "{\r\n \"properties\": {\r\n \"licenseType\": \"Windows_Server\"\r\n }\r\n}\r\n\r\n", + "httpMethod": "PATCH", + "title": "Apply Hybrid Benefit to VM {VMName}", + "description": "# Windows Hybrid Benefit Application Information: VM \"{VMName}\"\n\n\n{WindowsID}\n\n**Attention!**\n\nThis action will apply the Windows Hybrid Benefit to the virtual machine with the name **{VMName}**. Please ensure that you are applying the benefit to the correct VM.\n\n**Resource Details:**\n\n- VM Name: {VMName}\n- Resource Group: {ResourceGroup}\n- Prioritize AHB: {AHBPriority}\n\n### Required RBAC Permissions\n\nTo perform this action, you need to have **Contributor** permissions on the Resource Group where the VM is located.\n\nPlease review the information carefully before proceeding with applying the Windows Hybrid Benefit.\n", + "actionName": "Applying Hybrid benefit to VM {VMName}", + "runLabel": "Apply Hybrid Benefit to VM: \"{VMName}\"" + } + } + }, + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Sev4", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + }, + "tooltipFormat": { + "tooltip": "The virtual machines (VMs) with less than 8 cores are categorized as Low Priority, while those with 8 or more cores are classified as High Priority. " + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "WindowsId1", + "formatter": 5 + }, + { + "columnMatch": "Name", + "formatter": 5 + }, + { + "columnMatch": "HyperVGenerations", + "formatter": 5 + }, + { + "columnMatch": "vCPUsPerCore", + "formatter": 5 + }, + { + "columnMatch": "VMIDFull", + "formatter": 5 + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "VM Name", + "label": "VM Name" + }, + { + "columnId": "VMRG", + "label": "Resource Group" + }, + { + "columnId": "VMLocation", + "label": "Location" + }, + { + "columnId": "QuickFix", + "label": "Enable AHB" + }, + { + "columnId": "Prioritize AHB?", + "label": "AHB Priority" + }, + { + "columnId": "VMSize", + "label": "SKU" + }, + { + "columnId": "OSType", + "label": "OS Type" + }, + { + "columnId": "OsVersion", + "label": "OS Version" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + }, + { + "columnId": "vCPUs", + "label": "Number of vCPU" + }, + { + "columnId": "MemoryGB", + "label": "Memory" + }, + { + "columnId": "MaxNetworkInterfaces", + "label": "Max. NICs" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "AHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "VM+SKU+vCores" + }, + { + "type": 1, + "content": { + "json": "List of Windows VMs with Hybrid Benefit groupped by Subscription.", + "style": "info" + }, + "conditionalVisibility": { + "parameterName": "AHBEnabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "AHB By SUbscription" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"50d79765-aad4-437e-a90b-8cc7865e7081\",\"mergeType\":\"inner\",\"leftTable\":\"WindowsAHBEnabled\",\"rightTable\":\"query - Get VM vCPU\",\"leftColumn\":\"VMSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[WindowsAHBEnabled].WindowsId\",\"mergedName\":\"VM Name\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[WindowsAHBEnabled].VMRG\",\"mergedName\":\"Resource Group\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[WindowsAHBEnabled].VMSize\",\"mergedName\":\"VM SKU\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - Get VM vCPU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\">=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"static\",\"resultVal\":\"High Priority\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"static\",\"resultVal\":\"Low Priority\"}}]},{\"originalName\":\"[WindowsAHBEnabled].LicenseType\",\"mergedName\":\"License Type\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores per VM\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"8\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"([\\\"vCPUs\\\"] + 7) & ~7\"}}]},{\"originalName\":\"[WindowsAHBEnabled].VMLocation\",\"mergedName\":\"Location\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[WindowsAHBEnabled].OSType\",\"mergedName\":\"OS Type\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[WindowsAHBEnabled].OsVersion\",\"mergedName\":\"OS Version\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[WindowsAHBEnabled].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[WindowsAHBEnabled].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - 0].VMName\"},{\"originalName\":\"[query - 0].VMSSize\"},{\"originalName\":\"[query - Get VM vCPU].Name\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\"},{\"originalName\":\"[WindowsAHBEnabled].VMSSize\"},{\"originalName\":\"[WindowsAHBEnabled].VMName\"}]}", + "size": 0, + "title": "VMs with AHB", + "noDataMessage": "None of your VMs have AHB enabled", + "noDataMessageStyle": 4, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "2", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Subscription Name", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "WindowsId1", + "label": "VM ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "AHBEnabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "VM+SKU+vCores-AHB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d8deb22b-a596-43ee-acc4-180849d26168\",\"mergeType\":\"inner\",\"leftTable\":\"VM Latest Change Last 7 days\",\"rightTable\":\"VM+SKU+vCores-AHB\",\"leftColumn\":\"VMID\",\"rightColumn\":\"VM Name\"}],\"projectRename\":[{\"originalName\":\"[VM Latest Change Last 7 days].VMID\",\"mergedName\":\"VMID\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM Latest Change Last 7 days].NewLicense\",\"mergedName\":\"NewLicense\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM Latest Change Last 7 days].DateofChange\",\"mergedName\":\"DateofChange\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].VM Name\",\"mergedName\":\"VM Name\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].Resource Group\",\"mergedName\":\"Resource Group\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].VM SKU\",\"mergedName\":\"VM SKU\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].Prioritize AHB?\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].License Type\",\"mergedName\":\"License Type\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].Consumed Cores per VM\",\"mergedName\":\"Consumed Cores per VM\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].Location\",\"mergedName\":\"Location\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].OS Type\",\"mergedName\":\"OS Type\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].OS Version\",\"mergedName\":\"OS Version\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[VM+SKU+vCores-AHB].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "Total Cores Enabled last 7 Days", + "noDataMessage": "Windows AHB hasn't been enabled in the last 7 days", + "showRefreshButton": true, + "queryType": 7, + "visualization": "barchart", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "NewLicense", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "vCPUs", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "chartSettings": { + "xAxis": "VM Name", + "yAxis": [ + "Consumed Cores per VM" + ], + "group": null, + "createOtherGroup": 0, + "seriesLabelSettings": [ + { + "seriesName": "Consumed Cores per VM", + "color": "grayBlue" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "LastAHB", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "Total Cores Enabled last 7 Days" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d8deb22b-a596-43ee-acc4-180849d26168\",\"mergeType\":\"inner\",\"leftTable\":\"VM Latest Change Last 7 days\",\"rightTable\":\"VM+SKU+vCores-AHB\",\"leftColumn\":\"VMID\",\"rightColumn\":\"VM Name\"}],\"projectRename\":[{\"originalName\":\"[VM+SKU+vCores-AHB].VM Name\",\"mergedName\":\"VM Name\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].Resource Group\",\"mergedName\":\"Resource Group\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM Latest Change Last 7 days].NewLicense\",\"mergedName\":\"NewLicense\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM Latest Change Last 7 days].DateofChange\",\"mergedName\":\"DateofChange\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].VM SKU\",\"mergedName\":\"VM SKU\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].Consumed Cores per VM\",\"mergedName\":\"Consumed Cores per VM\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].Prioritize AHB?\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].Location\",\"mergedName\":\"Location\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26168\"},{\"originalName\":\"[VM+SKU+vCores-AHB].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[VM+SKU+vCores-AHB].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"},{\"originalName\":\"[VM Latest Change Last 7 days].VMID\"},{\"originalName\":\"[VM+SKU+vCores-AHB].OS Type\"},{\"originalName\":\"[VM+SKU+vCores-AHB].OS Version\"},{\"originalName\":\"[VM+SKU+vCores-AHB].License Type\"}]}", + "size": 0, + "title": "Total Cores Enabled last 7 Days - Detailed view", + "noDataMessage": "No AHB has been enabled in the last 7 days", + "showExportToExcel": true, + "queryType": 7, + "visualization": "table", + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "NewLicense", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "vCPUs", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "chartSettings": { + "xAxis": "VM Name", + "yAxis": [ + "Consumed Cores per VM" + ], + "group": null, + "createOtherGroup": 0, + "seriesLabelSettings": [ + { + "seriesName": "Consumed Cores per VM", + "color": "grayBlue" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "LastAHB", + "comparison": "isEqualTo", + "value": "Yes" + }, + "showPin": false, + "name": "Total Cores Enabled last 7 Days - Details" + } + ] + }, + "name": "VM" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "VM" + }, + { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "VM" + } + ], + "name": "VM/VMSS-RGFilter" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "SQL Server VMs", + "subTarget": "SQLVM", + "preText": "VM", + "style": "link" + }, + { + "id": "79e7a97a-1413-41e8-b4c6-ebd1d0a45e2e", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "SQL DB", + "subTarget": "SQLDB", + "style": "link" + }, + { + "id": "1f381e5b-7071-41ce-a354-c2df93445cae", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "SQL Managed Instances", + "subTarget": "SQLMI", + "style": "link" + } + ] + }, + "name": "links - 4" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", + "size": 0, + "title": "SQL VM AHB Disabled", + "noDataMessage": "All of your VMs have AHB enabled.", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "Get-SQL-AHB-Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) == 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", + "size": 0, + "title": "SQL VM AHB Enabled", + "noDataMessage": "None of your VMs have AHB enabled.", + "noDataMessageStyle": 5, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "Get-SQL-AHB-Enabled" + }, + { + "type": 1, + "content": { + "json": "## SQL Virtual Machines Azure Hybrid Benefit (AHB) Overview" + }, + "name": "SQL Text" + }, + { + "type": 1, + "content": { + "json": "Apply to SQL Server 1 to 4 vCPUs exchange: For every 1 core of SQL Server Enterprise Edition, you get 4 vCPUs of SQL Managed Instance or Azure SQL Database general purpose and Hyperscale tiers, or 4 vCPUs of SQL Server Standard edition on Azure VMs.\r\n\r\nThe SQL virtual machines (VMs) with less than 4 cores are categorized as **Low Priority**, while those with 8 or more cores are classified as **High Priority**. In situations where there are insufficient Azure Hybrid benefit licenses to cover all the VMs in the environment, it is recommended to prioritize the High Priority VMs.", + "style": "info" + }, + "name": "SQL License Info", + "styleSettings": { + "margin": "10px", + "showBorder": true + } + }, + { + "type": 1, + "content": { + "json": "### AHB Overview\r\nSummary of all SQL on VMs with and without SQL AHB.", + "style": "info" + }, + "name": "AHB Overview21" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "loadType": "explicit", + "loadButtonText": "Load SQL Info", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n | where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID\r\n | extend CheckSQLVMAHB = case(\r\n type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines', iif((properties.['sqlServerLicenseType'])\r\n !has 'AHUB', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by SubscriptionName, CheckSQLVMAHB", + "size": 0, + "title": "Summary of SQL on VMs with or without AHB per Subscription", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ], + "labelSettings": [ + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "CheckSQLVMAHB", + "label": "Is AHB enabled?" + }, + { + "columnId": "count_", + "label": "Number of Resources" + } + ] + }, + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLVMAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" + } + }, + "customWidth": "50", + "name": "Summary of SQL on VMs with or without AHB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n | where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID\r\n | extend CheckSQLVMAHB = case(\r\n type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines', iif((properties.['sqlServerLicenseType'])\r\n !has 'AHUB', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by CheckSQLVMAHB", + "size": 0, + "title": "Summary SQL Enabled and Disabled", + "noDataMessage": "You don't have any SQL VM", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart" + }, + "customWidth": "50", + "name": "Summary SQL Enabled and Disabled" + }, + { + "type": 1, + "content": { + "json": "### Consumed Licenses\r\nTotal number of SQL licenses cores consumed by all SQL running on Virtual Machines.\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Consumed Licenses123" + }, + { + "type": 1, + "content": { + "json": "### Number of required Cores to enable SQL Azure Hybrid Benefit\r\nNumber of cores required to enable SQL AHB across the entire environment.\r\n\r\n\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Number of required Cores to enable SQL AHB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"cd7477ac-acd6-4894-b929-53348c7640e8\",\"mergeType\":\"inner\",\"leftTable\":\"API-Get_VM_SKU\",\"rightTable\":\"Get-SQL-AHB-Enabled\",\"leftColumn\":\"Name\",\"rightColumn\":\"VMSize\"}],\"projectRename\":[{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLID\",\"mergedName\":\"SQLID\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLVersion\",\"mergedName\":\"SQLVersion\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLSKU\",\"mergedName\":\"SQLSKU\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLAgentType\",\"mergedName\":\"SQLAgentType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[API-Get_VM_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLID1\",\"mergedName\":\"SQLID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL VM AHB Consumed Cores per VM", + "noDataMessage": "None of your VMs have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores" + ], + "group": "VMName", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQL+SKU AHB Enabled - per VM" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"cd7477ac-acd6-4894-b929-53348c7640e8\",\"mergeType\":\"inner\",\"leftTable\":\"API-Get_VM_SKU\",\"rightTable\":\"Get-SQL-AHB-Enabled\",\"leftColumn\":\"Name\",\"rightColumn\":\"VMSize\"}],\"projectRename\":[{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLID\",\"mergedName\":\"SQLID\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLVersion\",\"mergedName\":\"SQLVersion\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLSKU\",\"mergedName\":\"SQLSKU\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLAgentType\",\"mergedName\":\"SQLAgentType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLID1\",\"mergedName\":\"SQLID1\",\"fromId\":\"unknown\"},{\"originalName\":\"[API-Get_VM_SKU].Name\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMName\"}]}", + "size": 0, + "title": "SQL VM AHB Consumed Cores per Priority", + "noDataMessage": "None of your VMs have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "showPin": false, + "name": "Summary SQL+SKU AHB Enabled -" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"cd7477ac-acd6-4894-b929-53348c7640b5\",\"mergeType\":\"inner\",\"leftTable\":\"API-Get_VM_SKU\",\"rightTable\":\"Get-SQL-AHB-Disabled\",\"leftColumn\":\"Name\",\"rightColumn\":\"VMSize\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"([\\\"vCPUs\\\"] +3) & ~3\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[API-Get_VM_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLID\",\"mergedName\":\"SQLID\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLVersion\",\"mergedName\":\"SQLVersion\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLSKU\",\"mergedName\":\"SQLSKU\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLAgentType\",\"mergedName\":\"SQLAgentType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLID1\",\"mergedName\":\"SQLID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "Cores not enabled per AHB Priority", + "noDataMessage": "All of your VMs have AHB enabled.", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "warning", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "sortBy": [], + "chartSettings": { + "yAxis": [ + "Consumed Cores" + ], + "showMetrics": false, + "showLegend": true + } + }, + "customWidth": "33", + "name": " Summary - SQL Cores AHB Disabled " + } + ] + }, + "name": "SQL Overview RG" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", + "version": "KqlParameterItem/1.0", + "name": "SQLAVMHUBEnabled", + "label": "See SQL VMs with AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 + } + }, + { + "id": "f1ac5e53-253c-4afb-8bc5-b1ba2efea3eb", + "version": "KqlParameterItem/1.0", + "name": "SQLVMAHBDisabled", + "label": "See SQL VMs without AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "value": "Yes" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "SQL AHB Disabled" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"cd7477ac-acd6-4894-b929-53348c7640b5\",\"mergeType\":\"inner\",\"leftTable\":\"API-Get_VM_SKU\",\"rightTable\":\"Get-SQL-AHB-Disabled\",\"leftColumn\":\"Name\",\"rightColumn\":\"VMSize\"}],\"projectRename\":[{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLID\",\"mergedName\":\"VM Name\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLVersion\",\"mergedName\":\"SQLVersion\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLSKU\",\"mergedName\":\"SQLSKU\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLAgentType\",\"mergedName\":\"SQLAgentType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640b5\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLID1\",\"mergedName\":\"SQLID1\",\"fromId\":\"unknown\"},{\"originalName\":\"[API-Get_VM_SKU].Name\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLID\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMName\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMRG\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMLocation\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLVersion\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLSKU\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SQLAgentType\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].LicenseType\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].SubscriptionName\"},{\"originalName\":\"[Get-SQL-AHB-Disabled].VMSize\"}]}", + "size": 0, + "title": "SQL VM AHB Disabled", + "noDataMessage": "All of your VMs have AHB enabled.", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "warning", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "SubscriptionName" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "VM Name", + "label": "Name" + }, + { + "columnId": "VMRG", + "label": "Resource Group" + }, + { + "columnId": "VMLocation", + "label": "Location" + }, + { + "columnId": "VMSize", + "label": "SKU" + }, + { + "columnId": "vCPUs", + "label": "Number of vCPU" + }, + { + "columnId": "Consumed Cores", + "label": "Consumed Cores" + }, + { + "columnId": "SQLVersion", + "label": "SQL Version" + }, + { + "columnId": "SQLSKU", + "label": "SQL SKU" + }, + { + "columnId": "SQLAgentType", + "label": "SQL Agent" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "SQLID1", + "label": "Resource ID" + } + ] + }, + "sortBy": [] + }, + "conditionalVisibility": { + "parameterName": "SQLVMAHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL+SKU AHB Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"cd7477ac-acd6-4894-b929-53348c7640e8\",\"mergeType\":\"inner\",\"leftTable\":\"API-Get_VM_SKU\",\"rightTable\":\"Get-SQL-AHB-Enabled\",\"leftColumn\":\"Name\",\"rightColumn\":\"VMSize\"}],\"projectRename\":[{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLID\",\"mergedName\":\"SQLID\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLVersion\",\"mergedName\":\"SQLVersion\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLSKU\",\"mergedName\":\"SQLSKU\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLAgentType\",\"mergedName\":\"SQLAgentType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"cd7477ac-acd6-4894-b929-53348c7640e8\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].SQLID1\",\"mergedName\":\"SQLID1\",\"fromId\":\"unknown\"},{\"originalName\":\"[API-Get_VM_SKU].Name\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\"},{\"originalName\":\"[Get-SQL-AHB-Enabled].VMName\"}]}", + "size": 0, + "title": "SQL VM AHB Enabled", + "noDataMessage": "None of your VMs have AHB enabled.", + "noDataMessageStyle": 4, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "Subscription", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "SubscriptionName" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "SQLID", + "label": "Name" + }, + { + "columnId": "VMRG", + "label": "Resource Group" + }, + { + "columnId": "VMLocation", + "label": "Location" + }, + { + "columnId": "VMSize", + "label": "SKU" + }, + { + "columnId": "vCPUs", + "label": "Number of vCPU" + }, + { + "columnId": "Consumed Cores", + "label": "Consumed Cores" + }, + { + "columnId": "SQLVersion", + "label": "SQL Version" + }, + { + "columnId": "SQLSKU", + "label": "SQL SKU" + }, + { + "columnId": "SQLAgentType", + "label": "SQL Agent" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "SQLID1", + "label": "Resource ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "SQLAVMHUBEnabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL+SKU AHB Enabled" + } + ] + }, + "name": "SQL Detailed Info" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "SQLVM" + }, + "name": "SQL VM" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "title": "SQL Database", + "items": [ + { + "type": 1, + "content": { + "json": "## SQL Databases Azure Hybrid Benefit (AHB) Overview" + }, + "name": "SQL Databases AHB" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "e4aa368f-dcf2-44a6-88f9-a395c04eb21f", + "cellValue": "SQLType", + "linkTarget": "parameter", + "linkLabel": "SQL Database", + "subTarget": "SQLDatabase", + "style": "link" + }, + { + "id": "a94e8dc2-34be-4d97-934d-c27e1816c4fe", + "cellValue": "SQLType", + "linkTarget": "parameter", + "linkLabel": "SQL ElasticPool", + "subTarget": "SQLElastic", + "style": "link" + } + ] + }, + "name": "links - 8" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "SQLDB" + }, + "name": "text - 0" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and tostring(properties.['licenseType']) == 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\" and tostring(sku.name) != \"ElasticPool\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])\r\n| extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/databases', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n )\r\n) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, CheckSQLDBAHB,SQLLocation, LicenseType, StorageAccountType, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n", + "size": 0, + "title": "AHB Disabled", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLDB AHB Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and tostring(properties.['licenseType']) != 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\" and tostring(sku.name) != \"ElasticPool\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=sku.name, SKUTier=sku.tier, SQLLocation = location, vCores=tostring(sku.capacity), LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, SQLLocation, LicenseType, StorageAccountType, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n\r\n", + "size": 0, + "title": "AHB Enabled", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLDB AHB Enabled" + }, + { + "type": 1, + "content": { + "json": "Apply to SQL Server 1 to 4 vCPUs exchange: For every 1 core of SQL Server Enterprise Edition, you get 4 vCPUs of SQL Managed Instance or Azure SQL Database general purpose and Hyperscale tiers, or 4 vCPUs of SQL Server Standard edition on Azure VMs.\r\n\r\nThe SQL virtual machines (VMs) with less than 4 cores are categorized as **Low Priority**, while those with 8 or more cores are classified as **High Priority**. In situations where there are insufficient Azure Hybrid benefit licenses to cover all the VMs in the environment, it is recommended to prioritize the High Priority VMs.\r\n\r\nFor Azure SQL Database, Azure Hybrid Benefit is only available when using the provisioned compute tier of the vCore-based purchasing model. Azure Hybrid Benefit doesn't apply to DTU-based purchasing models or the serverless compute tier.", + "style": "info" + }, + "name": "Apply to SQL Server 1 to 4 vCPUs " + }, + { + "type": 1, + "content": { + "json": "### AHB Overview\r\nSummary of all SQL Databases with and without SQL AHB.", + "style": "info" + }, + "name": " AHB Overview SQL DB" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "loadType": "explicit", + "loadButtonText": "Load SQL DB Info", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and kind contains 'vcore' and kind !contains \"serverless\" and tostring(sku.name) != \"ElasticPool\"\r\n | extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=sku.name, SKUTier=sku.tier, SQLLocation = location, LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/databases', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by SubscriptionName, CheckSQLDBAHB", + "size": 0, + "title": "Summary of SQL Databases with or without AHB per Subscription", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ], + "labelSettings": [ + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "CheckSQLDBAHB", + "label": "Is AHB enabled?" + }, + { + "columnId": "count_", + "label": "Number of resources" + } + ] + }, + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLDBAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" + } + }, + "customWidth": "50", + "name": "Summary of SQL DBs with or without AHB per subs" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and kind contains 'vcore' and kind !contains \"serverless\" and tostring(sku.name) != \"ElasticPool\"\r\n | extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=sku.name, SKUTier=sku.tier, SQLLocation = location, LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/databases', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by CheckSQLDBAHB", + "size": 0, + "title": "Summary of SQL Databases with or without AHB", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLDBAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" + } + }, + "customWidth": "50", + "name": "Summary of SQL DBs with or without AHB " + }, + { + "type": 1, + "content": { + "json": "### Consumed Licenses\r\nTotal number of SQL licenses cores consumed by all SQL Databases\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Total number of SQL licenses cores consumed" + }, + { + "type": 1, + "content": { + "json": "### Number of required Cores to enable SQL Azure Hybrid Benefit\r\nNumber of cores required to enable SQL AHB across the entire environment.\r\n\r\n\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Text SQL DB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Consumed Cores per VM", + "noDataMessage": "None of your SQL DB have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores" + ], + "group": "SQLName", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLDB+SKU AHB Enabled - per VM" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Consumed Cores per Priority", + "noDataMessage": "None of your SQL DB have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLDB+SKU AHB Enabled - per Priority" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Cores not enabled per AHB Priority", + "noDataMessage": "All of your SQL DB have AHB enabled.", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLDB+SKU AHB Disabled - per Priority" + } + ] + }, + "name": "SQL DB Info" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", + "version": "KqlParameterItem/1.0", + "name": "SQLDBHUBEnabled", + "label": "See SQL DBs with AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 + } + }, + { + "id": "f1ac5e53-253c-4afb-8bc5-b1ba2efea3eb", + "version": "KqlParameterItem/1.0", + "name": "SQLDBAHBDisabled", + "label": "See SQL DBs without AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "SQL DB Without AHB" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071ed\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Disabled", + "noDataMessage": "All of your SQL DBs have AHB enabled.", + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "SubscriptionName", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "SubscriptionName" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "SQLDBID", + "label": "Database Name" + }, + { + "columnId": "SQLName", + "label": "Server Name" + }, + { + "columnId": "SQLRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "vCores", + "label": "Number of vCore" + }, + { + "columnId": "CheckSQLDBAHB", + "label": "Is AHB enabled?" + }, + { + "columnId": "SQLLocation", + "label": "Location" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "StorageAccountType", + "label": "Storage Account Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "SQLDBID1", + "label": "Resource ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "SQLDBAHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL DB Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071f9\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Enabled", + "noDataMessage": "None of you SQL DBs have AHB enabled.", + "noDataMessageStyle": 4, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "labelSettings": [ + { + "columnId": "SQLDBID", + "label": "Name" + }, + { + "columnId": "SQLName", + "label": "Database Name" + }, + { + "columnId": "SQLRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "SQLLocation", + "label": "Location" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "StorageAccountType", + "label": "Storage Account Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "vCores", + "label": "Number of vCore" + }, + { + "columnId": "SQLDBID1", + "label": "Resource ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "SQLDBHUBEnabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL DB AHB Enabled" + } + ] + }, + "name": "Load SQL DB Detailed Info" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SQLType", + "comparison": "isEqualTo", + "value": "SQLDatabase" + }, + "name": "SQLDatabase" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "SQL Elastic Pool" + }, + "name": "text - 0" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources\r\n| where type =~ 'Microsoft.Sql/servers/elasticPools' and tostring(properties.['licenseType']) == 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType'])\r\n| extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/elasticPools', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n )\r\n) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, CheckSQLDBAHB,SQLLocation, LicenseType, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n", + "size": 0, + "title": "AHB Disabled", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLElastic AHB Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources\r\n| where type =~ 'Microsoft.Sql/servers/elasticPools' and tostring(properties.['licenseType']) != 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType'])\r\n| extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/elasticPools', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n ) \r\n ) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, SQLLocation, LicenseType, CheckSQLDBAHB, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n\r\n", + "size": 0, + "title": "AHB Enabled", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLElastic AHB Enabled" + }, + { + "type": 1, + "content": { + "json": "Apply to SQL Server 1 to 4 vCPUs exchange: For every 1 core of SQL Server Enterprise Edition, you get 4 vCPUs of SQL Managed Instance or Azure SQL Database general purpose and Hyperscale tiers, or 4 vCPUs of SQL Server Standard edition on Azure VMs.\r\n\r\nThe SQL virtual machines (VMs) with less than 4 cores are categorized as **Low Priority**, while those with 8 or more cores are classified as **High Priority**. In situations where there are insufficient Azure Hybrid benefit licenses to cover all the VMs in the environment, it is recommended to prioritize the High Priority VMs.\r\n\r\nFor Azure SQL Database, Azure Hybrid Benefit is only available when using the provisioned compute tier of the vCore-based purchasing model. Azure Hybrid Benefit doesn't apply to DTU-based purchasing models or the serverless compute tier.", + "style": "info" + }, + "name": "Apply to SQL Elastic Server 1 to 4 vCPUs " + }, + { + "type": 1, + "content": { + "json": "### AHB Overview\r\nSummary of all SQL Databases with and without SQL AHB.", + "style": "info" + }, + "name": " AHB Overview SQL Elastic" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "loadType": "explicit", + "loadButtonText": "Load SQL DB Info", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources\r\n| where type =~ 'Microsoft.Sql/servers/elasticPools' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/elasticPools', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by SubscriptionName, CheckSQLDBAHB", + "size": 0, + "title": "Summary of SQL Databases with or without AHB per Subscription", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ], + "labelSettings": [ + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "CheckSQLDBAHB", + "label": "Is AHB enabled?" + }, + { + "columnId": "count_", + "label": "Number of resources" + } + ] + }, + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLDBAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" + } + }, + "customWidth": "50", + "name": "Summary of SQL Elastic with or without AHB per subs" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources\r\n| where type =~ 'Microsoft.Sql/servers/elasticPools' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/elasticPools', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by CheckSQLDBAHB", + "size": 0, + "title": "Summary of SQL Databases with or without AHB", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLDBAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" + } + }, + "customWidth": "50", + "name": "Summary of SQL DBs with or without AHB " + }, + { + "type": 1, + "content": { + "json": "### Consumed Licenses\r\nTotal number of SQL licenses cores consumed by all SQL Databases\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Total number of SQL licenses cores consumed" + }, + { + "type": 1, + "content": { + "json": "### Number of required Cores to enable SQL Azure Hybrid Benefit\r\nNumber of cores required to enable SQL AHB across the entire environment.\r\n\r\n\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Text SQL DB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"}]}", + "size": 0, + "title": "SQL DB Elastic Pools AHB Consumed Cores per VM", + "noDataMessage": "None of your SQL DB Elastic Pools have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "vCores" + ], + "group": "SQLName", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLElastic+SKU AHB Enabled - per VM" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"}]}", + "size": 0, + "title": "SQL Elastic AHB Consumed Cores per Priority", + "noDataMessage": "None of your SQL DB Elastic Pools have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "vCores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLElastic+SKU AHB Enabled - per Priority" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLElastic AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"}]}", + "size": 0, + "title": "SQL DB AHB Cores not enabled per AHB Priority", + "noDataMessage": "All of your SQL DB have AHB enabled.", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "vCores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLDB+SKU AHB Disabled - per Priority" + } + ] + }, + "name": "SQL Elastic Info" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", + "version": "KqlParameterItem/1.0", + "name": "SQLDBHUBEnabled", + "label": "See SQL DBs with AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 + } + }, + { + "id": "f1ac5e53-253c-4afb-8bc5-b1ba2efea3eb", + "version": "KqlParameterItem/1.0", + "name": "SQLDBAHBDisabled", + "label": "See SQL DBs without AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "SQL DB Without AHB" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071ed\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"}]}", + "size": 0, + "title": "SQL DB AHB Disabled", + "noDataMessage": "All of your SQL DBs have AHB enabled.", + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "SubscriptionName", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "SubscriptionName" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "SQLDBID", + "label": "Database Name" + }, + { + "columnId": "SQLName", + "label": "Server Name" + }, + { + "columnId": "SQLRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "vCores", + "label": "Number of vCore" + }, + { + "columnId": "CheckSQLDBAHB", + "label": "Is AHB enabled?" + }, + { + "columnId": "SQLLocation", + "label": "Location" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "SQLDBID1", + "label": "Resource ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "SQLDBAHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL DB Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071f9\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"}]}", + "size": 0, + "title": "SQL DB AHB Enabled", + "noDataMessage": "None of you SQL DBs have AHB enabled.", + "noDataMessageStyle": 4, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "labelSettings": [ + { + "columnId": "SQLDBID", + "label": "Name" + }, + { + "columnId": "SQLName", + "label": "Database Name" + }, + { + "columnId": "SQLRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "vCores", + "label": "Number of vCore" + }, + { + "columnId": "SQLLocation", + "label": "Location" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "SQLDBID1", + "label": "Resource ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "SQLDBHUBEnabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL DB AHB Enabled" + } + ] + }, + "name": "Load SQL DB Detailed Info" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SQLType", + "comparison": "isEqualTo", + "value": "SQLElastic" + }, + "name": "SQLElasticPool" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "SQLDB" + }, + "name": "SQLDBGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "title": "SQL Managed Instance", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n | extend SubscriptionName=name \r\n | join (resources | where type =~ 'Microsoft.Sql/managedInstances' and tostring(properties.['licenseType']) == 'LicenseIncluded'\r\n | extend ManagedInstance=id, SQLRG=resourceGroup, SQLLocation=location, vCores=tostring(sku.capacity),LicenseType = tostring(properties.['licenseType'])\r\n | extend CheckSQLMIAHB = case(\r\n type =~ 'Microsoft.Sql/managedInstances', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n ) \r\n ) on subscriptionId \r\n | project ManagedInstance,SQLRG, SQLLocation, CheckSQLMIAHB, vCores, LicenseType, SubscriptionName\r\n | where SQLRG in ({ResourceGroup})\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), ManagedInstance=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct ManagedInstance\r\n )\r\n on ManagedInstance", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLMIAHBDisabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n | extend SubscriptionName=name \r\n | join (resources | where type =~ 'Microsoft.Sql/managedInstances' and tostring(properties.['licenseType']) != 'LicenseIncluded'\r\n | extend ManagedInstance=id, SQLName=name, SQLRG=resourceGroup, SQLLocation=location, vCores=tostring(sku.capacity),LicenseType = tostring(properties.['licenseType'])\r\n | extend CheckSQLMIAHB = case(\r\n type =~ 'Microsoft.Sql/managedInstances', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n ) \r\n ) on subscriptionId \r\n | project ManagedInstance, SQLName, SQLRG, SQLLocation, CheckSQLMIAHB, vCores, LicenseType, SubscriptionName\r\n | where SQLRG in ({ResourceGroup})\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), ManagedInstance=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct ManagedInstance\r\n )\r\n on ManagedInstance\r\n ", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLMIAHBEnabled" + }, + { + "type": 1, + "content": { + "json": "# SQL Managed Instances Azure Hybrid Benefit (AHB) Overview\r\n" + }, + "name": "SQL Managed Instances AHB" + }, + { + "type": 1, + "content": { + "json": "Apply to SQL Server 1 to 4 vCPUs exchange: For every 1 core of SQL Server Enterprise Edition, you get 4 vCPUs of SQL Managed Instance or Azure SQL Database general purpose and Hyperscale tiers, or 4 vCPUs of SQL Server Standard edition on Azure VMs.\r\n\r\nThe SQL virtual machines (VMs) with less than 4 cores are categorized as **Low Priority**, while those with 8 or more cores are classified as **High Priority**. In situations where there are insufficient Azure Hybrid benefit licenses to cover all the VMs in the environment, it is recommended to prioritize the High Priority VMs.", + "style": "info" + }, + "name": "Apply to SQL Server 1 to 4 vCPUs exchange" + }, + { + "type": 1, + "content": { + "json": "### AHB Overview\r\nSummary of all SQL Databases with and without SQL AHB.", + "style": "info" + }, + "name": "SQL Databases with and without SQL AHB." + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "loadType": "explicit", + "loadButtonText": "Load SQL MI Info", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n | extend SubscriptionName=name \r\n | join (resources | where type =~ 'Microsoft.Sql/managedInstances' \r\n | extend ManagedInstance=id, SQLRG=resourceGroup, SQLLocation=location, vCores=tostring(sku.capacity),LicenseType = tostring(properties.['licenseType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), ManagedInstance=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct ManagedInstance\r\n )\r\n on ManagedInstance\r\n | extend CheckSQLMIAHB = case(\r\n type =~ 'Microsoft.Sql/managedInstances', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n ) \r\n ) on subscriptionId \r\n| project ManagedInstance,SQLRG, SQLLocation, CheckSQLMIAHB, vCores, LicenseType, SubscriptionName\r\n| summarize count() by SubscriptionName, CheckSQLMIAHB", + "size": 0, + "title": "Summary of SQL MI with or without AHB per Subscription", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLMIAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName" + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + }, + "showBorder": false + }, + "chartSettings": { + "yAxis": [ + "count_" + ], + "group": "CheckSQLMIAHB", + "createOtherGroup": null + } + }, + "customWidth": "50", + "name": "Summary of SQL MI with or without AHB per subs" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" \r\n | extend SubscriptionName=name \r\n | join (resources | where type =~ 'Microsoft.Sql/managedInstances'\r\n | extend ManagedInstance=id, SQLRG=resourceGroup, SQLLocation=location, vCores=tostring(sku.capacity),LicenseType = tostring(properties.['licenseType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), ManagedInstance=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct ManagedInstance\r\n )\r\n on ManagedInstance\r\n | extend CheckSQLMIAHB = case(\r\n type =~ 'Microsoft.Sql/managedInstances', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n ) \r\n ) on subscriptionId \r\n| project ManagedInstance,SQLRG, SQLLocation, CheckSQLMIAHB, vCores, LicenseType, SubscriptionName\r\n| summarize count() by SubscriptionName, CheckSQLMIAHB", + "size": 0, + "title": "Summary of SQL Managed Instance with or without AHB", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "chartSettings": { + "yAxis": [ + "count_" + ], + "group": "CheckSQLMIAHB", + "createOtherGroup": null + } + }, + "customWidth": "50", + "name": "Summary of SQL MI with or without AHB " + }, + { + "type": 1, + "content": { + "json": "### Consumed Licenses\r\nTotal number of SQL licenses cores consumed by all SQL Managed Instances.\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Consumed Licenses" + }, + { + "type": 1, + "content": { + "json": "### Number of required Cores to enable SQL Azure Hybrid Benefit\r\nNumber of cores required to enable SQL AHB across the entire environment.\r\n\r\n\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Number of required Cores to enable SQL " + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLMIAHBEnabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLMIAHBEnabled].ManagedInstance\",\"mergedName\":\"ManagedInstance\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].CheckSQLMIAHB\",\"mergedName\":\"CheckSQLMIAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].ManagedInstance1\",\"mergedName\":\"ManagedInstance1\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLMIAHBEnabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL Managed Instance AHB Consumed Cores per VM", + "noDataMessage": "None of your SQL MI have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "vCores" + ], + "group": "SQLName", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLMI+SKU AHB Enabled - per VM" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLMIAHBEnabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLMIAHBEnabled].ManagedInstance\",\"mergedName\":\"ManagedInstance\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].CheckSQLMIAHB\",\"mergedName\":\"CheckSQLMIAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBEnabled].ManagedInstance1\",\"mergedName\":\"ManagedInstance1\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLMIAHBEnabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL Managed Instance AHB Consumed Cores per Priority", + "noDataMessage": "None of your SQL MI have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "vCores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLMI+SKU AHB Enabled - per Priority" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLMIAHBDisabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLMIAHBDisabled].ManagedInstance\",\"mergedName\":\"ManagedInstance\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBDisabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBDisabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBDisabled].CheckSQLMIAHB\",\"mergedName\":\"CheckSQLMIAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBDisabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBDisabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBDisabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLMIAHBDisabled].ManagedInstance1\",\"mergedName\":\"ManagedInstance1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL Managed Instances AHB Cores not enabled per AHB Priority", + "noDataMessage": "All of your SQL MI have AHB enabled.", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "vCores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLMI+SKU AHB Disabled - per Priority" + } + ] + }, + "name": "SQL MI Info" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", + "version": "KqlParameterItem/1.0", + "name": "SQLMIAHBEnabled", + "label": "See SQL MIs with AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 + } + }, + { + "id": "f1ac5e53-253c-4afb-8bc5-b1ba2efea3eb", + "version": "KqlParameterItem/1.0", + "name": "SQLMIAHBDisabled", + "label": "See SQL MIs without AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "SQL MI AHB Disabled" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071ed\",\"mergeType\":\"table\",\"leftTable\":\"SQLMIAHBDisabled\"}],\"projectRename\":[{\"originalName\":\"[SQLMIAHBDisabled].ManagedInstance\",\"mergedName\":\"ManagedInstance\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLMIAHBDisabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLMIAHBDisabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLMIAHBDisabled].CheckSQLMIAHB\",\"mergedName\":\"CheckSQLMIAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLMIAHBDisabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLMIAHBDisabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLMIAHBDisabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"}]}", + "size": 0, + "title": "SQL Managed Instance AHB Disabled", + "noDataMessage": "All of your SQL MIs have AHB enabled.", + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "SubscriptionName", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "SubscriptionName" + ], + "expandTopLevel": true + } + } + }, + "conditionalVisibility": { + "parameterName": "SQLMIAHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL MI Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071f9\",\"mergeType\":\"table\",\"leftTable\":\"SQLMIAHBEnabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLMIAHBEnabled].ManagedInstance\",\"mergedName\":\"ManagedInstance\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLMIAHBEnabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLMIAHBEnabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLMIAHBEnabled].CheckSQLMIAHB\",\"mergedName\":\"CheckSQLMIAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLMIAHBEnabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLMIAHBEnabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLMIAHBEnabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"}]}", + "size": 0, + "title": "SQL Managed Instance AHB Enabled", + "noDataMessage": "None of you SQL MIs have AHB enabled.", + "showExportToExcel": true, + "queryType": 7 + }, + "conditionalVisibility": { + "parameterName": "SQLMIAHBEnabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL MI AHB Enabled" + } + ] + }, + "name": "SQL MI Detailed" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "SQLMI" + }, + "name": "SQL MI" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "SQL" + }, + "name": "SQLAHB" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "title": "Linux Hybrid Benefit", + "loadType": "explicit", + "loadButtonText": "Load Linux Recommendations", + "items": [ + { + "type": 1, + "content": { + "json": "## Linux Azure Hybrid Benefit (AHB) Overview" + }, + "name": "Linux Text" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":null,\"headers\":[],\"method\":\"GET\",\"path\":\"/subscriptions/{SingleSubHidden}/providers/Microsoft.Compute/skus?$filter=location eq '{Location}'\",\"urlParams\":[{\"key\":\"api-version\",\"value\":\"2021-07-01\"}],\"batchDisabled\":false,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.*[?(@.resourceType=='virtualMachines')]\",\"columns\":[{\"path\":\"name\",\"columnid\":\"Name\"},{\"path\":\"capabilities[?(@.name=='vCPUs')].value\",\"columnid\":\"vCPUs\"},{\"path\":\"capabilities[?(@.name=='MemoryGB')].value\",\"columnid\":\"MemoryGB\"},{\"path\":\"capabilities[?(@.name=='MaxNetworkInterfaces')].value\",\"columnid\":\"MaxNetworkInterfaces\"},{\"path\":\"capabilities[?(@.name=='HyperVGenerations')].value\",\"columnid\":\"HyperVGenerations\"},{\"path\":\"capabilities[?(@.name=='vCPUsPerCore')].value\",\"columnid\":\"vCPUsPerCore\"}]}}]}", + "size": 0, + "title": "Get VM vCPU", + "exportParameterName": "ResourceSKU", + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "rowLimit": 5000 + } + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "API-Get_VMLinux_SKU" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines' and (properties.storageProfile.imageReference.publisher == 'suse' or properties.storageProfile.imageReference.publisher=='RedHat')\r\n| where isnull ((properties.['licenseType']))\r\n| extend LinuxId=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.storageProfile.imageReference.publisher), OsVersion = tostring(properties.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.['licenseType']), VMSSize=tostring(sku.name)\r\n| order by type asc \r\n| project LinuxId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OsVersion,LicenseType\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), LinuxId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct LinuxId\r\n )\r\n on LinuxId", + "size": 0, + "title": "AHB Disabled", + "noDataMessage": "None of your Linux VMs have AHB enabled.", + "noDataMessageStyle": 4, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "LinuxAHBDisabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines' and (properties.storageProfile.imageReference.publisher == 'suse' or properties.storageProfile.imageReference.publisher=='RedHat')\r\n| where isnotnull ((properties.['licenseType']))\r\n| extend LinuxId=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.storageProfile.imageReference.publisher), OsVersion = tostring(properties.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.['licenseType']), VMSSize=tostring(sku.name)\r\n| order by type asc \r\n| project LinuxId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OsVersion,LicenseType\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), LinuxId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct LinuxId\r\n )\r\n on LinuxId", + "size": 0, + "title": "AHB Enabled", + "noDataMessage": "All of your Linux VMs have AHB enabled.", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "LinuxAHBRGEnabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines' and (properties.storageProfile.imageReference.publisher == 'suse' or properties.storageProfile.imageReference.publisher=='RedHat')\r\n| extend LicenseType = tostring(properties.['licenseType'])\r\n| extend LinuxId=id\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), LinuxId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct LinuxId\r\n )\r\n on LinuxId\r\n| extend CheckAHBLinux = case(\r\n type == 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets',\r\n iff(isnull((properties.['licenseType'])),\r\n \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not Linux\"\r\n )\r\n| summarize count() by CheckAHBLinux", + "size": 0, + "title": "Summary of Linux VMs with or without AHB", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart" + }, + "customWidth": "50", + "name": "Summary of Linux VMs with or without AHB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d8deb22b-a596-43ee-acc4-180849d26130\",\"mergeType\":\"inner\",\"leftTable\":\"LinuxAHBRGEnabled\",\"rightTable\":\"API-Get_VMLinux_SKU\",\"leftColumn\":\"VMSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"ConsumedCores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"8\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"[\\\"vCPUs\\\"]\"}}]},{\"originalName\":\"[WindowsAHBEnabled].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[WindowsAHBEnabled].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"},{\"originalName\":\"[LinuxAHBRGEnabled].LinuxId\",\"mergedName\":\"LinuxId\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMSSize\",\"mergedName\":\"VMSSize\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[LinuxAHBRGEnabled].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[LinuxAHBRGEnabled].OsVersion\",\"mergedName\":\"OsVersion\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[LinuxAHBRGEnabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[LinuxAHBRGEnabled].LinuxId1\",\"mergedName\":\"LinuxId1\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VMLinux_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VMLinux_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VMLinux_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VMLinux_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VMLinux_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VMLinux_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"}]}", + "size": 0, + "title": "Consumed Cores per VM", + "noDataMessage": "None of your Linux VM have AHB enabled", + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "ConsumedCores", + "formatter": 0, + "formatOptions": { + "aggregation": "Sum" + } + } + ] + }, + "tileSettings": { + "titleContent": {}, + "leftContent": { + "columnMatch": "ConsumedCores", + "formatter": 12, + "formatOptions": { + "palette": "blue" + } + }, + "showBorder": false + }, + "graphSettings": { + "type": 0 + }, + "chartSettings": { + "yAxis": [ + "ConsumedCores" + ], + "group": "VMName", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Linux Consumed Cores per VM" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", + "version": "KqlParameterItem/1.0", + "name": "LinuxAHBEnabled", + "label": "See Linux VMs with AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 + } + }, + { + "id": "4c3ff9fa-d9c8-4d35-94d4-48ba3a1547fd", + "version": "KqlParameterItem/1.0", + "name": "LinuxAHBDisabled", + "label": "See Linux VMs without AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [] + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 + } + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "Linux VMs without AHB" + }, + { + "type": 1, + "content": { + "json": "List of Linux VMs with Hybrid Benefit groupped by Subscription." + }, + "conditionalVisibility": { + "parameterName": "LinuxAHBEnabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "Linux VMs with Hybrid Benefit" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\",\"mergeType\":\"inner\",\"leftTable\":\"LinuxAHBRGEnabled\",\"rightTable\":\"API-Get_VMLinux_SKU\",\"leftColumn\":\"VMSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[LinuxAHBRGEnabled].LinuxId\",\"mergedName\":\"VM ID\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBRGEnabled].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBRGEnabled].OsVersion\",\"mergedName\":\"OsVersion\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBRGEnabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBRGEnabled].LinuxId1\",\"mergedName\":\"LinuxId1\",\"fromId\":\"unknown\"},{\"originalName\":\"[LinuxAHBEnabled].VMName\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\"},{\"originalName\":\"[API-Get_VM_SKU].Name\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\"},{\"originalName\":\"[LinuxAHBEnabled].VMSSize\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMName\"},{\"originalName\":\"[LinuxAHBRGEnabled].VMSSize\"}]}", + "size": 0, + "title": "Linux VMs with AHB", + "noDataMessage": "None of your Linux VMs have AHB enabled", + "noDataMessageStyle": 4, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Sev4", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + } + } + ] + }, + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "NewLicense", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "vCPUs", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "chartSettings": { + "xAxis": "VM Name", + "yAxis": [ + "Consumed Cores per VM" + ], + "group": null, + "createOtherGroup": 0, + "seriesLabelSettings": [ + { + "seriesName": "Consumed Cores per VM", + "color": "grayBlue" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "LinuxAHBEnabled", + "comparison": "isEqualTo", + "value": "yes" + }, + "name": "Linux-VM+SKU+vCores-AHB" + }, + { + "type": 1, + "content": { + "json": "List of Linux VMs without Hybrid Benefit groupped by Subscription." + }, + "conditionalVisibility": { + "parameterName": "LinuxAHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "LinuxAHBDisabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\",\"mergeType\":\"inner\",\"leftTable\":\"LinuxAHBDisabled\",\"rightTable\":\"API-Get_VMLinux_SKU\",\"leftColumn\":\"VMSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[LinuxAHBDisabled].LinuxId\",\"mergedName\":\"LinuxId\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBDisabled].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBDisabled].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBDisabled].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBDisabled].VMSSize\",\"mergedName\":\"VMSSize\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBDisabled].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBDisabled].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBDisabled].OsVersion\",\"mergedName\":\"OsVersion\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBDisabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[API-Get_VMLinux_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"8a27a7d8-5ea8-4408-ac20-2fc4e65ca095\"},{\"originalName\":\"[LinuxAHBDisabled].LinuxId1\",\"mergedName\":\"LinuxId1\",\"fromId\":\"unknown\"},{\"originalName\":\"[LinuxAHBEnabled].VMName\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\"},{\"originalName\":\"[API-Get_VM_SKU].Name\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\"},{\"originalName\":\"[LinuxAHBEnabled].VMSSize\"}]}", + "size": 0, + "title": "Linux VMs without AHB", + "noDataMessage": "None of your Linux VMs have AHB enabled", + "noDataMessageStyle": 4, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Sev4", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + } + } + ] + }, + "tileSettings": { + "showBorder": false, + "titleContent": { + "columnMatch": "NewLicense", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "vCPUs", + "formatter": 12, + "formatOptions": { + "palette": "auto" + }, + "numberFormat": { + "unit": 17, + "options": { + "maximumSignificantDigits": 3, + "maximumFractionDigits": 2 + } + } + } + }, + "chartSettings": { + "xAxis": "VM Name", + "yAxis": [ + "Consumed Cores per VM" + ], + "group": null, + "createOtherGroup": 0, + "seriesLabelSettings": [ + { + "seriesName": "Consumed Cores per VM", + "color": "grayBlue" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "LinuxAHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "Linux-VM+SKU+vCores-AHBDisabled" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "LinuxVM" + }, + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "VM" + } + ], + "name": "Linux" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "title": "VMSS", + "items": [ + { + "type": 1, + "content": { + "json": "## Windows Azure Hybrid Benefit (AHB) Overview - VM Scale Set" + }, + "name": "AHB Overview - VM Scale Set" + }, + { + "type": 1, + "content": { + "json": "Each two-processor license or each set of 16-core licenses, either Datacenter or Standard editions, are entitled to two instances of up to 8 cores, or one instance of up to 16 cores.\r\n\r\nThe virtual machines (VMs) with less than 8 cores are categorized as **Low Priority**, while those with 8 or more cores are classified as **High Priority**. In situations where there are insufficient Azure Hybrid benefit licenses to cover all the VMs in the environment, it is recommended to prioritize the High Priority VMs.", + "style": "info" + }, + "name": "Each two-processor license or each set of 16-core licenses" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualMachineScaleSets'\r\n| where tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType) == 'Windows' and tostring(properties.virtualMachineProfile.licenseType) == \"Windows_Server\"\r\n| extend WindowsId=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType), OSVersion = tostring(properties.virtualMachineProfile.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.virtualMachineProfile.licenseType), VMSSize=tostring(sku.name)\r\n ) on subscriptionId \r\n| order by type asc \r\n| project WindowsId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OSVersion,LicenseType, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "LoadVMSSTab", + "comparison": "isEqualTo", + "value": "Yes" + }, + { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "True" + } + ], + "name": "VMSSAHBEnabled-RG" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualMachineScaleSets'\r\n| where tostring(properties.storageProfile.osDisk.osType) == 'Windows' or tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType) == 'Windows'\r\n| where tostring(properties.['licenseType']) !has 'Windows' and tostring(properties.virtualMachineProfile.['licenseType']) !has 'Windows'\r\n| extend WindowsId=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType), OsVersion = tostring(properties.virtualMachineProfile.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.virtualMachineProfile.licenseType), VMSSize=tostring(sku.name)\r\n ) on subscriptionId \r\n| order by type asc \r\n| project WindowsId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OsVersion,LicenseType, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId\r\n", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "LoadVMSSTab", + "comparison": "isEqualTo", + "value": "Yes" + }, + { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "True" + } + ], + "name": "VMSSAHBDisabled-RG" + }, + { + "type": 1, + "content": { + "json": "### Consumed Licenses\r\nTotal number of Windows licenses cores consumed by all Windows virtual machines.\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Windows virtual machine" + }, + { + "type": 1, + "content": { + "json": "### Number of required Cores to enable Windows Azure Hybrid Benefit\r\nNumber of cores required to enable AHB across the entire environment.", + "style": "info" + }, + "customWidth": "50", + "name": "Number of required Cores to AHB" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "loadType": "explicit", + "loadButtonText": "Load VMSS Info", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"50d79765-aad4-437e-a90b-8cc7865e7081\",\"mergeType\":\"inner\",\"leftTable\":\"VMSSAHBEnabled-RG\",\"rightTable\":\"API-Get_VM_SKU\",\"leftColumn\":\"VMSSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\">=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores per VM\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"8\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\" ([\\\"vCPUs\\\"] + 7) & ~7\"}}]},{\"originalName\":\"[query - 0].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].WindowsId\",\"mergedName\":\"WindowsId\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMSSize\",\"mergedName\":\"VMSSize\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].OSVersion\",\"mergedName\":\"OSVersion\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[API-Get_VM_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"unknown\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"unknown\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"unknown\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"unknown\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"unknown\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"},{\"originalName\":\"[WindowsAHBEnabled].WindowsId\"},{\"originalName\":\"[WindowsAHBEnabled].VMRG\"},{\"originalName\":\"[WindowsAHBEnabled].VMLocation\"},{\"originalName\":\"[WindowsAHBEnabled].OSType\"},{\"originalName\":\"[WindowsAHBEnabled].OsVersion\"},{\"originalName\":\"[WindowsAHBEnabled].LicenseType\"},{\"originalName\":\"[query - Get VM vCPU].Name\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\"},{\"originalName\":\"[WindowsAHBEnabled].VMSSize\"}]}", + "size": 0, + "title": "Cores not enabled per AHB Priority", + "noDataMessage": "All of your VMs have AHB enabled", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Sev4", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores per VM" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Cores NOT enabled per AHB Priority" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"50d79765-aad4-437e-a90b-8cc7865e7081\",\"mergeType\":\"inner\",\"leftTable\":\"VMSSAHBEnabled-RG\",\"rightTable\":\"API-Get_VM_SKU\",\"leftColumn\":\"VMSSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\">=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores per VM\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"8\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\" ([\\\"vCPUs\\\"] + 7) & ~7\"}}]},{\"originalName\":\"[WindowsAHBEnabled].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].WindowsId\",\"mergedName\":\"WindowsId\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMSSize\",\"mergedName\":\"VMSSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].OSVersion\",\"mergedName\":\"OSVersion\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"},{\"originalName\":\"[WindowsAHBEnabled].WindowsId\"},{\"originalName\":\"[WindowsAHBEnabled].VMRG\"},{\"originalName\":\"[WindowsAHBEnabled].VMLocation\"},{\"originalName\":\"[WindowsAHBEnabled].OSType\"},{\"originalName\":\"[WindowsAHBEnabled].OsVersion\"},{\"originalName\":\"[WindowsAHBEnabled].LicenseType\"},{\"originalName\":\"[query - Get VM vCPU].Name\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\"},{\"originalName\":\"[WindowsAHBEnabled].VMSSize\"}]}", + "size": 0, + "title": "Consumed Cores per AHB Priority", + "noDataMessage": "None of your VMs have AHB enabled", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Sev4", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + } + } + ] + } + }, + "customWidth": "33", + "name": "Consumed Cores per AHB Priority" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d8deb22b-a596-43ee-acc4-180849d26130\",\"mergeType\":\"inner\",\"leftTable\":\"VMSSAHBEnabled-RG\",\"rightTable\":\"API-Get_VM_SKU\",\"leftColumn\":\"VMSSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"ConsumedCores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"8\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"([\\\"vCPUs\\\"] + 7) & ~7\"}}]},{\"originalName\":\"[WindowsAHBEnabled].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[VMSSAHBEnabled-RG].WindowsId\",\"mergedName\":\"WindowsId\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMSSize\",\"mergedName\":\"VMSSize\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].OSVersion\",\"mergedName\":\"OSVersion\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VM_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"d8deb22b-a596-43ee-acc4-180849d26130\"},{\"originalName\":\"[VMSSAHBEnabled-RG].WindowsId1\",\"mergedName\":\"WindowsId1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "Consumed Cores per VMSS", + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "ConsumedCores", + "formatter": 0, + "formatOptions": { + "aggregation": "Sum" + } + } + ] + }, + "tileSettings": { + "titleContent": {}, + "leftContent": { + "columnMatch": "ConsumedCores", + "formatter": 12, + "formatOptions": { + "palette": "blue" + } + }, + "showBorder": false + }, + "graphSettings": { + "type": 0 + }, + "chartSettings": { + "yAxis": [ + "ConsumedCores" + ], + "group": "VMName", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Consumed Cores per VMSS" + } + ] + }, + "name": "VMSS RG Overview" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", + "version": "KqlParameterItem/1.0", + "name": "VMSSAHBEnabled", + "label": "See VMSS with AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 + } + }, + { + "id": "f1ac5e53-253c-4afb-8bc5-b1ba2efea3eb", + "version": "KqlParameterItem/1.0", + "name": "VMSSAHBDisabled", + "label": "See VMSS without AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "VMSS Without AHB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"50d79765-aad4-437e-a90b-8cc7865e7081\",\"mergeType\":\"inner\",\"leftTable\":\"VMSSAHBEnabled-RG\",\"rightTable\":\"API-Get_VM_SKU\",\"leftColumn\":\"VMSSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[WindowsAHBEnabled].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\">=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"static\",\"resultVal\":\"High Priority\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"static\",\"resultVal\":\"Low Priority\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores per VM\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\"<=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"expression\",\"resultVal\":\"8\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"([\\\"vCPUs\\\"] + 7) & ~7\"}}]},{\"originalName\":\"[VMSSAHBEnabled-RG].WindowsId\",\"mergedName\":\"WindowsId\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMSSize\",\"mergedName\":\"VMSSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].OSVersion\",\"mergedName\":\"OSVersion\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBEnabled-RG].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - 0].VMName\"},{\"originalName\":\"[query - 0].VMSSize\"},{\"originalName\":\"[query - Get VM vCPU].Name\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\"},{\"originalName\":\"[WindowsAHBEnabled].VMSSize\"},{\"originalName\":\"[WindowsAHBEnabled].VMName\"},{\"originalName\":\"[VMSSAHBEnabled].VMSize\"},{\"originalName\":\"[VMSSAHBEnabled].VMName\"},{\"originalName\":\"[VMSSAHBEnabled-Tag].VMName\"},{\"originalName\":\"[VMSSAHBEnabled-Tag].VMSize\"}]}", + "size": 0, + "title": "VMSS with AHB", + "noDataMessage": "None of your VMSS have AHB enabled", + "noDataMessageStyle": 4, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "2", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Subscription Name", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true + } + } + }, + "conditionalVisibility": { + "parameterName": "VMSSAHBEnabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "VMSS+SKU+vCores-AHB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"50d79765-aad4-437e-a90b-8cc7865e7081\",\"mergeType\":\"inner\",\"leftTable\":\"VMSSAHBDisabled-RG\",\"rightTable\":\"API-Get_VM_SKU\",\"leftColumn\":\"VMSSize\",\"rightColumn\":\"Name\"}],\"projectRename\":[{\"originalName\":\"[query - 0].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - 0].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCPUs\",\"operator\":\">=\",\"rightValType\":\"static\",\"rightVal\":\"8\",\"resultValType\":\"static\",\"resultVal\":\"High Priority\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"static\",\"resultVal\":\"Low Priority\"}}]},{\"originalName\":\"[VMSSAHBDisabled-RG].WindowsId\",\"mergedName\":\"WindowsId\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBDisabled-RG].VMName\",\"mergedName\":\"VMName\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBDisabled-RG].VMRG\",\"mergedName\":\"VMRG\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBDisabled-RG].VMSize\",\"mergedName\":\"VMSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBDisabled-RG].VMSSize\",\"mergedName\":\"VMSSize\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBDisabled-RG].VMLocation\",\"mergedName\":\"VMLocation\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBDisabled-RG].OSType\",\"mergedName\":\"OSType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBDisabled-RG].OsVersion\",\"mergedName\":\"OsVersion\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBDisabled-RG].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[VMSSAHBDisabled-RG].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].Name\",\"mergedName\":\"Name\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUs\",\"mergedName\":\"vCPUs\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].MemoryGB\",\"mergedName\":\"MemoryGB\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].MaxNetworkInterfaces\",\"mergedName\":\"MaxNetworkInterfaces\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].HyperVGenerations\",\"mergedName\":\"HyperVGenerations\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[API-Get_VM_SKU].vCPUsPerCore\",\"mergedName\":\"vCPUsPerCore\",\"fromId\":\"50d79765-aad4-437e-a90b-8cc7865e7081\"},{\"originalName\":\"[query - 0].VMName\"},{\"originalName\":\"[query - 0].VMSSize\"},{\"originalName\":\"[query - Get VM vCPU].Name\"},{\"originalName\":\"[query - Get VM vCPU].MemoryGB\"},{\"originalName\":\"[query - Get VM vCPU].MaxNetworkInterfaces\"},{\"originalName\":\"[query - Get VM vCPU].HyperVGenerations\"},{\"originalName\":\"[query - Get VM vCPU].vCPUsPerCore\"},{\"originalName\":\"[VMSS-AHB-Disabled].VMName\"},{\"originalName\":\"[VMSS-AHB-Disabled-Tag].VMSize\"},{\"originalName\":\"[VMSS-AHB-Disabled-Tag].VMName\"}]}", + "size": 0, + "title": "VMSS without AHB", + "noDataMessage": "All of your VMSS have AHB enabled", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Sev4", + "text": "{0}{1}" + } + ] + }, + "numberFormat": { + "unit": 17, + "options": { + "style": "decimal" + } + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "subscriptionId" + ], + "expandTopLevel": true + } + } + }, + "conditionalVisibility": { + "parameterName": "VMSSAHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "VMSS+SKU+vCores" + } + ] + }, + "name": "VMSS RG Details" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "VMSS" + }, + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "VM" + } + ], + "name": "VMSS-RG" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "selectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RateOptimization" + } + ], + "name": "AHB Overview" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "RateOptimization" + }, + "name": "group - RateOptimization group" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "6b8c0a46-6867-498b-9a3e-799a2475a11a", + "cellValue": "selectedOverviewTab", + "linkTarget": "parameter", + "linkLabel": "Welcome", + "subTarget": "instructions", + "style": "link" + }, + { + "id": "da748ed1-f329-42d4-962d-9b2339baf7c4", + "cellValue": "selectedOverviewTab", + "linkTarget": "parameter", + "linkLabel": "Resources overview", + "subTarget": "resourcesMap", + "style": "link" + }, + { + "id": "a4b4de18-b90e-4212-86a2-ea5fabc4f40c", + "cellValue": "selectedOverviewTab", + "linkTarget": "parameter", + "linkLabel": "Security recommendations", + "subTarget": "securityRecommendations", + "style": "link" + }, + { + "id": "a18f24d2-3320-4c53-a86d-db32c920c8f7", + "cellValue": "selectedOverviewTab", + "linkTarget": "parameter", + "linkLabel": "Reliability recommendations", + "subTarget": "reliabilityRecommendations", + "style": "link" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "Welcome" + }, + "name": "tabs - overview tabs" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "6a9ccf8c-9f3e-4ee0-b45b-f511401f8656", + "version": "KqlParameterItem/1.0", + "name": "mapSubscriptions", + "label": "Subscriptions", + "type": 6, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::all", + "value": [ + "value::all" + ] + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "Welcome" + }, + { + "parameterName": "selectedOverviewTab", + "comparison": "isNotEqualTo", + "value": "instructions" + } + ], + "name": "parameters - OverviewSubscriptions" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type == \"microsoft.advisor/recommendations\"\r\n| where tostring (properties.category) has \"Security\"\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=tostring(properties.impact),Recommendation=tostring(properties.shortDescription.problem),subscriptionId", + "size": 0, + "title": "Azure Advisor security recommendations", + "noDataMessage": "You are following all of our security recommendations for the selected subscriptions.", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{mapSubscriptions}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Impact", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High", + "representation": "red", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Medium", + "representation": "orange", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low", + "representation": "blue", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "gray", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Impact" + ] + } + } + }, + "name": "query - advisorSecurityRecommendations" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "Welcome" + }, + { + "parameterName": "selectedOverviewTab", + "comparison": "isEqualTo", + "value": "securityRecommendations" + } + ], + "name": "group - securityRecommendations" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Welcome to the cost optimization workbook" + }, + "name": "Welcome" + }, + { + "type": 1, + "content": { + "json": "### Reference: [Microsoft Azure Well-Architected Framework - cost optimization pillar](https://learn.microsoft.com/azure/architecture/framework/cost/overview)", + "style": "upsell" + }, + "name": "Reference" + }, + { + "type": 1, + "content": { + "json": "This workbook aims to offer a comprehensive overview of your Azure environment's resource usage, aligning with the WAF Cost Optimization pillar. It identifies recommendations to optimize efficiency, providing guidance on potential opportunities. Please note that the workbook serves as guidance to highlight optimization opportunities, and the extent of cost reduction depends on their implementation.\r\n\r\n## Overview of the cost optimization pillar\r\n\r\n* The cost optimization pillar provides principles for balancing business goals with technology needs to create a cost-effective workload while avoiding capital-intensive solutions.The workbook emphasizes the importance of reducing waste and improving operational efficiencies.\r\n\r\n* To assess your workload based on the principles outlined in the [Microsoft Azure Well-Architected Framework](https://learn.microsoft.com/azure/architecture/framework/), reference the [Microsoft Azure Well-Architected Review](https://learn.microsoft.com/assessments/?id=azure-architecture-review&mode=pre-assessment&session=20dc50e4-5b71-4f38-bc49-51cc1d9f205c) tool.\r\n\r\n\r\n\r\n\r\n" + }, + "name": "objective" + }, + { + "type": 1, + "content": { + "json": "Indicates an implemented recommendation that can result in a environment that is following the Cost Optimization & Cost Governance principles.", + "style": "success" + }, + "customWidth": "50", + "name": "Greenlight", + "styleSettings": { + "margin": "10px", + "showBorder": true + } + }, + { + "type": 1, + "content": { + "json": "## Prerequisites\r\n\r\nThis workbook requires the following least-privileged (minimum) roles on your Subscriptions:\r\n\r\n * **Reader** : allows you to import the workbook without saving it and view all of the workbook tabs.\r\n * **Workbook Contributor** : allows you to import and save the workbook\r\n\r\nThis workbook includes \"Quick Fix\" actions within certain queries. The permissions necessary to execute these actions may vary and are documented for each specific action.\r\n\r\n\r\n" + }, + "name": "Prerequisites" + }, + { + "type": 1, + "content": { + "json": "## Feedback\r\n\r\n [ Submit feedback here ](https://aka.ms/advisor_cost_wb_feedback) on your experience with workbooks at any time.\r\n\r\n\r\n\r\n [Submit any issues ](https://aka.ms/costworkbookfeedback) with the workbook template to GitHub." + }, + "name": "text - 5" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "Welcome" + }, + { + "parameterName": "selectedOverviewTab", + "comparison": "isEqualTo", + "value": "instructions" + } + ], + "name": "Welcome" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "summarize count() by location", + "size": 2, + "title": "Resource distribution per region", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{mapSubscriptions}" + ], + "visualization": "map", + "mapSettings": { + "locInfo": "AzureLoc", + "locInfoColumn": "location", + "sizeSettings": "count_", + "sizeAggregation": "Sum", + "labelSettings": "location", + "legendMetric": "count_", + "legendAggregation": "Sum", + "itemColorSettings": { + "nodeColorField": "count_", + "colorAggregation": "Sum", + "type": "heatmap", + "heatmapPalette": "greenRed" + } + } + }, + "name": "query - resourcesMap" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "selectedOverviewTab", + "comparison": "isEqualTo", + "value": "resourcesMap" + }, + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "Welcome" + } + ], + "name": "group - resourceOverview" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type == \"microsoft.advisor/recommendations\"\r\n| where tostring (properties.category) has \"HighAvailability\"\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=tostring(properties.impact),Recommendation=tostring(properties.shortDescription.problem),subscriptionId", + "size": 0, + "title": "Azure Advisor reliability recommendations", + "noDataMessage": "You are following all of our reliability recommendations for the selected subscriptions.", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{mapSubscriptions}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Impact", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High", + "representation": "red", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Medium", + "representation": "orange", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low", + "representation": "blue", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "gray", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Impact" + ] + } + } + }, + "name": "query - advisorReliabilityRecommendations" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "Welcome" + }, + { + "parameterName": "selectedOverviewTab", + "comparison": "isEqualTo", + "value": "reliabilityRecommendations" + } + ], + "name": "group - reliabilityRecommendations" + } + ], + "fallbackResourceIds": [ + "Azure Monitor" + ], + "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" + }, + "version": "", + "workbookJson": "[string(variables('$fxv#0'))]", + "workbookId": "0b2", + "telemetryId": "[format('00f120b5-2007-6120-0000-{0}30126b006', variables('workbookId'))]", + "finOpsToolkitVersion": "0.4", + "resourceTags": "[union(parameters('tags'), createObject('ftk-version', variables('finOpsToolkitVersion'), 'ftk-tool', format('{0} workbook', parameters('displayName'))))]" + }, + "resources": [ + { + "condition": "[parameters('enableDefaultTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('pid-{0}-{1}', variables('telemetryId'), uniqueString(deployment().name, parameters('location')))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "FinOps toolkit", + "version": "[variables('finOpsToolkitVersion')]" + } + }, + "resources": [] + } + } + }, + { + "type": "Microsoft.Insights/workbooks", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))]", + "location": "[parameters('location')]", + "tags": "[variables('resourceTags')]", + "kind": "shared", + "properties": { + "category": "workbook", + "description": "[parameters('description')]", + "displayName": "[parameters('displayName')]", + "serializedData": "[variables('workbookJson')]", + "sourceId": "Azure Monitor", + "version": "[variables('version')]" + } + } + ], + "outputs": { + "workbookId": { + "type": "string", + "metadata": { + "description": "The resource ID of the workbook." + }, + "value": "[resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName')))]" + }, + "workbookUrl": { + "type": "string", + "metadata": { + "description": "Link to the workbook in the Azure portal." + }, + "value": "[format('{0}/#view/AppInsightsExtension/UsageNotebookBlade/ComponentId/Azure%20Monitor/ConfigurationId/{1}/Type/{2}/WorkbookTemplateName/{3}', environment().portal, uriComponent(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName')))), reference(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))), '2022-04-01').category, uriComponent(reference(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))), '2022-04-01').displayName))]" + } + } +} \ No newline at end of file diff --git a/docs/deploy/optimization-workbook-0.4.ui.json b/docs/deploy/optimization-workbook-0.4.ui.json new file mode 100644 index 000000000..63512825c --- /dev/null +++ b/docs/deploy/optimization-workbook-0.4.ui.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "basics": { + "description": "The Cost optimization workbook provides an overview of the cost posture of your Azure environment. [Learn more](https://aka.ms/finops/toolkit)", + "location": { + "label": "Location", + "resourceTypes": ["Microsoft.Insights/workbooks"] + } + } + }, + "resourceTypes": ["Microsoft.Insights/workbooks"], + "basics": [ + { + "name": "displayName", + "type": "Microsoft.Common.TextBox", + "label": "Name", + "defaultValue": "Cost optimization", + "toolTip": "Name of the workbook.", + "constraints": { + "required": true, + "regex": "^.{1,250}$", + "validationMessage": "Name cannot be longer than 250 characters." + }, + "visible": true + } + ], + "steps": [ + { + "name": "tags", + "label": "Tags", + "elements": [ + { + "name": "tagsByResource", + "label": "Tags", + "toolTip": "Tags to apply.", + "type": "Microsoft.Common.TagsByResource", + "resources": ["Microsoft.Insights/workbooks"] + } + ] + } + ], + "outputs": { + "displayName": "[basics('displayName')]", + "location": "[location()]", + "tags": "[steps('tags').tagsByResource]" + } + } +} diff --git a/docs/deploy/optimization-workbook-latest.json b/docs/deploy/optimization-workbook-latest.json index 5694efd5d..fdbd337d3 100644 --- a/docs/deploy/optimization-workbook-latest.json +++ b/docs/deploy/optimization-workbook-latest.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.24.24.22086", - "templateHash": "15079220710436654877" + "templateHash": "18432665164063758796" } }, "parameters": { @@ -146,7 +146,9 @@ "quote": "'", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "includeAll": false, "showDefault": false }, @@ -155,7 +157,9 @@ }, "defaultValue": "value::all", "label": " Subscription", - "value": ["value::all"] + "value": [ + "value::all" + ] }, { "id": "f342a111-002a-47fd-807f-0d4ccac0618a", @@ -168,9 +172,13 @@ "quote": "'", "delimiter": ",", "query": "resources\r\n| distinct resourceGroup", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "defaultValue": "value::all", @@ -184,7 +192,9 @@ "type": 1, "isRequired": true, "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "isHiddenWhenLocked": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", @@ -196,7 +206,9 @@ "name": "TagName", "type": 2, "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -211,7 +223,9 @@ "name": "TagValue", "type": 2, "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -244,7 +258,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "37ceb1c3-3930-4689-a90b-22f26e42bd81", @@ -256,7 +272,9 @@ "quote": "'", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "includeAll": false, "showDefault": false }, @@ -277,9 +295,13 @@ "quote": "'", "delimiter": ",", "query": "resources\r\n| distinct resourceGroup", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "defaultValue": "value::all", @@ -293,7 +315,9 @@ "type": 1, "isRequired": true, "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "isHiddenWhenLocked": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", @@ -305,7 +329,9 @@ "name": "TagName", "type": 2, "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -320,7 +346,9 @@ "name": "TagValue", "type": 2, "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -353,7 +381,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "eae8a0d2-14e6-4cd1-a2d2-fd6b207cf517", @@ -361,9 +391,13 @@ "name": "Location", "type": 2, "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::1"] + "additionalResourceOptions": [ + "value::1" + ] }, "timeContext": { "durationMs": 86400000 @@ -386,49 +420,6 @@ }, "name": "parameters - location" }, - { - "type": 11, - "content": { - "version": "LinkItem/1.0", - "style": "tabs", - "links": [ - { - "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Azure App Service", - "subTarget": "webapp", - "style": "link" - }, - { - "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Azure Kubernetes Service", - "subTarget": "AKS", - "style": "link" - }, - { - "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Azure Synapse", - "subTarget": "Synapse", - "preText": "VM", - "style": "link" - }, - { - "id": "820d600c-8ab3-4622-ba5a-52f60574d111", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Monitoring", - "subTarget": "Monitoring", - "style": "link" - } - ] - }, - "name": "links - Storage" - }, { "type": 12, "content": { @@ -436,1010 +427,1231 @@ "groupType": "editable", "items": [ { - "type": 1, - "content": { - "json": "# Synapse\r\nA Synapse Workspace is considered unused if it doesn't have any SQL pools attached to it\r\n", - "style": "upsell" - }, - "name": "Synapse" - }, - { - "type": 3, + "type": 9, "content": { - "version": "KqlItem/1.0", - "query": "Resources\r\n| where type =~ 'Microsoft.Synapse/workspaces'\r\n| join kind=leftouter (\r\n Resources\r\n | where type =~ 'Microsoft.Synapse/workspaces/sqlPools'\r\n | extend SynapseWorkspaceResourceId = substring(id, 0, indexof(id, '/sqlPools/'))\r\n | summarize sqlpoolCount = count() by SynapseWorkspaceResourceId\r\n) on $left.id == $right.SynapseWorkspaceResourceId\r\n| join kind=leftouter (\r\n Resources\r\n | where type =~ 'Microsoft.Synapse/workspaces/bigDataPools'\r\n | extend SynapseWorkspaceResourceId = substring(id, 0, indexof(id, '/bigDataPools/'))\r\n | summarize bigdatapoolCount = count() by SynapseWorkspaceResourceId\r\n) on $left.id == $right.SynapseWorkspaceResourceId\r\n| where (isnull(sqlpoolCount) or sqlpoolCount == 0) and (isnull(bigdatapoolCount) or bigdatapoolCount == 0)\r\n| project id, resourceGroup, subscriptionId, location\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", - "size": 0, - "title": "Unused Synapase workspace", - "noDataMessage": "All of your Synapse workspaces have SQL pools.", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "id1", - "formatter": 5 - }, - { - "columnMatch": "storageaccount", - "formatter": 13, - "formatOptions": { - "linkTarget": "Resource", - "subTarget": "insights", - "linkIsContextBlade": true, - "showIcon": true - } - } - ], - "labelSettings": [ - { - "columnId": "id", - "label": "Resource ID" + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "eae8a0d2-14e6-4cd1-a2d2-fd6b207cf517", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] }, - { - "columnId": "resourceGroup", - "label": "Resource Group" + "timeContext": { + "durationMs": 86400000 }, - { - "columnId": "subscriptionId", - "label": "Subscription Name" - } - ] - } + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" }, - "name": "Get-Synapse1" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "Synapse" - }, - "name": "SynapseGroup" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, { - "type": 1, + "type": 11, "content": { - "json": "# Azure Kubernetes Service\r\n- Enable cluster autoscaler to automatically adjust the number of agent nodes in response to resource constraints\r\n\r\n- Consider using Azure Spot VMs for workloads that can handle interruptions, early terminations, or evictions. For example, workloads such as batch processing jobs, development and testing environments, and large compute workloads may be good candidates to be scheduled on a spot node pool.\r\n\r\n- Utilize the Horizontal pod autoscaler to adjust the number of pods in a deployment depending on CPU utilization or other select metrics.\r\n\r\n- Use the Start/Stop feature in Azure Kubernetes Services (AKS).\r\n\r\n", - "style": "upsell" + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Azure App Service", + "subTarget": "webapp", + "style": "link" + }, + { + "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Azure Kubernetes Service", + "subTarget": "AKS", + "style": "link" + }, + { + "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Azure Synapse", + "subTarget": "Synapse", + "preText": "VM", + "style": "link" + }, + { + "id": "820d600c-8ab3-4622-ba5a-52f60574d111", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Monitoring", + "subTarget": "Monitoring", + "style": "link" + } + ] }, - "name": "Azure Kubernetes Service" + "name": "links - Storage" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "\tresources\r\n | where resourceGroup in ({ResourceGroup})\r\n\t| where type == \"microsoft.containerservice/managedclusters\"\r\n\t| extend AKSname=name,location=location,Sku=tostring(sku.name),Tier=tostring(sku.tier),AgentPoolProfiles=properties.agentPoolProfiles\r\n | project id,AKSname,resourceGroup,subscriptionId,Sku,Tier,AgentPoolProfiles,location\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id\r\n\t| mvexpand AgentPoolProfiles\r\n\t| extend ProfileName = tostring(AgentPoolProfiles.name) ,mode=AgentPoolProfiles.mode,AutoScaleEnabled = AgentPoolProfiles.enableAutoScaling ,SpotVM=AgentPoolProfiles.scaleSetPriority, VMSize=tostring(AgentPoolProfiles.vmSize),minCount=tostring(AgentPoolProfiles.minCount),maxCount=tostring(AgentPoolProfiles.maxCount) , nodeCount=tostring(AgentPoolProfiles.['count'])\r\n | project id,ProfileName,Sku,Tier,mode,AutoScaleEnabled,SpotVM, VMSize,nodeCount,minCount,maxCount,location,resourceGroup,subscriptionId,AKSname\r\n \r\n", - "size": 0, - "noDataMessage": "You have no AKS clusters!", - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "AKS Name", - "formatter": 1 - }, - { - "columnMatch": "id", - "formatter": 13, - "formatOptions": { - "linkTarget": "Resource", - "subTarget": "Insights", - "showIcon": true - } + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Synapse\r\nA Synapse Workspace is considered unused if it doesn't have any SQL pools attached to it\r\n", + "style": "upsell" }, - { - "columnMatch": "mode", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "name": "Synapse" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| where type =~ 'Microsoft.Synapse/workspaces'\r\n| join kind=leftouter (\r\n Resources\r\n | where type =~ 'Microsoft.Synapse/workspaces/sqlPools'\r\n | extend SynapseWorkspaceResourceId = substring(id, 0, indexof(id, '/sqlPools/'))\r\n | summarize sqlpoolCount = count() by SynapseWorkspaceResourceId\r\n) on $left.id == $right.SynapseWorkspaceResourceId\r\n| join kind=leftouter (\r\n Resources\r\n | where type =~ 'Microsoft.Synapse/workspaces/bigDataPools'\r\n | extend SynapseWorkspaceResourceId = substring(id, 0, indexof(id, '/bigDataPools/'))\r\n | summarize bigdatapoolCount = count() by SynapseWorkspaceResourceId\r\n) on $left.id == $right.SynapseWorkspaceResourceId\r\n| where (isnull(sqlpoolCount) or sqlpoolCount == 0) and (isnull(bigdatapoolCount) or bigdatapoolCount == 0)\r\n| project id, resourceGroup, subscriptionId, location\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Unused Synapase workspace", + "noDataMessage": "All of your Synapse workspaces have SQL pools.", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ { - "operator": "==", - "thresholdValue": "System", - "representation": "Gear", - "text": "{0}{1}" + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } }, { - "operator": "==", - "thresholdValue": "User", - "representation": "Person", - "text": "{0}{1}" + "columnMatch": "id1", + "formatter": 5 }, { - "operator": "Default", - "thresholdValue": null, - "representation": "success", - "text": "{0}{1}" + "columnMatch": "storageaccount", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "insights", + "linkIsContextBlade": true, + "showIcon": true + } } - ] - } - }, - { - "columnMatch": "AutoScaleEnabled", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + ], + "labelSettings": [ { - "operator": "==", - "thresholdValue": "true", - "representation": "success", - "text": "Enabled" + "columnId": "id", + "label": "Resource ID" }, { - "operator": "Default", - "thresholdValue": null, - "representation": "disabled", - "text": "Disabled" - } - ] - } - }, - { - "columnMatch": "SpotVM", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ - { - "operator": "is Empty", - "representation": "2", - "text": "{0}{1}Not Spot VM" + "columnId": "resourceGroup", + "label": "Resource Group" }, { - "operator": "Default", - "thresholdValue": null, - "representation": "success", - "text": "{0}{1}" + "columnId": "subscriptionId", + "label": "Subscription Name" } ] } }, - { - "columnMatch": "Group", - "formatter": 1 - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } + "name": "Get-Synapse1" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "Synapse" + }, + "name": "SynapseGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Azure Kubernetes Service\r\n- Enable cluster autoscaler to automatically adjust the number of agent nodes in response to resource constraints\r\n\r\n- Consider using Azure Spot VMs for workloads that can handle interruptions, early terminations, or evictions. For example, workloads such as batch processing jobs, development and testing environments, and large compute workloads may be good candidates to be scheduled on a spot node pool.\r\n\r\n- Utilize the Horizontal pod autoscaler to adjust the number of pods in a deployment depending on CPU utilization or other select metrics.\r\n\r\n- Use the Start/Stop feature in Azure Kubernetes Services (AKS).\r\n\r\n", + "style": "upsell" }, - { - "columnMatch": "AKSname", - "formatter": 5 - } - ], - "filter": true, - "hierarchySettings": { - "treeType": 1, - "groupBy": ["AKSname"], - "expandTopLevel": true + "name": "Azure Kubernetes Service" }, - "labelSettings": [ - { - "columnId": "id", - "label": "ID" - }, - { - "columnId": "ProfileName", - "label": "Profile Name" - }, - { - "columnId": "Sku", - "label": "SKU" - }, - { - "columnId": "Tier", - "label": "SKU Tier" - }, - { - "columnId": "mode", - "label": "Mode" - }, - { - "columnId": "AutoScaleEnabled", - "label": "Autoscale enabled?" - }, - { - "columnId": "SpotVM", - "label": "Spot VM?" - }, - { - "columnId": "VMSize", - "label": "VM SKU" - }, - { - "columnId": "nodeCount", - "label": "Number of nodes" - }, - { - "columnId": "minCount", - "label": "Minimum nodes" - }, - { - "columnId": "maxCount", - "label": "Maximum nodes" - }, - { - "columnId": "location", - "label": "Location" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" - }, - { - "columnId": "subscriptionId", - "label": "Subscription Name" + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "\tresources\r\n | where resourceGroup in ({ResourceGroup})\r\n\t| where type == \"microsoft.containerservice/managedclusters\"\r\n\t| extend AKSname=name,location=location,Sku=tostring(sku.name),Tier=tostring(sku.tier),AgentPoolProfiles=properties.agentPoolProfiles\r\n | project id,AKSname,resourceGroup,subscriptionId,Sku,Tier,AgentPoolProfiles,location\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id\r\n\t| mvexpand AgentPoolProfiles\r\n\t| extend ProfileName = tostring(AgentPoolProfiles.name) ,mode=AgentPoolProfiles.mode,AutoScaleEnabled = AgentPoolProfiles.enableAutoScaling ,SpotVM=AgentPoolProfiles.scaleSetPriority, VMSize=tostring(AgentPoolProfiles.vmSize),minCount=tostring(AgentPoolProfiles.minCount),maxCount=tostring(AgentPoolProfiles.maxCount) , nodeCount=tostring(AgentPoolProfiles.['count'])\r\n | project id,ProfileName,Sku,Tier,mode,AutoScaleEnabled,SpotVM, VMSize,nodeCount,minCount,maxCount,location,resourceGroup,subscriptionId,AKSname\r\n \r\n", + "size": 0, + "noDataMessage": "You have no AKS clusters!", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "AKS Name", + "formatter": 1 + }, + { + "columnMatch": "id", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "Insights", + "showIcon": true + } + }, + { + "columnMatch": "mode", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "System", + "representation": "Gear", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "User", + "representation": "Person", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "AutoScaleEnabled", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "true", + "representation": "success", + "text": "Enabled" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "disabled", + "text": "Disabled" + } + ] + } + }, + { + "columnMatch": "SpotVM", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "is Empty", + "representation": "2", + "text": "{0}{1}Not Spot VM" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "AKSname", + "formatter": 5 + } + ], + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "AKSname" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "id", + "label": "ID" + }, + { + "columnId": "ProfileName", + "label": "Profile Name" + }, + { + "columnId": "Sku", + "label": "SKU" + }, + { + "columnId": "Tier", + "label": "SKU Tier" + }, + { + "columnId": "mode", + "label": "Mode" + }, + { + "columnId": "AutoScaleEnabled", + "label": "Autoscale enabled?" + }, + { + "columnId": "SpotVM", + "label": "Spot VM?" + }, + { + "columnId": "VMSize", + "label": "VM SKU" + }, + { + "columnId": "nodeCount", + "label": "Number of nodes" + }, + { + "columnId": "minCount", + "label": "Minimum nodes" + }, + { + "columnId": "maxCount", + "label": "Maximum nodes" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + }, + { + "columnId": "AKSname", + "label": "AKS Name" + } + ] + } }, - { - "columnId": "AKSname", - "label": "AKS Name" - } - ] - } - }, - "name": "Get-All-AKS" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "AKS" - }, - "name": "AKSGroup" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 1, - "content": { - "json": "# Azure App Service\r\n## Save with Premium v3 reserved instances\r\nWhen you commit to an Azure App Service Premium v3 reserved instance you can save money. The reservation discount is applied automatically to the number of running instances that match the reservation scope and attributes - you don't need to assign a reservation to a specific instance to get the discounts.\r\n\r\n## Determine the right reserved instance size before you buy\r\nBefore you buy a reservation, you should determine the size of the Premium v3 reserved instance that you need. The following sections will help you determine the right Premium v3 reserved instance size.\r\n\r\n## Use Autoscale appropriately\r\nAutoscale can be used to provision resources for when they're needed or on demand, which allows you to minimize costs when your environment is idle.\r\n", - "style": "upsell" - }, - "name": "Azure App Service" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'Microsoft.Web/sites'\r\n| extend WebAppRG=resourceGroup, WebAppName=name, AppServicePlan=tostring(properties.serverFarmId), SKU=tostring(properties.sku), Type=kind, Status=tostring(properties.state), WebAppLocation=location, SubscriptionName=subscriptionId\r\n| project id,WebAppName, Type, Status, WebAppLocation, AppServicePlan, WebAppRG,SubscriptionName\r\n| order by id asc\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", - "size": 0, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] - }, - "conditionalVisibility": { - "parameterName": "isVisible", - "comparison": "isEqualTo", - "value": "Never" - }, - "name": "query - WebFunctionStatus" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type == \"microsoft.web/serverfarms\"\r\n| extend planId=tolower(tostring(id)),skuname = tostring(sku.name) , skutier = tostring(sku.tier), workers=tostring(properties.numberOfWorkers),webRG=resourceGroup,maxworkers=tostring(properties.maximumNumberOfWorkers), SubscriptionName=subscriptionId\r\n| project planId, name, skuname, skutier, workers, maxworkers, webRG, SubscriptionName\r\n| join kind=leftouter (resources | where type ==\"microsoft.insights/autoscalesettings\" | project planId=tolower(tostring(properties.targetResourceUri)), PredictiveAutoscale=properties.predictiveAutoscalePolicy.scaleMode, AutoScaleProfiles=properties.profiles,resourceGroup) on planId", - "size": 0, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "name": "Get-All-AKS" + } + ] }, "conditionalVisibility": { - "parameterName": "isVisible", + "parameterName": "SelectedSubTab", "comparison": "isEqualTo", - "value": "Never" + "value": "AKS" }, - "name": "query - AppServiceplandetails" + "name": "AKSGroup" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\",\"mergeType\":\"inner\",\"leftTable\":\"query - AppServiceplandetails\",\"rightTable\":\"query - WebFunctionStatus\",\"leftColumn\":\"planId\",\"rightColumn\":\"AppServicePlan\"}],\"projectRename\":[{\"originalName\":\"[query - AppServiceplandetails].type\",\"mergedName\":\"type\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].tenantId\",\"mergedName\":\"tenantId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].kind\",\"mergedName\":\"kind\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].location\",\"mergedName\":\"location\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].managedBy\",\"mergedName\":\"managedBy\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].sku\",\"mergedName\":\"sku\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].plan\",\"mergedName\":\"plan\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].properties\",\"mergedName\":\"properties\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].tags\",\"mergedName\":\"tags\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].identity\",\"mergedName\":\"identity\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].zones\",\"mergedName\":\"zones\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].extendedLocation\",\"mergedName\":\"extendedLocation\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].planId\",\"mergedName\":\"planId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - WebFunctionStatus].id\",\"mergedName\":\"id\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].name\",\"mergedName\":\"name\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].Status\",\"mergedName\":\"Status\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].Type\",\"mergedName\":\"Type\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].skuname\",\"mergedName\":\"skuname\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].skutier\",\"mergedName\":\"skutier\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].PredictiveAutoscale\",\"mergedName\":\"PredictiveAutoscale\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].AutoScaleProfiles\",\"mergedName\":\"AutoScaleProfiles\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].workers\",\"mergedName\":\"workers\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].maxworkers\",\"mergedName\":\"maxworkers\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].webRG\",\"mergedName\":\"webRG\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].planId1\",\"mergedName\":\"planId1\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].resourceGroup\",\"mergedName\":\"resourceGroup\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].WebAppName\",\"mergedName\":\"WebAppName\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].WebAppLocation\",\"mergedName\":\"WebAppLocation\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].AppServicePlan\",\"mergedName\":\"AppServicePlan\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].WebAppRG\",\"mergedName\":\"WebAppRG\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].SubscriptionName\"},{\"originalName\":\"[query - WebFunctionStatus].id1\"},{\"originalName\":\"[query - AppServiceplandetails].resourceGroup1\"}]}", - "size": 0, - "title": "Web Apps", - "noDataMessage": "You have no WebApps!", - "showExportToExcel": true, - "queryType": 7, - "gridSettings": { - "formatters": [ - { - "columnMatch": "Name", - "formatter": 1 + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Azure App Service\r\n## Save with Premium v3 reserved instances\r\nWhen you commit to an Azure App Service Premium v3 reserved instance you can save money. The reservation discount is applied automatically to the number of running instances that match the reservation scope and attributes - you don't need to assign a reservation to a specific instance to get the discounts.\r\n\r\n## Determine the right reserved instance size before you buy\r\nBefore you buy a reservation, you should determine the size of the Premium v3 reserved instance that you need. The following sections will help you determine the right Premium v3 reserved instance size.\r\n\r\n## Use Autoscale appropriately\r\nAutoscale can be used to provision resources for when they're needed or on demand, which allows you to minimize costs when your environment is idle.\r\n", + "style": "upsell" }, - { - "columnMatch": "SubscriptionName", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } + "name": "Azure App Service" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'Microsoft.Web/sites'\r\n| extend WebAppRG=resourceGroup, WebAppName=name, AppServicePlan=tostring(properties.serverFarmId), SKU=tostring(properties.sku), Type=kind, Status=tostring(properties.state), WebAppLocation=location, SubscriptionName=subscriptionId\r\n| project id,WebAppName, Type, Status, WebAppLocation, AppServicePlan, WebAppRG,SubscriptionName\r\n| order by id asc\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] }, - { - "columnMatch": "name", - "formatter": 5 + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "Never" }, - { - "columnMatch": "Status", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ - { - "operator": "==", - "thresholdValue": "Running", - "representation": "success", - "text": "{0}{1}" - }, - { - "operator": "==", - "thresholdValue": "Stopped", - "representation": "disabled", - "text": "{0}{1}" - }, - { - "operator": "Default", - "thresholdValue": null, - "representation": "success", - "text": "{0}{1}" - } - ] - } - }, - { - "columnMatch": "webRG", - "formatter": 5 - }, - { - "columnMatch": "planId1", - "formatter": 5 - }, - { - "columnMatch": "resourceGroup", - "formatter": 5 - }, - { - "columnMatch": "WebAppName", - "formatter": 5 - }, - { - "columnMatch": "AppServicePlan", - "formatter": 5 - }, - { - "columnMatch": "WebAppRG", - "formatter": 14, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "Group", - "formatter": 1 - } - ], - "rowLimit": 1000, - "filter": true, - "hierarchySettings": { - "treeType": 1, - "groupBy": ["name"], - "expandTopLevel": true + "name": "query - WebFunctionStatus" }, - "labelSettings": [ - { - "columnId": "SubscriptionName", - "label": "Subscription Name" - }, - { - "columnId": "planId", - "label": "Plan ID" - }, - { - "columnId": "id", - "label": "ID" - }, - { - "columnId": "name", - "label": "Name" - }, - { - "columnId": "skuname", - "label": "SKU" - }, - { - "columnId": "skutier", - "label": "SKU Tier" - }, - { - "columnId": "PredictiveAutoscale", - "label": "Autoscale Enabled?" - }, - { - "columnId": "AutoScaleProfiles", - "label": "Autoscale Profile" - }, - { - "columnId": "workers", - "label": "Workers" - }, - { - "columnId": "maxworkers", - "label": "Max. Workers" - }, - { - "columnId": "webRG", - "label": "Application Resource Group" - }, - { - "columnId": "WebAppName", - "label": "Application Name" - }, - { - "columnId": "WebAppLocation", - "label": "Application Location" - }, - { - "columnId": "AppServicePlan", - "label": "App Service Plan" - }, - { - "columnId": "WebAppRG", - "label": "Application Resource Group" - } - ] - } - }, - "name": "Get-Idle-WebApp" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "webapp" - }, - "name": "WebAppGroup" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 1, - "content": { - "json": "## Log Analytics workspace\r\nA [Log Analytics workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-workspace-overview) is a unique environment for log data from Azure Monitor and other Azure services, such as Microsoft Sentinel and Microsoft Defender for Cloud. Each workspace has its own data repository and configuration but might combine data from multiple services. The following advices could be of help in cost optimization:\r\n\r\n1. Adopt [commitment tiers](https://learn.microsoft.com/azure/azure-monitor/logs/cost-logs#commitment-tiers) where applicable.\r\n2. Adopt [Azure Monitor Logs dedicated cluster](https://learn.microsoft.com/azure/azure-monitor/logs/cost-logs#dedicated-clusters) if a single workspace does not ingest enough data as per the minimum commitment tier (100 GB/day) or if it is possible to aggregate ingestion costs from more than one workspace in the same region.\r\n3. Convert the free tier based workspace to **Pay-as-you-go** model and add them to an Azure Monitor Logs dedicated cluster where possible.", - "style": "upsell" - }, - "name": "MonitoringRecommendations" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "resources\r\n| where type =~ 'microsoft.operationalinsights/workspaces'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend \r\n state = trim(' ', tostring(properties.provisioningState)),\r\n sku = trim(' ', tostring(properties.sku.name)),\r\n skuUpdate = trim(' ', tostring(properties.sku.lastSkuUpdate)),\r\n retentionDays = toint(properties.retentionInDays),\r\n dailyquotaGB = trim(' ', tostring(properties.workspaceCapping.dailyQuotaGb))\r\n| extend dailyquotaGB = iif(dailyquotaGB !=-1.0, dailyquotaGB,\"--\")\r\n| project id, resourceGroup, location, retentionDays, dailyquotaGB, sku, subscriptionId\r\n| join kind = inner (\r\n resources\r\n | where type =~ 'microsoft.operationalinsights/workspaces'\r\n | where resourceGroup in ({ResourceGroup})\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags[tagName])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | summarize arg_max(tagName, tagValue) by id\r\n) on id\r\n| extend resourceGroup = tostring(split(id,'/providers/')[0])\r\n| project-away id1", - "size": 0, - "title": "Log Analytics Workspaces", - "showRefreshButton": true, - "exportMultipleValues": true, - "exportedParameters": [ { - "fieldName": "id", - "parameterName": "selectedWorkspaceId", - "parameterType": 1 - } - ], - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "id", - "formatter": 13, - "formatOptions": { - "linkTarget": "Resource", - "subTarget": "insights", - "linkIsContextBlade": true, - "showIcon": true - } - }, - { - "columnMatch": "resourceGroup", - "formatter": 14, - "formatOptions": { - "linkTarget": "Resource", - "linkIsContextBlade": true, - "showIcon": true - } - }, - { - "columnMatch": "retentionDays", - "formatter": 4, - "formatOptions": { - "min": 1, - "max": 730, - "palette": "blue", - "customColumnWidthSetting": "10%" - } + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type == \"microsoft.web/serverfarms\" and sku.tier !~ 'Free'\r\n| extend planId=tolower(tostring(id)),skuname = tostring(sku.name) , skutier = tostring(sku.tier), workers=tostring(properties.numberOfWorkers),webRG=resourceGroup,maxworkers=tostring(properties.maximumNumberOfWorkers), Sites=tostring(properties.numberOfSites), SubscriptionName=subscriptionId\r\n| project planId, name, skuname, skutier, workers, maxworkers, webRG, Sites, SubscriptionName\r\n| join kind=leftouter (resources | where type ==\"microsoft.insights/autoscalesettings\" | project planId=tolower(tostring(properties.targetResourceUri)), PredictiveAutoscale=properties.predictiveAutoscalePolicy.scaleMode, AutoScaleProfiles=properties.profiles,resourceGroup) on planId\r\n", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] }, - { - "columnMatch": "dailyquotaGB", - "formatter": 0, - "formatOptions": { - "customColumnWidthSetting": "10%" - } + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "Never" }, - { - "columnMatch": "sku", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "colors", - "thresholdsGrid": [ + "name": "query - AppServiceplandetails" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\",\"mergeType\":\"inner\",\"leftTable\":\"query - AppServiceplandetails\",\"rightTable\":\"query - WebFunctionStatus\",\"leftColumn\":\"planId\",\"rightColumn\":\"AppServicePlan\"}],\"projectRename\":[{\"originalName\":\"[query - AppServiceplandetails].type\",\"mergedName\":\"type\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].tenantId\",\"mergedName\":\"tenantId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].kind\",\"mergedName\":\"kind\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].location\",\"mergedName\":\"location\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].managedBy\",\"mergedName\":\"managedBy\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].sku\",\"mergedName\":\"sku\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].plan\",\"mergedName\":\"plan\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].properties\",\"mergedName\":\"properties\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].tags\",\"mergedName\":\"tags\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].identity\",\"mergedName\":\"identity\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].zones\",\"mergedName\":\"zones\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].extendedLocation\",\"mergedName\":\"extendedLocation\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].planId\",\"mergedName\":\"planId\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - WebFunctionStatus].id\",\"mergedName\":\"id\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].name\",\"mergedName\":\"name\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].Status\",\"mergedName\":\"Status\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].Type\",\"mergedName\":\"Type\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].skuname\",\"mergedName\":\"skuname\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].skutier\",\"mergedName\":\"skutier\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].PredictiveAutoscale\",\"mergedName\":\"PredictiveAutoscale\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].AutoScaleProfiles\",\"mergedName\":\"AutoScaleProfiles\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - AppServiceplandetails].workers\",\"mergedName\":\"workers\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].maxworkers\",\"mergedName\":\"maxworkers\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].webRG\",\"mergedName\":\"webRG\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].planId1\",\"mergedName\":\"planId1\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].resourceGroup\",\"mergedName\":\"resourceGroup\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].WebAppName\",\"mergedName\":\"WebAppName\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].WebAppLocation\",\"mergedName\":\"WebAppLocation\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].AppServicePlan\",\"mergedName\":\"AppServicePlan\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - WebFunctionStatus].WebAppRG\",\"mergedName\":\"WebAppRG\",\"fromId\":\"3fddbdd9-c4eb-46ae-b6b0-654c0da7b1a8\"},{\"originalName\":\"[query - AppServiceplandetails].Sites\",\"mergedName\":\"Sites\",\"fromId\":\"unknown\"},{\"originalName\":\"[query - WebFunctionStatus].SubscriptionName\"},{\"originalName\":\"[query - WebFunctionStatus].id1\"},{\"originalName\":\"[query - AppServiceplandetails].resourceGroup1\"}]}", + "size": 0, + "title": "Web Apps", + "noDataMessage": "You have no WebApps!", + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ { - "operator": "==", - "thresholdValue": "lacluster", - "representation": "green", - "text": "{0}{1}" + "columnMatch": "Name", + "formatter": 1 }, { - "operator": "==", - "thresholdValue": "free", - "representation": "gray", - "text": "{0}{1}" + "columnMatch": "SubscriptionName", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } }, { - "operator": "==", - "thresholdValue": "capacityreservation", - "representation": "green", - "text": "{0}{1}" + "columnMatch": "name", + "formatter": 5 }, { - "operator": "Default", - "thresholdValue": null, - "representation": "red", - "text": "{0}{1}" - } - ] - } - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": "Resource", - "linkIsContextBlade": true, - "showIcon": true - } - }, - { - "columnMatch": "tagName", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "columnMatch": "Status", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "Running", + "representation": "success", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Stopped", + "representation": "disabled", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } + }, { - "operator": "is Empty", - "representation": "Blank", - "text": "{0}{1}" + "columnMatch": "webRG", + "formatter": 5 }, { - "operator": "Default", - "thresholdValue": null, - "representation": "Tags", - "text": "{0}{1}" - } - ] - } - }, - { - "columnMatch": "tagValue", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "columnMatch": "planId1", + "formatter": 5 + }, { - "operator": "is Empty", - "representation": "Blank", - "text": "{0}{1}" + "columnMatch": "resourceGroup", + "formatter": 5 }, { - "operator": "Default", - "thresholdValue": null, - "representation": "Tags", - "text": "{0}{1}" + "columnMatch": "WebAppName", + "formatter": 5 + }, + { + "columnMatch": "AppServicePlan", + "formatter": 5 + }, + { + "columnMatch": "WebAppRG", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 1 } - ] - } - } - ], - "rowLimit": 10000, - "filter": true, - "labelSettings": [ - { - "columnId": "id", - "label": "Workspace" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" - }, - { - "columnId": "location", - "label": "Location" - }, - { - "columnId": "retentionDays", - "label": "Retention (days)" - }, - { - "columnId": "dailyquotaGB", - "label": "Daily Cap (GB)" - }, - { - "columnId": "sku", - "label": "Pricing Tier" - }, - { - "columnId": "subscriptionId", - "label": "Subscription" - }, - { - "columnId": "tagName", - "label": "Tag Name" - }, - { - "columnId": "tagValue", - "label": "Tag Value" - } - ] - }, - "sortBy": [] - }, - "name": "logAnalyticsWorkspaces", - "styleSettings": { - "showBorder": true - } - }, - { - "type": 1, - "content": { - "json": "💡_Select one or more workspaces from the list above to see daily ingestion trend_" - }, - "conditionalVisibility": { - "parameterName": "selectedWorkspaceId", - "comparison": "isEqualTo" - }, - "name": "text - 3", - "styleSettings": { - "showBorder": true - } - }, - { - "type": 9, - "content": { - "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], - "parameters": [ - { - "id": "d9c04e61-453f-4f85-8d7e-1a34037d836b", - "version": "KqlParameterItem/1.0", - "name": "selectedWorkspaces", - "type": 5, - "isRequired": true, - "multiSelect": true, - "quote": "'", - "delimiter": ",", - "query": "where type =~ 'microsoft.operationalinsights/workspaces'\r\n| where id in ({selectedWorkspaceId})", - "crossComponentResources": ["{Subscription}"], - "isHiddenWhenLocked": true, - "typeSettings": { - "additionalResourceOptions": [], - "showDefault": false - }, - "timeContext": { - "durationMs": 2592000000 + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "name" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "planId", + "label": "Plan ID" + }, + { + "columnId": "id", + "label": "ID" + }, + { + "columnId": "name", + "label": "Name" + }, + { + "columnId": "skuname", + "label": "SKU" + }, + { + "columnId": "skutier", + "label": "SKU Tier" + }, + { + "columnId": "PredictiveAutoscale", + "label": "Autoscale Enabled?" + }, + { + "columnId": "AutoScaleProfiles", + "label": "Autoscale Profile" + }, + { + "columnId": "workers", + "label": "Workers" + }, + { + "columnId": "maxworkers", + "label": "Max. Workers" + }, + { + "columnId": "webRG", + "label": "Application Resource Group" + }, + { + "columnId": "WebAppName", + "label": "Application Name" + }, + { + "columnId": "WebAppLocation", + "label": "Application Location" + }, + { + "columnId": "AppServicePlan", + "label": "App Service Plan" + }, + { + "columnId": "WebAppRG", + "label": "Application Resource Group" + } + ] + } }, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "value": null + "name": "Get-Idle-WebApp" }, { - "id": "2108523c-fb80-49b3-9ff1-ea5e5eca2091", - "version": "KqlParameterItem/1.0", - "name": "TimeRange", - "label": "Time range", - "type": 4, - "isRequired": true, - "typeSettings": { - "selectableValues": [ - { - "durationMs": 172800000 - }, - { - "durationMs": 604800000 - }, - { - "durationMs": 1209600000 - }, - { - "durationMs": 2592000000 - } - ] - }, - "timeContext": { - "durationMs": 2592000000 + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type == \"microsoft.web/serverfarms\" and properties.numberOfSites == \"0\"\r\n| extend id, planId=tolower(tostring(id)),skuname = tostring(sku.name) , skutier = tostring(sku.tier), workers=tostring(properties.numberOfWorkers),webRG=resourceGroup,maxworkers=tostring(properties.maximumNumberOfWorkers), Sites=tostring(properties.numberOfSites), SubscriptionName=subscriptionId\r\n| project id, planId, name, skuname, skutier, workers, maxworkers, webRG, Sites, SubscriptionName\r\n| join kind=leftouter (resources | where type ==\"microsoft.insights/autoscalesettings\" | project planId=tolower(tostring(properties.targetResourceUri)), PredictiveAutoscale=properties.predictiveAutoscalePolicy.scaleMode, AutoScaleProfiles=properties.profiles,resourceGroup) on planId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n ) on id", + "size": 0, + "noDataMessage": "All of your App Service's plan have at least one website.", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "name", + "formatter": 5 + }, + { + "columnMatch": "maxworkers", + "formatter": 5 + }, + { + "columnMatch": "SubscriptionName", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "planId1", + "formatter": 5 + }, + { + "columnMatch": "PredictiveAutoscale", + "formatter": 5 + }, + { + "columnMatch": "AutoScaleProfiles", + "formatter": 5 + }, + { + "columnMatch": "resourceGroup", + "formatter": 5 + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "planId", + "label": "App Service Plan " + }, + { + "columnId": "name", + "label": "SKU Name" + }, + { + "columnId": "skuname", + "label": "SKU Name" + }, + { + "columnId": "skutier", + "label": "SKU Tier" + }, + { + "columnId": "workers", + "label": "Number of Workers " + }, + { + "columnId": "maxworkers", + "label": "Number of websites" + }, + { + "columnId": "webRG", + "label": "Resource Group " + }, + { + "columnId": "Sites", + "label": "Number of websites" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + } + ] + } }, - "value": { - "durationMs": 2592000000 - } + "name": "query - IdleServicePlans" } - ], - "style": "pills", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources" + ] }, "conditionalVisibility": { - "parameterName": "_", + "parameterName": "SelectedSubTab", "comparison": "isEqualTo", - "value": "_" + "value": "webapp" }, - "name": "parameters - 2" + "name": "WebAppGroup" }, { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "Usage\r\n| where StartTime >= startofday({TimeRange:start}) and EndTime < startofday(now())\r\n| where IsBillable == true\r\n| project Quantity, ResourceUri, TimeGenerated\r\n| summarize BillableDataGB = sum(Quantity / 1024.) by bin(TimeGenerated, 1d)\r\n| project TimeGenerated, BillableDataGB", - "size": 0, - "aggregation": 5, - "title": "Total Daily Ingestion for selected workspaces - Trend by {TimeRange:label}", - "timeContextFromParameter": "TimeRange", - "showRefreshButton": true, - "showExportToExcel": true, - "queryType": 0, - "resourceType": "microsoft.operationalinsights/workspaces", - "crossComponentResources": ["{selectedWorkspaces}"], - "visualization": "barchart", - "chartSettings": { - "seriesLabelSettings": [ - { - "seriesName": "BillableDataGB", - "label": "Ingested data" - } - ], - "ySettings": { - "numberFormatSettings": { - "unit": 39, - "options": { - "style": "decimal", - "useGrouping": true, - "maximumFractionDigits": 2 - } - } - } - } - }, - "conditionalVisibility": { - "parameterName": "selectedWorkspaceId", - "comparison": "isNotEqualTo" - }, - "name": "dailyIngestionTrend", - "styleSettings": { - "showBorder": true - } - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "Monitoring" - }, - "name": "MonitoringGroup" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| where properties.impactedField has \"Workspaces\"\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Impact=properties.impact,resourceGroup,subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Recommendation=tostring(properties.shortDescription.problem), Impact=properties.impact,resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| where Category == 'Cost' \r\n| where SubCategory has \"Workspaces\"\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2", - "size": 0, - "title": "Azure Advisor Cost recommendations", - "noDataMessage": "You are following all of our cost recommendations for Monitoring", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "Group", - "formatter": 1 - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "id", - "formatter": 5 - }, - { - "columnMatch": "stableId", - "formatter": 5 - }, - { - "columnMatch": "recommendationTypeId", - "formatter": 5 - }, - { - "columnMatch": "maxCpuP95", - "formatter": 5 - }, - { - "columnMatch": "excludeRecomm", - "formatter": 5 - }, - { - "columnMatch": "lowCpuThreshold", - "formatter": 5 - }, - { - "columnMatch": "AdditionaInfo", - "formatter": 5, - "formatOptions": { - "customColumnWidthSetting": "19ch" - } - }, - { - "columnMatch": "isActive1", - "formatter": 5 + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Log Analytics workspace\r\nA [Log Analytics workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-workspace-overview) is a unique environment for log data from Azure Monitor and other Azure services, such as Microsoft Sentinel and Microsoft Defender for Cloud. Each workspace has its own data repository and configuration but might combine data from multiple services. The following advices could be of help in cost optimization:\r\n\r\n1. Adopt [commitment tiers](https://learn.microsoft.com/azure/azure-monitor/logs/cost-logs#commitment-tiers) where applicable.\r\n2. Adopt [Azure Monitor Logs dedicated cluster](https://learn.microsoft.com/azure/azure-monitor/logs/cost-logs#dedicated-clusters) if a single workspace does not ingest enough data as per the minimum commitment tier (100 GB/day) or if it is possible to aggregate ingestion costs from more than one workspace in the same region.\r\n3. Convert the free tier based workspace to **Pay-as-you-go** model and add them to an Azure Monitor Logs dedicated cluster where possible.", + "style": "upsell" }, - { - "columnMatch": "excludeProperty", - "formatter": 5 - } - ], - "rowLimit": 1000, - "filter": true, - "hierarchySettings": { - "treeType": 1, - "groupBy": ["Recommendation"], - "expandTopLevel": true + "name": "MonitoringRecommendations" }, - "labelSettings": [ - { - "columnId": "AffectedResource", - "label": "Affected Resource" - }, - { - "columnId": "Category", - "label": "Recommendation Category" - }, - { - "columnId": "SubCategory", - "label": "Affected Resource Type" - }, - { - "columnId": "Recommendation", - "label": "Recommendation" - }, - { - "columnId": "Impact", - "label": "Impact" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" - }, - { - "columnId": "subscriptionId", - "label": "Subscription ID" - } - ] - } - }, - "conditionalVisibility": { - "parameterName": "isVisible", - "comparison": "isEqualTo", - "value": "true" - }, - "name": "Get-AdvisorRecommendations-Monitoring" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where type has \"microsoft.operationalinsights/workspaces\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", - "size": 0, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "rowLimit": 10000 - } + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type =~ 'microsoft.operationalinsights/workspaces'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend \r\n state = trim(' ', tostring(properties.provisioningState)),\r\n sku = trim(' ', tostring(properties.sku.name)),\r\n skuUpdate = trim(' ', tostring(properties.sku.lastSkuUpdate)),\r\n retentionDays = toint(properties.retentionInDays),\r\n dailyquotaGB = trim(' ', tostring(properties.workspaceCapping.dailyQuotaGb))\r\n| extend dailyquotaGB = iif(dailyquotaGB !=-1.0, dailyquotaGB,\"--\")\r\n| project id, resourceGroup, location, retentionDays, dailyquotaGB, sku, subscriptionId\r\n| join kind = inner (\r\n resources\r\n | where type =~ 'microsoft.operationalinsights/workspaces'\r\n | where resourceGroup in ({ResourceGroup})\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags[tagName])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | summarize arg_max(tagName, tagValue) by id\r\n) on id\r\n| extend resourceGroup = tostring(split(id,'/providers/')[0])\r\n| project-away id1", + "size": 0, + "title": "Log Analytics Workspaces", + "showRefreshButton": true, + "exportMultipleValues": true, + "exportedParameters": [ + { + "fieldName": "id", + "parameterName": "selectedWorkspaceId", + "parameterType": 1 + } + ], + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "id", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "insights", + "linkIsContextBlade": true, + "showIcon": true + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": "Resource", + "linkIsContextBlade": true, + "showIcon": true + } + }, + { + "columnMatch": "retentionDays", + "formatter": 4, + "formatOptions": { + "min": 1, + "max": 730, + "palette": "blue", + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "dailyquotaGB", + "formatter": 0, + "formatOptions": { + "customColumnWidthSetting": "10%" + } + }, + { + "columnMatch": "sku", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "lacluster", + "representation": "green", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "free", + "representation": "gray", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "capacityreservation", + "representation": "green", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "red", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": "Resource", + "linkIsContextBlade": true, + "showIcon": true + } + }, + { + "columnMatch": "tagName", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "is Empty", + "representation": "Blank", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Tags", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "tagValue", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "is Empty", + "representation": "Blank", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "Tags", + "text": "{0}{1}" + } + ] + } + } + ], + "rowLimit": 10000, + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Workspace" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "retentionDays", + "label": "Retention (days)" + }, + { + "columnId": "dailyquotaGB", + "label": "Daily Cap (GB)" + }, + { + "columnId": "sku", + "label": "Pricing Tier" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + }, + { + "columnId": "tagName", + "label": "Tag Name" + }, + { + "columnId": "tagValue", + "label": "Tag Value" + } + ] + }, + "sortBy": [] + }, + "name": "logAnalyticsWorkspaces", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 1, + "content": { + "json": "💡_Select one or more workspaces from the list above to see daily ingestion trend_" + }, + "conditionalVisibility": { + "parameterName": "selectedWorkspaceId", + "comparison": "isEqualTo" + }, + "name": "text - 3", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "d9c04e61-453f-4f85-8d7e-1a34037d836b", + "version": "KqlParameterItem/1.0", + "name": "selectedWorkspaces", + "type": 5, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "where type =~ 'microsoft.operationalinsights/workspaces'\r\n| where id in ({selectedWorkspaceId})", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 2592000000 + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null + }, + { + "id": "2108523c-fb80-49b3-9ff1-ea5e5eca2091", + "version": "KqlParameterItem/1.0", + "name": "TimeRange", + "label": "Time range", + "type": 4, + "isRequired": true, + "typeSettings": { + "selectableValues": [ + { + "durationMs": 172800000 + }, + { + "durationMs": 604800000 + }, + { + "durationMs": 1209600000 + }, + { + "durationMs": 2592000000 + } + ] + }, + "timeContext": { + "durationMs": 2592000000 + }, + "value": { + "durationMs": 2592000000 + } + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "conditionalVisibility": { + "parameterName": "_", + "comparison": "isEqualTo", + "value": "_" + }, + "name": "parameters - 2" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Usage\r\n| where StartTime >= startofday({TimeRange:start}) and EndTime < startofday(now())\r\n| where IsBillable == true\r\n| project Quantity, ResourceUri, TimeGenerated\r\n| summarize BillableDataGB = sum(Quantity / 1024.) by bin(TimeGenerated, 1d)\r\n| project TimeGenerated, BillableDataGB", + "size": 0, + "aggregation": 5, + "title": "Total Daily Ingestion for selected workspaces - Trend by {TimeRange:label}", + "timeContextFromParameter": "TimeRange", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces", + "crossComponentResources": [ + "{selectedWorkspaces}" + ], + "visualization": "barchart", + "chartSettings": { + "seriesLabelSettings": [ + { + "seriesName": "BillableDataGB", + "label": "Ingested data" + } + ], + "ySettings": { + "numberFormatSettings": { + "unit": 39, + "options": { + "style": "decimal", + "useGrouping": true, + "maximumFractionDigits": 2 + } + } + } + } + }, + "conditionalVisibility": { + "parameterName": "selectedWorkspaceId", + "comparison": "isNotEqualTo" + }, + "name": "dailyIngestionTrend", + "styleSettings": { + "showBorder": true + } + } + ] }, "conditionalVisibility": { - "parameterName": "isVisible", + "parameterName": "SelectedSubTab", "comparison": "isEqualTo", - "value": "true" + "value": "Monitoring" }, - "name": "query - tags - list all network resources" + "name": "MonitoringGroup" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Monitoring\",\"rightTable\":\"query - tags - list all network resources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].AffectedResource\",\"mergedName\":\"Affected Resource\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].Category\",\"mergedName\":\"Recommendation Category\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].SubCategory\",\"mergedName\":\"Affected Resource Type\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].subscriptionId\",\"mergedName\":\"Subscription ID\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].id\",\"mergedName\":\"id\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].stableId\",\"mergedName\":\"stableId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].recommendationTypeId\",\"mergedName\":\"recommendationTypeId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].maxCpuP95\",\"mergedName\":\"maxCpuP95\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].excludeRecomm\",\"mergedName\":\"excludeRecomm\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].lowCpuThreshold\",\"mergedName\":\"lowCpuThreshold\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].AdditionaInfo\",\"mergedName\":\"AdditionaInfo\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].isActive1\",\"mergedName\":\"isActive1\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].excludeProperty\",\"mergedName\":\"excludeProperty\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[query - tags - list all network resources].id\",\"mergedName\":\"id1\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeProperty\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].isActive1\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].AdditionaInfo\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].lowCpuThreshold\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeRecomm\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].maxCpuP95\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].recommendationTypeId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].stableId\"},{\"originalName\":\"[query - tags - list all network resources].id\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].id\"}]}", - "size": 0, - "title": "Azure Advisor Cost recommendations", - "noDataMessage": "You are following all of our cost recommendations for Monitoring", - "noDataMessageStyle": 3, - "queryType": 7 - }, - "showPin": false, - "name": "query - Merge - Monitoring Advisor recommendations" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "Monitoring" - }, - "name": "AdvisorGroupMonitoring" - } - ] - }, - "conditionalVisibilities": [ - { - "parameterName": "SelectedTab", - "comparison": "isEqualTo", + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| where properties.impactedField has \"Workspaces\"\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Impact=properties.impact,resourceGroup,subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Recommendation=tostring(properties.shortDescription.problem), Impact=properties.impact,resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| where Category == 'Cost' \r\n| where SubCategory has \"Workspaces\"\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Monitoring", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "stableId", + "formatter": 5 + }, + { + "columnMatch": "recommendationTypeId", + "formatter": 5 + }, + { + "columnMatch": "maxCpuP95", + "formatter": 5 + }, + { + "columnMatch": "excludeRecomm", + "formatter": 5 + }, + { + "columnMatch": "lowCpuThreshold", + "formatter": 5 + }, + { + "columnMatch": "AdditionaInfo", + "formatter": 5, + "formatOptions": { + "customColumnWidthSetting": "19ch" + } + }, + { + "columnMatch": "isActive1", + "formatter": 5 + }, + { + "columnMatch": "excludeProperty", + "formatter": 5 + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "AffectedResource", + "label": "Affected Resource" + }, + { + "columnId": "Category", + "label": "Recommendation Category" + }, + { + "columnId": "SubCategory", + "label": "Affected Resource Type" + }, + { + "columnId": "Recommendation", + "label": "Recommendation" + }, + { + "columnId": "Impact", + "label": "Impact" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + } + ] + } + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "Get-AdvisorRecommendations-Monitoring" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where type has \"microsoft.operationalinsights/workspaces\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "rowLimit": 10000 + } + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "query - tags - list all network resources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Monitoring\",\"rightTable\":\"query - tags - list all network resources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].AffectedResource\",\"mergedName\":\"Affected Resource\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].Category\",\"mergedName\":\"Recommendation Category\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].SubCategory\",\"mergedName\":\"Affected Resource Type\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].subscriptionId\",\"mergedName\":\"Subscription ID\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].id\",\"mergedName\":\"id\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].stableId\",\"mergedName\":\"stableId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].recommendationTypeId\",\"mergedName\":\"recommendationTypeId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].maxCpuP95\",\"mergedName\":\"maxCpuP95\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].excludeRecomm\",\"mergedName\":\"excludeRecomm\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].lowCpuThreshold\",\"mergedName\":\"lowCpuThreshold\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].AdditionaInfo\",\"mergedName\":\"AdditionaInfo\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].isActive1\",\"mergedName\":\"isActive1\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Monitoring].excludeProperty\",\"mergedName\":\"excludeProperty\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[query - tags - list all network resources].id\",\"mergedName\":\"id1\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeProperty\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].isActive1\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].AdditionaInfo\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].lowCpuThreshold\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeRecomm\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].maxCpuP95\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].recommendationTypeId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].stableId\"},{\"originalName\":\"[query - tags - list all network resources].id\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].id\"}]}", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Monitoring", + "noDataMessageStyle": 3, + "queryType": 7 + }, + "showPin": false, + "name": "query - Merge - Monitoring Advisor recommendations" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "Monitoring" + }, + "name": "AdvisorGroupMonitoring" + } + ] + }, + "name": "group - 0 " + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", "value": "UsageOptimization" }, { @@ -1461,7 +1673,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "7a720abf-5b4a-4fb1-adaf-2383e70f625d", @@ -1473,7 +1687,9 @@ "quote": "'", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "includeAll": false, "showDefault": false }, @@ -1494,9 +1710,13 @@ "quote": "'", "delimiter": ",", "query": "resources\r\n| distinct resourceGroup", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "defaultValue": "value::all", @@ -1510,7 +1730,9 @@ "type": 1, "isRequired": true, "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "isHiddenWhenLocked": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", @@ -1522,7 +1744,9 @@ "name": "TagName", "type": 2, "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -1537,7 +1761,9 @@ "name": "TagValue", "type": 2, "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -1570,7 +1796,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "ae7eb928-8873-46f8-a3ff-77f45c207fb3", @@ -1578,9 +1806,13 @@ "name": "Location", "type": 2, "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::1"] + "additionalResourceOptions": [ + "value::1" + ] }, "timeContext": { "durationMs": 86400000 @@ -1603,2127 +1835,2991 @@ }, "name": "parameters - location" }, - { - "type": 11, - "content": { - "version": "LinkItem/1.0", - "style": "tabs", - "links": [ - { - "id": "e5d97e9d-97e6-45f2-871c-376799213b6a", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Azure Firewall", - "subTarget": "firewall", - "style": "link" - }, - { - "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Application Gateway", - "subTarget": "appGateway", - "preText": "VM", - "style": "link" - }, - { - "id": "61595d5e-9f25-4919-95a6-1462739f4657", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Load Balancer", - "subTarget": "loadBalancer", - "style": "link" - }, - { - "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Public IP Address", - "subTarget": "publicIP", - "style": "link" - }, - { - "id": "79e7a97a-1413-41e8-b4c6-ebd1d0a45e2e", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Virtual Network Gateway", - "subTarget": "vpnGw", - "style": "link" - }, - { - "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Advisor recommendations", - "subTarget": "advisorNetworking", - "style": "link" - } - ] - }, - "name": "links - Networking" - }, { "type": 12, "content": { "version": "NotebookGroup/1.0", "groupType": "editable", + "title": "Networking cost optimization recommendations", "items": [ { - "type": 12, + "type": 9, "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ { - "type": 1, - "content": { - "json": "# Recommendations for Application Gateways\r\nReview Application Gateways which include backend pools with no targets. Resources listed with 2 red signs are considered idle.", - "style": "upsell" + "id": "ae7eb928-8873-46f8-a3ff-77f45c207fb3", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] }, - "name": "Recommendations for Application Gateways" + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 11, + "content": { + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "e5d97e9d-97e6-45f2-871c-376799213b6a", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Azure Firewall", + "subTarget": "firewall", + "style": "link" }, { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "resources\r\n| where type =~ 'Microsoft.Network/applicationGateways' and resourceGroup in ({ResourceGroup})\r\n| extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier= tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools,resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id, name, SKUName, SKUTier, SKUCapacity,resourceGroup,subscriptionId\r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Network/applicationGateways' and resourceGroup in ({ResourceGroup})\r\n | mvexpand backendPools = properties.backendAddressPools\r\n | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations)\r\n | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses)\r\n | extend backendPoolName = backendPools.properties.backendAddressPools.name\r\n | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by id\r\n) on id\r\n| project-away id1\r\n| where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount))\r\n| order by id asc\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", - "size": 0, - "title": "Application gateways with empty backend pools", - "noDataMessage": "You don't have any Application Gateways with empty backendpools", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "SKUCapacity", - "formatter": 1 - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } + "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Application Gateway", + "subTarget": "appGateway", + "preText": "VM", + "style": "link" + }, + { + "id": "61595d5e-9f25-4919-95a6-1462739f4657", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Load Balancer", + "subTarget": "loadBalancer", + "style": "link" + }, + { + "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Public IP Address", + "subTarget": "publicIP", + "style": "link" + }, + { + "id": "79e7a97a-1413-41e8-b4c6-ebd1d0a45e2e", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Virtual Network Gateway", + "subTarget": "vpnGw", + "style": "link" + }, + { + "id": "5655ef75-a5ec-4f4b-badf-a99191a0493f", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "NAT Gateway", + "subTarget": "natgw", + "style": "link" + }, + { + "id": "68a77162-06c2-4648-83e0-f8f41c4fbda7", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Express Route", + "subTarget": "ER", + "style": "link" + }, + { + "id": "5dd4cb39-5aa1-4de9-bc4c-338e15b8d389", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Private DNS & Private Endpoint", + "subTarget": "privatedns", + "style": "link" + }, + { + "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Advisor recommendations", + "subTarget": "advisorNetworking", + "style": "link" + } + ] + }, + "name": "links - Networking" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Application Gateways\r\nReview Application Gateways which include backend pools with no targets. Resources listed with 2 red signs are considered idle.", + "style": "upsell" }, - { - "columnMatch": "backendIPCount", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "name": "Recommendations for Application Gateways" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type =~ 'Microsoft.Network/applicationGateways' and resourceGroup in ({ResourceGroup})\r\n| extend backendPoolsCount = array_length(properties.backendAddressPools),SKUName= tostring(properties.sku.name), SKUTier= tostring(properties.sku.tier),SKUCapacity=properties.sku.capacity,backendPools=properties.backendAddressPools,resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id, name, SKUName, SKUTier, SKUCapacity,resourceGroup,subscriptionId\r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Network/applicationGateways' and resourceGroup in ({ResourceGroup})\r\n | mvexpand backendPools = properties.backendAddressPools\r\n | extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations)\r\n | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses)\r\n | extend backendPoolName = backendPools.properties.backendAddressPools.name\r\n | summarize backendIPCount = sum(backendIPCount) ,backendAddressesCount=sum(backendAddressesCount) by id\r\n) on id\r\n| project-away id1\r\n| where (backendIPCount == 0 or isempty(backendIPCount)) and (backendAddressesCount==0 or isempty(backendAddressesCount))\r\n| order by id asc\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Application gateways with empty backend pools", + "noDataMessage": "You don't have any Application Gateways with empty backendpools", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ { - "operator": "==", - "thresholdValue": "0", - "representation": "disabled", - "text": "No Backend IPs" + "columnMatch": "SKUCapacity", + "formatter": 1 }, { - "operator": ">", - "thresholdValue": "0", - "representation": "success", - "text": "Backend IP configured" + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } }, { - "operator": "Default", - "thresholdValue": null, - "representation": "success", - "text": "{0}{1}" - } - ] - } - }, - { - "columnMatch": "backendAddressesCount", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ - { - "operator": "==", - "thresholdValue": "0", - "representation": "disabled", - "text": "No Backend targets" + "columnMatch": "backendIPCount", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "0", + "representation": "disabled", + "text": "No Backend IPs" + }, + { + "operator": ">", + "thresholdValue": "0", + "representation": "success", + "text": "Backend IP configured" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } }, { - "operator": ">", - "thresholdValue": "0", - "representation": "success", - "text": "Backend targets available" + "columnMatch": "backendAddressesCount", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "0", + "representation": "disabled", + "text": "No Backend targets" + }, + { + "operator": ">", + "thresholdValue": "0", + "representation": "success", + "text": "Backend targets available" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } }, { - "operator": "Default", - "thresholdValue": null, - "representation": "success", - "text": "{0}{1}" - } - ] - } - }, - { - "columnMatch": "id1", - "formatter": 5 - }, - { - "columnMatch": "Recommendation", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "colors", - "thresholdsGrid": [ - { - "operator": "==", - "thresholdValue": "No Backend targets", - "representation": "redBright", - "text": "No Backend targets" + "columnMatch": "id1", + "formatter": 5 }, { - "operator": "Default", - "thresholdValue": null, - "representation": "green", - "text": "Backend targets enabled" - } - ] - } - }, - { - "columnMatch": "backendPoolIPTarget", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "columnMatch": "Recommendation", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "No Backend targets", + "representation": "redBright", + "text": "No Backend targets" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "green", + "text": "Backend targets enabled" + } + ] + } + }, { - "operator": ">", - "thresholdValue": "0", - "representation": "success", - "text": "" + "columnMatch": "backendPoolIPTarget", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": ">", + "thresholdValue": "0", + "representation": "success", + "text": "" + }, + { + "operator": "==", + "thresholdValue": "0", + "representation": "disabled", + "text": "" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } }, { - "operator": "==", - "thresholdValue": "0", - "representation": "disabled", - "text": "" + "columnMatch": "backendPoolVMTarget", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "is Empty", + "representation": "disabled", + "text": "" + }, + { + "operator": ">", + "thresholdValue": "0", + "representation": "success", + "text": "" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "{0}{1}" + } + ] + } }, { - "operator": "Default", - "thresholdValue": null, - "representation": "success", - "text": "{0}{1}" - } - ] - } - }, - { - "columnMatch": "backendPoolVMTarget", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "columnMatch": "Recommednation", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "colors", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "No Backend targets", + "representation": "redBright", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "green", + "text": "{0}{1}" + } + ] + } + } + ], + "labelSettings": [ { - "operator": "is Empty", - "representation": "disabled", - "text": "" + "columnId": "id", + "label": "ID" }, { - "operator": ">", - "thresholdValue": "0", - "representation": "success", - "text": "" + "columnId": "name", + "label": "Name" }, { - "operator": "Default", - "thresholdValue": null, - "representation": "success", - "text": "{0}{1}" - } - ] - } - }, - { - "columnMatch": "Recommednation", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "colors", - "thresholdsGrid": [ + "columnId": "SKUName", + "label": "SKU" + }, { - "operator": "==", - "thresholdValue": "No Backend targets", - "representation": "redBright", - "text": "{0}{1}" + "columnId": "SKUTier", + "label": "SKU Tier" }, { - "operator": "Default", - "thresholdValue": null, - "representation": "green", - "text": "{0}{1}" + "columnId": "SKUCapacity", + "label": "Capacity" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + }, + { + "columnId": "backendIPCount", + "label": "Has backend pool for IPs?" + }, + { + "columnId": "backendAddressesCount", + "label": "Has backend pool for VMs?" + }, + { + "columnId": "id1", + "label": "ResourceID" } ] } - } - ], - "labelSettings": [ - { - "columnId": "id", - "label": "ID" - }, - { - "columnId": "name", - "label": "Name" - }, - { - "columnId": "SKUName", - "label": "SKU" - }, - { - "columnId": "SKUTier", - "label": "SKU Tier" - }, - { - "columnId": "SKUCapacity", - "label": "Capacity" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" - }, - { - "columnId": "subscriptionId", - "label": "Subscription ID" - }, - { - "columnId": "backendIPCount", - "label": "Has backend pool for IPs?" - }, - { - "columnId": "backendAddressesCount", - "label": "Has backend pool for VMs?" }, - { - "columnId": "id1", - "label": "ResourceID" - } - ] - } + "name": "Get-Idle-AppGW" + } + ] }, - "name": "Get-Idle-AppGW" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "appGateway" - }, - "name": "NetworkingAppGateway" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 1, - "content": { - "json": "# Recommendations for Load Balancers\r\nReview Load balancers with no backend pools, and remove them if not needed.", - "style": "upsell" + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "appGateway" }, - "name": "Recommendations for Load Balancers" + "name": "NetworkingAppGateway" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "resources \r\n| where resourceGroup in ({ResourceGroup})\r\n| extend resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup), SKUName=tostring(sku.name),SKUTier=tostring(sku.tier),location,backendAddressPools = properties.backendAddressPools\r\n| where type =~ 'microsoft.network/loadbalancers' and array_length(backendAddressPools) == 0 and sku.name!='Basic'\r\n| order by id asc\r\n| project id,name, SKUName,SKUTier,backendAddressPools, location,resourceGroup, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", - "size": 0, - "title": "Load Balancers with empty backend pools", - "noDataMessage": "You don't have any Load Balancers with empty backendpools", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "backendAddressPools", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Load Balancers\r\nReview Load balancers with no backend pools, and remove them if not needed.", + "style": "upsell" + }, + "name": "Recommendations for Load Balancers" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where resourceGroup in ({ResourceGroup})\r\n| extend resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup), SKUName=tostring(sku.name),SKUTier=tostring(sku.tier),location,backendAddressPools = properties.backendAddressPools\r\n| where type =~ 'microsoft.network/loadbalancers' and array_length(backendAddressPools) == 0 and sku.name!='Basic'\r\n| order by id asc\r\n| project id,name, SKUName,SKUTier,backendAddressPools, location,resourceGroup, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Load Balancers with empty backend pools", + "noDataMessage": "You don't have any Load Balancers with empty backendpools", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ { - "operator": "==", - "thresholdValue": "0", - "representation": "disabled", - "text": "Empty Backend Pool" + "columnMatch": "backendAddressPools", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "0", + "representation": "disabled", + "text": "Empty Backend Pool" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "" + } + ] + } }, { - "operator": "Default", - "thresholdValue": null, - "representation": "success", - "text": "" + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "name", + "label": "Name" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "backendAddressPools", + "label": "Has backend pool?" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + }, + { + "columnId": "id1", + "label": "ResourceID" } ] } }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "id1", - "formatter": 5 - } - ], - "labelSettings": [ - { - "columnId": "id", - "label": "Resource ID" - }, - { - "columnId": "name", - "label": "Name" - }, - { - "columnId": "SKUName", - "label": "SKU" + "name": "Get-Idle-LB" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "loadBalancer" + }, + "name": "LoadBalancerGroup" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Public IP Addresses\r\nReview unattached Public IP addresses, as they may represent additional cost.\r\n
This query will also show Public IPs attached to Idle network cards.\r\n", + "style": "upsell" }, - { - "columnId": "SKUTier", - "label": "SKU Tier" + "name": "Recommendations for PIP" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'Microsoft.Network/publicIPAddresses' and isempty(properties.ipConfiguration) and isempty(properties.natGateway) and properties.publicIPAllocationMethod =~ 'Static'\r\n| extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, Location=location ,resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId\r\n| union (\r\n Resources \r\n | where type =~ 'microsoft.network/networkinterfaces' and isempty(properties.virtualMachine) and isnull(properties.privateEndpoint) and isnotempty(properties.ipConfigurations) \r\n | extend IPconfig = properties.ipConfigurations \r\n | mv-expand IPconfig \r\n | extend PublicIpId= tostring(IPconfig.properties.publicIPAddress.id)\r\n | project PublicIpId\r\n | join ( \r\n resources \r\n | where type =~ 'Microsoft.Network/publicIPAddresses'\r\n | extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, resourceGroup, Location=location \r\n ) on PublicIpId\r\n | project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId\r\n)\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | extend PublicIpId=id\r\n | distinct PublicIpId\r\n )\r\n on PublicIpId\r\n", + "size": 0, + "title": "Unattached Public IPs", + "noDataMessage": "You have no unattached Public IPs", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "PublicIpId1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "PublicIpId", + "label": "ID" + }, + { + "columnId": "IPName", + "label": "Name" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "AllocationMethod", + "label": "Allocation Method" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + }, + { + "columnId": "PublicIpId1", + "label": "Resource ID" + } + ] + } }, - { - "columnId": "backendAddressPools", - "label": "Has backend pool?" + "name": "Get-Idle-PIP" + }, + { + "type": 1, + "content": { + "json": "# Routing Preference\r\n\r\nAzure routing preference enables you to choose how your traffic routes between Azure and the Internet. You can choose to route traffic either via the Microsoft network or via the ISP network (public internet). By default, traffic is routed via the Microsoft global network for all Azure services.\r\n\r\nRouting preference choices include:\r\n\r\n- **Microsoft Network**: Both ingress and egress traffic stays bulk of the travel on the Microsoft global network. This routing is also known as cold potato routing. This option has a higher ingress/egress cost.\r\n\r\n- **Public Internet (ISP network)**: The new routing choice Internet routing minimizes travel on the Microsoft global network and uses the transit ISP network to route your traffic. This routing is also known as hot potato routing.\r\n\r\nFor more information about routing preference, see [What is routing preference?](https://learn.microsoft.com/azure/virtual-network/ip-services/ip-services-overview#routing-preference).\r\n\r\n", + "style": "upsell" }, - { - "columnId": "location", - "label": "Location" + "name": "text - 3" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'Microsoft.Network/publicIPAddresses' and isnotempty(properties.ipConfiguration)\r\n| where tostring(properties.ipTags)== \"[]\"\r\n| extend PublicIpId=id, RoutingMethod=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, Location=location ,resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project PublicIpId,IPName, RoutingMethod,SKUName, resourceGroup, Location, AllocationMethod, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | extend PublicIpId=id\r\n | distinct PublicIpId\r\n )\r\n on PublicIpId", + "size": 0, + "title": "Public IP Addresses ", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "RoutingMethod", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "info", + "text": "Microsoft Network" + } + ] + } + }, + { + "columnMatch": "PublicIpId1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "PublicIpId", + "label": "ID" + }, + { + "columnId": "IPName", + "label": "Name" + }, + { + "columnId": "RoutingMethod", + "label": "Routing Method" + }, + { + "columnId": "SKUName", + "label": "SKU Name" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "AllocationMethod", + "label": "Allocation Method" + }, + { + "columnId": "subscriptionId", + "label": "SubscriptionId" + }, + { + "columnId": "PublicIpId1", + "label": "Resource ID" + } + ] + } }, - { - "columnId": "resourceGroup", - "label": "Resource Group" + "name": "Query-PIP-RoutingPreference" + }, + { + "type": 1, + "content": { + "json": "# DDoS IP Protection\r\nIf you need to protect fewer than 15 public IP resources, the IP Protection tier is the more cost-effective option. However, if you have more than 15 public IP resources to protect, then the Network Protection tier becomes more cost-effective. \r\n\r\nThis query will surface all Public IP (PIP) addressess with the DDoS Protection enabled. If there are more than 15 Public IP Addresses with DDoS protection in the same virtual network, then it is cheaper to enable DDoS Network protection.\r\n\r\nThe Network Protection tier also provides additional features, including:\r\n\r\n- DDoS Protection Rapid Response (DRR)\r\n- Cost protection guarantees\r\n- Web Application Firewall (WAF) discounts\r\n\r\nFor more information about DDoS protection, see [Which Azure DDoS Protection tier should I choose?](https://learn.microsoft.com/azure/ddos-protection/ddos-faq?source=recommendations#which-azure-ddos-protection-tier-should-i-choose-).", + "style": "upsell" }, - { - "columnId": "subscriptionId", - "label": "Subscription Name" + "name": "text - 5" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.network/publicipaddresses\"\r\n| project ddosProtection=tostring(properties.ddosSettings), name\r\n| where ddosProtection has \"Enabled\"\r\n| count\r\n| project TotalIpsProtected = Count\r\n| extend CheckIpsProtected = iff(TotalIpsProtected >= 15,\"Enable Network Protection tier\", \"Enable PIP DDoS Protection\")", + "size": 0, + "title": "Public IP Addresses DDoS Protection", + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "RoutingMethod", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "info", + "text": "Microsoft Network" + } + ] + } + }, + { + "columnMatch": "PublicIpId1", + "formatter": 5 + } + ] + } }, - { - "columnId": "id1", - "label": "ResourceID" - } - ] - } + "name": "Query-PIP-DDoSProtection" + } + ] }, - "name": "Get-Idle-LB" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "loadBalancer" - }, - "name": "LoadBalancerGroup" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 1, - "content": { - "json": "# Recommendations for Public IP Addresses\r\nReview unattached Public IP addresses, as they may represent additional cost.\r\n
This query will also show Public IPs attached to Idle network cards.\r\n", - "style": "upsell" + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "publicIP" }, - "name": "Recommendations for PIP" + "name": "PIPGroup" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "resources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'Microsoft.Network/publicIPAddresses' and isempty(properties.ipConfiguration) and isempty(properties.natGateway) \r\n| extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, Location=location ,resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId\r\n| union (\r\n Resources \r\n | where type =~ 'microsoft.network/networkinterfaces' and isempty(properties.virtualMachine) and isnull(properties.privateEndpoint) and isnotempty(properties.ipConfigurations) \r\n | extend IPconfig = properties.ipConfigurations \r\n | mv-expand IPconfig \r\n | extend PublicIpId= tostring(IPconfig.properties.publicIPAddress.id)\r\n | project PublicIpId\r\n | join ( \r\n resources \r\n | where type =~ 'Microsoft.Network/publicIPAddresses'\r\n | extend PublicIpId=id, IPName=name, AllocationMethod=tostring(properties.publicIPAllocationMethod), SKUName=sku.name, resourceGroup, Location=location \r\n ) on PublicIpId\r\n | project PublicIpId,IPName, SKUName, resourceGroup, Location, AllocationMethod, subscriptionId\r\n)\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | extend PublicIpId=id\r\n | distinct PublicIpId\r\n )\r\n on PublicIpId\r\n", - "size": 0, - "title": "Unattached Public IPs", - "noDataMessage": "You have no unattached Public IPs", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "PublicIpId1", - "formatter": 5 - } - ], - "labelSettings": [ - { - "columnId": "PublicIpId", - "label": "ID" - }, - { - "columnId": "IPName", - "label": "Name" - }, - { - "columnId": "SKUName", - "label": "SKU" + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Virtual Network Gateways\r\nReview idle Virtual Network Gateways that have no connections defined, as they may represent additional cost.\r\n", + "style": "upsell" }, - { - "columnId": "resourceGroup", - "label": "Resource Group" + "name": "Recommendations for idle virtualNetworkGateways" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.network/virtualnetworkgateways\"\r\n| extend resourceGroup =strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id, GWName=name,resourceGroup,location,subscriptionId\r\n| join kind = leftouter(\r\n resources\r\n | where type == \"microsoft.network/connections\"\r\n | extend id = tostring(properties.virtualNetworkGateway1.id)\r\n | project id\r\n | union (\r\n resources\r\n | where type == \"microsoft.network/connections\"\r\n | extend id = tostring(properties.virtualNetworkGateway2.id)\r\n | project id\r\n )\r\n) on id\r\n| where isempty(id1)\r\n| project id, GWName,resourceGroup,location,subscriptionId,status=id", + "size": 0, + "title": "Idle Virtual Network Gateways", + "noDataMessage": "No Idle Virtual Network Gateways found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "status", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Error-Connection not configured" + } + ] + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "GWName", + "label": "VPN Gateway Name" + }, + { + "columnId": "status", + "label": "Is connected?" + } + ] + } }, - { - "columnId": "AllocationMethod", - "label": "Allocation Method" - }, - { - "columnId": "subscriptionId", - "label": "Subscription Name" - }, - { - "columnId": "PublicIpId1", - "label": "Resource ID" - } - ] - } + "name": "query - Idle Virtual Network gateways" + } + ] }, - "name": "Get-Idle-PIP" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "publicIP" - }, - "name": "PIPGroup" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 1, - "content": { - "json": "# Recommendations for Virtual Network Gateways\r\nReview idle Virtual Network Gateways that have no connections defined, as they may represent additional cost.\r\n", - "style": "upsell" + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "vpnGw" }, - "name": "Recommendations for idle virtualNetworkGateways" + "name": "VPNGW Group" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "resources\r\n| where type == \"microsoft.network/virtualnetworkgateways\"\r\n| extend resourceGroup =strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id, GWName=name,resourceGroup,location,subscriptionId\r\n| join kind = leftouter(\r\n resources\r\n | where type == \"microsoft.network/connections\"\r\n | extend id = tostring(properties.virtualNetworkGateway1.id)\r\n | project id\r\n | union (\r\n resources\r\n | where type == \"microsoft.network/connections\"\r\n | extend id = tostring(properties.virtualNetworkGateway2.id)\r\n | project id\r\n )\r\n) on id\r\n| where isempty(id1)\r\n| project id, GWName,resourceGroup,location,subscriptionId,status=id", - "size": 0, - "title": "Idle Virtual Network Gateways", - "noDataMessage": "No Idle Virtual Network Gateways found", - "noDataMessageStyle": 3, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "status", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| where properties.impactedField has \"Network\"\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Impact=properties.impact,resourceGroup,subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Recommendation=tostring(properties.shortDescription.problem), Impact=properties.impact,resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| where Category == 'Cost' \r\n| where SubCategory has \"Network\"\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Networking", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ { - "operator": "Default", - "thresholdValue": null, - "representation": "warning", - "text": "Error-Connection not configured" + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "stableId", + "formatter": 5 + }, + { + "columnMatch": "recommendationTypeId", + "formatter": 5 + }, + { + "columnMatch": "maxCpuP95", + "formatter": 5 + }, + { + "columnMatch": "excludeRecomm", + "formatter": 5 + }, + { + "columnMatch": "lowCpuThreshold", + "formatter": 5 + }, + { + "columnMatch": "AdditionaInfo", + "formatter": 5, + "formatOptions": { + "customColumnWidthSetting": "19ch" + } + }, + { + "columnMatch": "isActive1", + "formatter": 5 + }, + { + "columnMatch": "excludeProperty", + "formatter": 5 + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "AffectedResource", + "label": "Affected Resource" + }, + { + "columnId": "Category", + "label": "Recommendation Category" + }, + { + "columnId": "SubCategory", + "label": "Affected Resource Type" + }, + { + "columnId": "Recommendation", + "label": "Recommendation" + }, + { + "columnId": "Impact", + "label": "Impact" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" } ] } - } - ], - "filter": true, - "labelSettings": [ - { - "columnId": "id", - "label": "Resource ID" - }, - { - "columnId": "GWName", - "label": "VPN Gateway Name" - }, - { - "columnId": "status", - "label": "Is connected?" - } - ] - } - }, - "name": "query - Idle Virtual Network gateways" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "vpnGw" - }, - "name": "VPNGW Group" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| where properties.impactedField has \"Network\"\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Impact=properties.impact,resourceGroup,subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=properties.category, SubCategory=properties.impactedField, Recommendation=tostring(properties.shortDescription.problem), Impact=properties.impact,resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| where Category == 'Cost' \r\n| where SubCategory has \"Network\"\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2", - "size": 0, - "title": "Azure Advisor Cost recommendations", - "noDataMessage": "You are following all of our cost recommendations for Networking", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "Group", - "formatter": 1 - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "id", - "formatter": 5 }, - { - "columnMatch": "stableId", - "formatter": 5 - }, - { - "columnMatch": "recommendationTypeId", - "formatter": 5 - }, - { - "columnMatch": "maxCpuP95", - "formatter": 5 + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" }, - { - "columnMatch": "excludeRecomm", - "formatter": 5 + "name": "Get-AdvisorRecommendations-Networking" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where type has \"Microsoft.Network\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] }, - { - "columnMatch": "lowCpuThreshold", - "formatter": 5 + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" }, - { - "columnMatch": "AdditionaInfo", - "formatter": 5, - "formatOptions": { - "customColumnWidthSetting": "19ch" - } + "name": "query - tags - list all network resources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Networking\",\"rightTable\":\"query - tags - list all network resources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Networking].AffectedResource\",\"mergedName\":\"AffectedResource\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].Category\",\"mergedName\":\"Category\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].SubCategory\",\"mergedName\":\"SubCategory\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].resourceGroup\",\"mergedName\":\"resourceGroup\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeProperty\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].isActive1\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].AdditionaInfo\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].lowCpuThreshold\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeRecomm\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].maxCpuP95\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].recommendationTypeId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].stableId\"},{\"originalName\":\"[query - tags - list all network resources].id\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].id\"}]}", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Networking", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 7 }, - { - "columnMatch": "isActive1", - "formatter": 5 + "showPin": false, + "name": "query - Merge - Network Advisor recommendations" + } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "advisorNetworking" + }, + "name": "AdvisorGroupNetworking" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for NAT Gateways\r\nReview idle NAT Gateways that have no subnet defined, as they may represent additional cost.\r\n", + "style": "upsell" }, - { - "columnMatch": "excludeProperty", - "formatter": 5 - } - ], - "rowLimit": 1000, - "filter": true, - "hierarchySettings": { - "treeType": 1, - "groupBy": ["Recommendation"], - "expandTopLevel": true + "name": "Recommendations for idle virtualNetworkGateways" }, - "labelSettings": [ - { - "columnId": "AffectedResource", - "label": "Affected Resource" - }, - { - "columnId": "Category", - "label": "Recommendation Category" - }, - { - "columnId": "SubCategory", - "label": "Affected Resource Type" - }, - { - "columnId": "Recommendation", - "label": "Recommendation" - }, - { - "columnId": "Impact", - "label": "Impact" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.network/natgateways\" and isnull(properties.subnets)\r\n| project id, GWName=name, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), Location=location ,resourceGroup=tostring(strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)),subnet=tostring(properties.subnet), subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Idle NAT Gateways", + "noDataMessage": "No idle NAT gateways found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subnet", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Not associated." + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "GWName", + "label": "NAT Gateway Name" + }, + { + "columnId": "SKUName", + "label": "SKU Name" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subnet", + "label": "Subnet" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + } + ] + } }, - { - "columnId": "subscriptionId", - "label": "Subscription ID" - } - ] - } + "name": "query - Idle NAT gateways" + } + ] }, "conditionalVisibility": { - "parameterName": "isVisible", + "parameterName": "SelectedSubTab", "comparison": "isEqualTo", - "value": "true" + "value": "natgw" }, - "name": "Get-AdvisorRecommendations-Networking" + "name": "NATGW Group" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where type has \"Microsoft.Network\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", - "size": 0, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Private DNS\r\nReview private DNS without [Virtual Network Links](https://learn.microsoft.com/azure/dns/private-dns-virtual-network-links).\r\n", + "style": "upsell" + }, + "name": "Recommendations for idle private dns" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == \"microsoft.network/privatednszones\" and properties.numberOfVirtualNetworkLinks == 0\r\n| project id, PrivateDNSName=name, NumberOfRecordSets=tostring(properties.numberOfRecordSets),resourceGroup=tostring(strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)),vNets=tostring(properties.properties.numberOfVirtualNetworkLinks), subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Idle private DNS ", + "noDataMessage": "No idle private DNS found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "vNets", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "0", + "representation": "2", + "text": "Not associated to any vNET" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Not associated to any vNET" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + }, + { + "columnMatch": "subnet", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Not associated." + } + ] + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "PrivateDNSName", + "label": "Private DNS name" + }, + { + "columnId": "NumberOfRecordSets", + "label": "Number of DNS records" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "vNets", + "label": "vNETs associated" + }, + { + "columnId": "subscriptionId", + "label": "Subscription" + } + ] + } + }, + "name": "query - Idle private DNS" + }, + { + "type": 1, + "content": { + "json": "# Recommendations for Private endpoints\r\nReview [Private Endpoints](https://learn.microsoft.com/azure/private-link/private-endpoint-overview) that are not connected to any resource.", + "style": "upsell" + }, + "name": "Recommendations for idle private endpoints" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type =~ \"microsoft.network/privateendpoints\"\r\n| extend connection = iff(array_length(properties.manualPrivateLinkServiceConnections) > 0, properties.manualPrivateLinkServiceConnections[0], properties.privateLinkServiceConnections[0])\r\n| extend subnetId = properties.subnet.id\r\n| extend subnetIdSplit = split(subnetId, \"/\")\r\n| extend vnetId = strcat_array(array_slice(subnetIdSplit,0,8), \"/\")\r\n| extend serviceId = tostring(connection.properties.privateLinkServiceId)\r\n| extend serviceIdSplit = split(serviceId, \"/\")\r\n| extend serviceName = tostring(serviceIdSplit[8])\r\n| extend serviceTypeEnum = iff(isnotnull(serviceIdSplit[6]), tolower(strcat(serviceIdSplit[6], \"/\", serviceIdSplit[7])), \"microsoft.network/privatelinkservices\")\r\n| extend stateEnum = tostring(connection.properties.privateLinkServiceConnectionState.status)\r\n| extend stateDescription = tostring(connection.properties.privateLinkServiceConnectionState.description)\r\n| extend groupIds = tostring(connection.properties.groupIds[0])\r\n| where stateEnum == \"Disconnected\"\r\n| extend Details = pack_all()\r\n| project id, PrivateDNSName=name, stateEnum, stateDescription, resourceGroup=tostring(strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)),serviceName, serviceTypeEnum, groupIds, vnetId, subnetId,subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Idle private endpoints", + "noDataMessage": "No idle private endpoints found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "serviceTypeEnum", + "formatter": 16, + "formatOptions": { + "showIcon": true + } + }, + { + "columnMatch": "vnetId", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "subnetId", + "formatter": 13, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "PrivateDNSName", + "label": "Private Endpoint name" + }, + { + "columnId": "stateEnum", + "label": "State" + }, + { + "columnId": "stateDescription", + "label": "State description" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "serviceName", + "label": "Resource Name" + }, + { + "columnId": "serviceTypeEnum", + "label": "Service Type" + }, + { + "columnId": "groupIds", + "label": "Resource Sub-type" + }, + { + "columnId": "vnetId", + "label": "Subnet" + }, + { + "columnId": "subnetId", + "label": "Subscription" + } + ] + } + }, + "name": "query - Idle private endpoint" + } + ] }, "conditionalVisibility": { - "parameterName": "isVisible", + "parameterName": "SelectedSubTab", "comparison": "isEqualTo", - "value": "true" + "value": "privatedns" }, - "name": "query - tags - list all network resources" + "name": "Private DNS and Private Endpoints Group" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Networking\",\"rightTable\":\"query - tags - list all network resources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Networking].AffectedResource\",\"mergedName\":\"AffectedResource\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].Category\",\"mergedName\":\"Category\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].SubCategory\",\"mergedName\":\"SubCategory\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].resourceGroup\",\"mergedName\":\"resourceGroup\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].subscriptionId\",\"mergedName\":\"subscriptionId\",\"fromId\":\"d446799d-b1af-4bca-9d72-84ba2d87008d\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeProperty\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].isActive1\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].AdditionaInfo\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].lowCpuThreshold\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].excludeRecomm\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].maxCpuP95\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].recommendationTypeId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].stableId\"},{\"originalName\":\"[query - tags - list all network resources].id\"},{\"originalName\":\"[Get-AdvisorRecommendations-Networking].id\"}]}", - "size": 0, - "title": "Azure Advisor Cost recommendations", - "noDataMessage": "You are following all of our cost recommendations for Networking", - "noDataMessageStyle": 3, - "queryType": 7 - }, - "showPin": false, - "name": "query - Merge - Network Advisor recommendations" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "advisorNetworking" - }, - "name": "AdvisorGroupNetworking" - } - ] - }, - "name": "networking - Subscription" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 1, - "content": { - "json": "# Recommendations for Azure Firewall\r\n\r\n## Azure Firewall Premium SKU\r\nThis table identifies Azure Firewalls with Premium SKU and evaluates whether the associated policy incorporates premium-only features or not. If a Premium SKU Firewall lacks a policy with premium features, such as TLS or intrusion detection it will be shown here. To learn more about Azure Firewall skus, check this [SKU comparison table](https://learn.microsoft.com/azure/firewall/choose-firewall-sku). ", - "style": "upsell" - }, - "name": "Recommendations for premium Firewall" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "resources \r\n| where type =~ 'Microsoft.Network/azureFirewalls'\r\n| project FWID=id, firewallName = name, SkuTier = tostring(properties.sku.tier), resourceGroup, location\r\n| join kind=inner (\r\n resources\r\n | where type =~ 'microsoft.network/firewallpolicies'\r\n | mv-expand properties.firewalls\r\n | extend intrusionDetection = tostring(properties.intrusionDetection contains \"Alert\"), transportSecurity = tostring(properties.transportSecurity contains \"keyVaultSecretId\")\r\n | extend FWID=tostring(properties_firewalls.id)\r\n | project PolicyName = name, PolicySKU=tostring(properties.sku.tier), intrusionDetection, transportSecurity, FWID\r\n) on FWID", - "size": 0, - "title": "Azure Firewall Premium", - "noDataMessage": "No Azure Firewall Premium found", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "firewallName", - "formatter": 5 - }, - { - "columnMatch": "FWID1", - "formatter": 5 - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "status", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ - { - "operator": "Default", - "thresholdValue": null, - "representation": "warning", - "text": "Error-Connection not configured" - } - ] - } - } - ], - "filter": true, - "labelSettings": [ - { - "columnId": "FWID", - "label": "Firewall Name" - }, - { - "columnId": "firewallName", - "label": "FWName" - }, - { - "columnId": "SkuTier", - "label": "SKU" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" - }, - { - "columnId": "location", - "label": "Location" - }, - { - "columnId": "PolicyName", - "label": "Policy Name" - }, - { - "columnId": "PolicySKU", - "label": "Policy SKU" - }, - { - "columnId": "intrusionDetection", - "label": "Is Intrusion Detection enabled?" - }, - { - "columnId": "transportSecurity", - "label": "Is TLS enabled?" - } - ] - } - }, - "name": "query - Optimize Premium AZ Firewall" - }, - { - "type": 1, - "content": { - "json": "## Avoid multiple Firewall instances in the same region\r\nOptimize the use of Azure Firewall by having a central instance of Azure Firewall in the hub virtual network or Virtual WAN secure hub and share the same firewall across many spoke virtual networks that are connected to the same hub from the same region. Ensure there's no unexpected cross-region traffic as part of the hub-spoke topology nor multiple Azure firewall instances deployed to the same region. To learn more about Azure Firewall design principles, check [Azure Well-Architected Framework review - Azure Firewall](https://learn.microsoft.com/azure/well-architected/service-guides/azure-firewall#cost-optimization).", - "style": "upsell" - }, - "name": "text - 3" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "resources \r\n| where type =~ 'Microsoft.Network/azureFirewalls'\r\n| mv-expand properties.ipConfigurations\r\n| project FWID=id, firewallName = name, SkuTier = tostring(properties.sku.tier), FWRG=resourceGroup, FWLocation=location, SubnetID=tostring(properties_ipConfigurations.properties.subnet.id)\r\n| join (\r\nresources\r\n| where type =~ 'Microsoft.Network/virtualNetworks' \r\n| mv-expand properties.subnets\r\n| where properties_subnets.id has 'AzureFirewallSubnet'\r\n| extend SubnetID=tostring(properties_subnets.id), SubnetName=name, SubnetLocation=location, SubnetRG=resourceGroup) on SubnetID\r\n| project FWID, FWRG,FWLocation, SubnetID,SubnetName, SubnetRG, SubnetLocation\r\n", - "size": 0, - "title": "Azure Firewall per location", - "noDataMessage": "No Firewall deployed", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "SubnetName", - "formatter": 5 - }, - { - "columnMatch": "firewallName", - "formatter": 5 - }, - { - "columnMatch": "FWID1", - "formatter": 5 - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "status", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ - { - "operator": "Default", - "thresholdValue": null, - "representation": "warning", - "text": "Error-Connection not configured" - } - ] - } - } - ], - "filter": true, - "labelSettings": [ - { - "columnId": "FWID", - "label": "Firewall Name" - }, - { - "columnId": "FWRG", - "label": "Firewall Resource Group" - }, - { - "columnId": "FWLocation", - "label": "Firewall Location" - }, - { - "columnId": "SubnetID", - "label": "Vnet / Subnet Name" - }, - { - "columnId": "SubnetName", - "label": "Subnet extended Name" - }, - { - "columnId": "SubnetRG", - "label": "Subnet Resource Group" - }, - { - "columnId": "SubnetLocation", - "label": "Subnet Location" - } - ] - } - }, - "name": "query - Firewall per Location" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "firewall" - }, - "name": "Firewall Group" - } - ] - }, - "conditionalVisibilities": [ - { - "parameterName": "SelectedTab", - "comparison": "isEqualTo", - "value": "UsageOptimization" - }, - { - "parameterName": "SelectedRateOptimizationTab", - "comparison": "isEqualTo", - "value": "Networking" - } - ], - "name": "NetworkingGroup", - "styleSettings": { - "showBorder": true - } - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "template", - "title": "Storage cost optimization recommendations", - "loadFromTemplateId": "", - "items": [ - { - "type": 9, - "content": { - "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], - "parameters": [ - { - "id": "37ceb1c3-3930-4689-a90b-22f26e42bd81", - "version": "KqlParameterItem/1.0", - "name": "Subscription", - "type": 6, - "isRequired": true, - "multiSelect": true, - "quote": "'", - "delimiter": ",", - "typeSettings": { - "additionalResourceOptions": ["value::all"], - "includeAll": false, - "showDefault": false - }, - "timeContext": { - "durationMs": 86400000 - }, - "defaultValue": "value::all", - "label": " Subscription" - }, - { - "id": "08f5fe68-c2e3-4882-9300-b3e33f572dfe", - "version": "KqlParameterItem/1.0", - "name": "ResourceGroup", - "label": "Resource Group", - "type": 2, - "isRequired": true, - "multiSelect": true, - "quote": "'", - "delimiter": ",", - "query": "resources\r\n| distinct resourceGroup", - "crossComponentResources": ["{Subscription}"], - "typeSettings": { - "additionalResourceOptions": ["value::all"], - "showDefault": false - }, - "defaultValue": "value::all", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources" - }, - { - "id": "4fea3013-df84-4930-a453-8a6bd0375130", - "version": "KqlParameterItem/1.0", - "name": "SingleSubHidden", - "type": 1, - "isRequired": true, - "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", - "crossComponentResources": ["{Subscription}"], - "isHiddenWhenLocked": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "label": "Hidden Subscription" - }, - { - "id": "8412f39d-ee67-4979-b887-47463b8848c2", - "version": "KqlParameterItem/1.0", - "name": "TagName", - "type": 2, - "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", - "crossComponentResources": ["{Subscription}"], - "typeSettings": { - "additionalResourceOptions": [] - }, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "value": null, - "label": "Tag Name" - }, - { - "id": "50c68f38-13a0-4aff-a259-4426c83b7cc0", - "version": "KqlParameterItem/1.0", - "name": "TagValue", - "type": 2, - "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", - "crossComponentResources": ["{Subscription}"], - "typeSettings": { - "additionalResourceOptions": [] - }, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "value": null, - "label": "Tag Value" - } - ], - "style": "pills", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources" - }, - "customWidth": "75", - "conditionalVisibilities": [ - { - "parameterName": "SelectedTab", - "comparison": "isNotEqualTo", - "value": "CostInformation" - }, - { - "parameterName": "SelectedTab", - "comparison": "isNotEqualTo", - "value": "Welcome" - } - ], - "name": "parameters - Filters" - }, - { - "type": 9, - "content": { - "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], - "parameters": [ - { - "id": "eae8a0d2-14e6-4cd1-a2d2-fd6b207cf517", - "version": "KqlParameterItem/1.0", - "name": "Location", - "type": 2, - "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", - "crossComponentResources": ["{Subscription}"], - "typeSettings": { - "additionalResourceOptions": ["value::1"] - }, - "timeContext": { - "durationMs": 86400000 - }, - "defaultValue": "value::1", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "label": "Resource Location" - } - ], - "style": "pills", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources" - }, - "customWidth": "25", - "conditionalVisibility": { - "parameterName": "SelectedTab", - "comparison": "isEqualTo", - "value": "AHB" - }, - "name": "parameters - location" - }, - { - "type": 11, - "content": { - "version": "LinkItem/1.0", - "style": "tabs", - "links": [ - { - "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Storage Accounts", - "subTarget": "Storage", - "preText": "VM", - "style": "link" - }, - { - "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Managed Disks", - "subTarget": "Disks", - "style": "link" - }, - { - "id": "86ff248b-1ce4-4194-8cd4-b1e0a9956b5d", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Backup", - "subTarget": "Backup", - "style": "link" - }, - { - "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", - "cellValue": "SelectedSubTab", - "linkTarget": "parameter", - "linkLabel": "Advisor recommendations", - "subTarget": "advisorStorage", - "style": "link" - } - ] - }, - "name": "links - Storage" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 1, - "content": { - "json": "## Idle backups\r\n\r\nReview protected items backup activity to determine if there are items that have not been backed up in the last 90 days. This could either mean that the underlying resource that's being backed up doesn't exist anymore or there's some issue with the resource that's preventing backups from being taken reliably.\r\n", - "style": "upsell" - }, - "name": "text - idleBackup" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "recoveryservicesresources\r\n| where type =~ 'microsoft.recoveryservices/vaults/backupfabrics/protectioncontainers/protecteditems'\r\n| extend vaultId = tostring(properties.vaultId),resourceId = tostring(properties.sourceResourceId),idleBackup= datetime_diff('day', now(), todatetime(properties.lastBackupTime)) > 90, resourceType=tostring(properties.workloadType), protectionState=tostring(properties.protectionState),lastBackupTime=tostring(properties.lastBackupTime), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup),lastBackupDate=todatetime(properties.lastBackupTime)\r\n| where idleBackup != 0\r\n| project resourceId,vaultId,idleBackup,lastBackupDate,resourceType,protectionState,lastBackupTime,location,resourceGroup,subscriptionId\r\n| join kind = inner(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | extend vaultId = id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | project vaultId\r\n )\r\n on vaultId\r\n | project-away vaultId1", - "size": 0, - "title": "Idle backups", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "idleBackup", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ - { - "operator": ">", - "thresholdValue": "0", - "representation": "2", - "text": "No backup in the last 90 days" + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Azure Firewall\r\n\r\n## Azure Firewall Premium SKU\r\nThis table identifies Azure Firewalls with Premium SKU and evaluates whether the associated policy incorporates premium-only features or not. If a Premium SKU Firewall lacks a policy with premium features, such as TLS or intrusion detection it will be shown here. To learn more about Azure Firewall skus, check this [SKU comparison table](https://learn.microsoft.com/azure/firewall/choose-firewall-sku). ", + "style": "upsell" + }, + "name": "Recommendations for premium Firewall" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'Microsoft.Network/azureFirewalls' and properties.sku.tier==\"Premium\"\r\n| project FWID=id, firewallName = name, SkuTier = tostring(properties.sku.tier), resourceGroup, location\r\n| join kind=inner (\r\n resources\r\n | where type =~ 'microsoft.network/firewallpolicies'\r\n | mv-expand properties.firewalls\r\n | extend intrusionDetection = tostring(properties.intrusionDetection contains \"Alert\" or properties.intrusionDetection contains \"Deny\"), transportSecurity = tostring(properties.transportSecurity contains \"keyVaultSecretId\")\r\n | extend FWID=tostring(properties_firewalls.id)\r\n | where intrusionDetection == \"False\" and transportSecurity == \"False\"\r\n | project PolicyName = name, PolicySKU=tostring(properties.sku.tier), intrusionDetection, transportSecurity, FWID\r\n) on FWID", + "size": 0, + "title": "Azure Firewall Premium", + "noDataMessage": "No Azure Firewall Premium found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "firewallName", + "formatter": 5 + }, + { + "columnMatch": "FWID1", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "status", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Error-Connection not configured" + } + ] + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "FWID", + "label": "Firewall Name" + }, + { + "columnId": "firewallName", + "label": "FWName" + }, + { + "columnId": "SkuTier", + "label": "SKU" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "PolicyName", + "label": "Policy Name" + }, + { + "columnId": "PolicySKU", + "label": "Policy SKU" + }, + { + "columnId": "intrusionDetection", + "label": "Is Intrusion Detection enabled?" + }, + { + "columnId": "transportSecurity", + "label": "Is TLS enabled?" + } + ] + } }, - { - "operator": "Default", - "thresholdValue": null, - "representation": "success", - "text": "" - } - ] - } - }, - { - "columnMatch": "resourceGroup", - "formatter": 14, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - } - ], - "labelSettings": [ - { - "columnId": "resourceId", - "label": "Resource ID" - }, - { - "columnId": "idleBackup", - "label": "Backup activity" - }, - { - "columnId": "lastBackupDate", - "label": "Last backup date" - }, - { - "columnId": "resourceType", - "label": "Resource type" - }, - { - "columnId": "protectionState", - "label": "Protection state" - }, - { - "columnId": "lastBackupTime", - "label": "Last backup time" - }, - { - "columnId": "location", - "label": "Location" + "name": "query - Optimize Premium AZ Firewall" + }, + { + "type": 1, + "content": { + "json": "## Avoid multiple Firewall instances in the same region\r\nOptimize the use of Azure Firewall by having a central instance of Azure Firewall in the hub virtual network or Virtual WAN secure hub and share the same firewall across many spoke virtual networks that are connected to the same hub from the same region. Ensure there's no unexpected cross-region traffic as part of the hub-spoke topology nor multiple Azure firewall instances deployed to the same region. To learn more about Azure Firewall design principles, check [Azure Well-Architected Framework review - Azure Firewall](https://learn.microsoft.com/azure/well-architected/service-guides/azure-firewall#cost-optimization).", + "style": "upsell" + }, + "name": "text - 3" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'Microsoft.Network/azureFirewalls'\r\n| mv-expand properties.ipConfigurations\r\n| project FWID=id, firewallName = name, SkuTier = tostring(properties.sku.tier), FWRG=resourceGroup, FWLocation=location, SubnetID=tostring(properties_ipConfigurations.properties.subnet.id)\r\n| join (\r\nresources\r\n| where type =~ 'Microsoft.Network/virtualNetworks' \r\n| mv-expand properties.subnets\r\n| where properties_subnets.id has 'AzureFirewallSubnet'\r\n| extend SubnetID=tostring(properties_subnets.id), SubnetName=name, SubnetLocation=location, SubnetRG=resourceGroup) on SubnetID\r\n| project FWID, FWRG,FWLocation, SubnetID,SubnetName, SubnetRG, SubnetLocation\r\n", + "size": 0, + "title": "Azure Firewall per location", + "noDataMessage": "No Firewall deployed", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "SubnetName", + "formatter": 5 + }, + { + "columnMatch": "firewallName", + "formatter": 5 + }, + { + "columnMatch": "FWID1", + "formatter": 5 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "status", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "Default", + "thresholdValue": null, + "representation": "warning", + "text": "Error-Connection not configured" + } + ] + } + } + ], + "filter": true, + "labelSettings": [ + { + "columnId": "FWID", + "label": "Firewall Name" + }, + { + "columnId": "FWRG", + "label": "Firewall Resource Group" + }, + { + "columnId": "FWLocation", + "label": "Firewall Location" + }, + { + "columnId": "SubnetID", + "label": "Vnet / Subnet Name" + }, + { + "columnId": "SubnetName", + "label": "Subnet extended Name" + }, + { + "columnId": "SubnetRG", + "label": "Subnet Resource Group" + }, + { + "columnId": "SubnetLocation", + "label": "Subnet Location" + } + ] + } + }, + "name": "query - Firewall per Location" + } + ] }, - { - "columnId": "resourceGroup", - "label": "Resource group" + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "firewall" }, - { - "columnId": "subscriptionId", - "label": "Subscription ID" - } - ] - }, - "sortBy": [] - }, - "name": "query - idleBackups" - }, - { - "type": 1, - "content": { - "json": "## Backup storage redundancy settings\r\n\r\nBy default, when you configure backup for resources, geo-redundant storage (GRS) replication is applied to these backups. While this is the recommended storage replication option as it creates more redundancy for your critical data, you can choose to protect items using locally-redundant storage (LRS) if that meets your backup availability needs for dev-test workloads. Using LRS instead of GRS halves the cost of your backup storage. \r\n\r\n🖱️Click on each vault to see the configured storage replication\r\n", - "style": "upsell" - }, - "name": "text - backupReplication" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "Resources\r\n| where type == 'microsoft.recoveryservices/vaults'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend skuTier = tostring(sku['tier']), skuName = tostring(sku['name']), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup),redundancySettings = tostring(properties.redundancySettings['standardTierStorageRedundancy'])\r\n| order by id asc\r\n| project id,redundancySettings, resourceGroup, location,subscriptionId, skuTier, skuName\r\n| join kind = innerunique (\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | extend vaultId = id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | project id\r\n)\r\non id\r\n| project-away id1\r\n", - "size": 0, - "title": "Recovery vaults storage replication ", - "exportedParameters": [ - { - "fieldName": "RGVault", - "parameterName": "resourceGroupVault", - "parameterType": 1 + "name": "Firewall Group" }, { - "fieldName": "subscriptionId", - "parameterName": "subscriptionId", - "parameterType": 1 - }, - { - "fieldName": "name", - "parameterName": "vaultName", - "parameterType": 1 - } - ], - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "redundancySettings", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ - { - "operator": "==", - "thresholdValue": "GeoRedundant", - "representation": "Globe", - "text": "{0}{1}" + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Recommendations for Express Route\r\n\r\n## Express Route Gateways without a completed circuit (ISP has not completed the connection)\r\nThis table identifies Express Route circutis that have not been completed. ", + "style": "upsell" }, - { - "operator": "Default", - "thresholdValue": null, - "representation": "ResourceFlat", - "text": "{0}{1}" - } - ] - } - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "RGVault", - "formatter": 5 - }, - { - "columnMatch": "name", - "formatter": 5 - } - ] - } - }, - "name": "query - backupStorageReplication" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "Backup" - }, - "name": "group - Backup" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 1, - "content": { - "json": "# Storage accounts\r\nGeneral-purpose v2 storage accounts support the latest Azure Storage features and incorporate all of the functionality of general-purpose v1 and Blob storage accounts. General-purpose v2 accounts are recommended for most storage scenarios.\r\n\r\n1. General-purpose v2 accounts deliver the lowest per-gigabyte capacity prices for Azure Storage, as well as industry-competitive transaction prices.\r\n2. General-purpose v2 accounts support default account access tiers of hot or cool and blob level tiering between hot, cool, or archive.\r\n3. General-purpose v2 accounts allows you to also use lifecycle management to optimize your storage cost", - "style": "upsell" - }, - "name": "Storage accounts" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "resources \r\n| where type =~ 'Microsoft.Storage/StorageAccounts' and kind !='StorageV2' and kind !='FileStorage'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend StorageAccountName=name, SAKind=kind,AccessTier=tostring(properties.accessTier),SKUName=sku.name, SKUTier=sku.tier, Location=location\r\n| order by id asc\r\n| project id,StorageAccountName, SKUName, SKUTier, SAKind,AccessTier, resourceGroup, Location, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", - "size": 0, - "title": "Storage accounts which are not v2", - "noDataMessage": "All storage accounts are General-purpose v2", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "id1", - "formatter": 5 - }, - { - "columnMatch": "storageaccount", - "formatter": 13, - "formatOptions": { - "linkTarget": "Resource", - "subTarget": "insights", - "linkIsContextBlade": true, - "showIcon": true - } - } - ], - "sortBy": [ - { - "itemKey": "$gen_link_id_0", - "sortOrder": 1 - } - ], - "labelSettings": [ - { - "columnId": "id", - "label": "Resource ID" - }, - { - "columnId": "StorageAccountName", - "label": "Name" - }, - { - "columnId": "SKUName", - "label": "SKU" - }, - { - "columnId": "SKUTier", - "label": "SKU Tier" - }, - { - "columnId": "SAKind", - "label": "Kind" - }, - { - "columnId": "AccessTier", - "label": "Access Tier" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" + "name": "Recommendations for premium Firewall" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type =~ 'Microsoft.Network/expressRouteCircuits' and properties.serviceProviderProvisioningState == \"NotProvisioned\"\r\n| extend ServiceLocation=tostring(properties.serviceProviderProperties.peeringLocation), ServiceProvider=tostring(properties.serviceProviderProperties.serviceProviderName), BandwidthInMbps=tostring(properties.serviceProviderProperties.bandwidthInMbps)\r\n| project ERId=id,ERName = name, ERRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), SKUFamily=tostring(sku.family), ERLocation = location, ServiceLocation, ServiceProvider, BandwidthInMbps\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | extend ERId=id\r\n | distinct ERId\r\n )\r\n on ERId\r\n\r\n", + "size": 0, + "title": "Idle Express Route circuits", + "noDataMessage": "No idle Express Route circuit found", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "ERId1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "ERId", + "label": "Express Route ID" + }, + { + "columnId": "ERName", + "label": "E.R. Name" + }, + { + "columnId": "ERRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU Name" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "SKUFamily", + "label": "SKU Family" + }, + { + "columnId": "ERLocation", + "label": "Location" + }, + { + "columnId": "ServiceLocation", + "label": "Service Location" + }, + { + "columnId": "ServiceProvider", + "label": "Service Provider" + }, + { + "columnId": "BandwidthInMbps", + "label": "Bandwidth in Mbps" + } + ] + } + }, + "name": "Idle Express Route circuits" + } + ] }, - { - "columnId": "Location", - "label": "Location" + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "ER" }, - { - "columnId": "subscriptionId", - "label": "Subscription Name" - } - ] - }, - "sortBy": [ - { - "itemKey": "$gen_link_id_0", - "sortOrder": 1 + "name": "Express Route Group" } ] }, - "name": "Get-Storagev1" + "name": "networking - Subscription" } ] }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "Storage" - }, - "name": "group - StorageAccount" - }, + "name": "group - 0" + } + ] + }, + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "UsageOptimization" + }, + { + "parameterName": "SelectedRateOptimizationTab", + "comparison": "isEqualTo", + "value": "Networking" + } + ], + "name": "NetworkingGroup", + "styleSettings": { + "showBorder": true + } + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "template", + "title": "Storage cost optimization recommendations", + "loadFromTemplateId": "", + "items": [ { - "type": 12, + "type": 9, "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ { - "type": 1, - "content": { - "json": "## Unattached Managed Disks\r\n\r\nReview Managed Disks that are not attached to any Virtual machine\r\n", - "style": "upsell" + "id": "37ceb1c3-3930-4689-a90b-22f26e42bd81", + "version": "KqlParameterItem/1.0", + "name": "Subscription", + "type": 6, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "typeSettings": { + "additionalResourceOptions": [ + "value::all" + ], + "includeAll": false, + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 }, - "name": "text - 3" + "defaultValue": "value::all", + "label": " Subscription" }, { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "resources \r\n| where type =~ 'microsoft.compute/disks' and managedBy == \"\"\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend diskState = tostring(properties.diskState)\r\n| where managedBy == \"\" and diskState != 'ActiveSAS'\r\nor diskState == 'Unattached' and diskState != 'ActiveSAS' \r\nand tags !contains 'ASR-ReplicaDisk' and tags !contains 'asrseeddisk'\r\n| extend DiskId=id, DiskIDfull=id, DiskName=name, SKUName=sku.name, SKUTier=sku.tier, DiskSizeGB=tostring(properties.diskSizeGB), Location=location, TimeCreated=tostring(properties.timeCreated), QuickFix=id\r\n| order by DiskId asc \r\n| project DiskId, DiskIDfull, DiskName, DiskSizeGB, SKUName, SKUTier, resourceGroup, QuickFix, Location, TimeCreated, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | extend DiskId = id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct DiskId\r\n )\r\n on DiskId", - "size": 0, - "title": "Unattached disks", - "noDataMessage": "There aren't any unattached disks!", - "noDataMessageStyle": 3, - "exportedParameters": [ - { - "fieldName": "DiskIDfull", - "parameterName": "DiskID" - }, - { - "fieldName": "DiskName", - "parameterName": "DiskName", - "parameterType": 1 - }, - { - "fieldName": "resourceGroup", - "parameterName": "ResourceGroup", - "parameterType": 1 - } + "id": "08f5fe68-c2e3-4882-9300-b3e33f572dfe", + "version": "KqlParameterItem/1.0", + "name": "ResourceGroup", + "label": "Resource Group", + "type": 2, + "isRequired": true, + "multiSelect": true, + "quote": "'", + "delimiter": ",", + "query": "resources\r\n| distinct resourceGroup", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::all" ], - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "visualization": "table", - "gridSettings": { - "formatters": [ - { - "columnMatch": "DiskIDfull", - "formatter": 5 - }, - { - "columnMatch": "QuickFix", - "formatter": 7, - "formatOptions": { - "linkTarget": "ArmAction", - "linkLabel": "Remove Idle Disk", - "linkIsContextBlade": true, - "templateRunContext": { - "componentIdSource": "column", - "componentId": "DiskId", - "templateUriSource": "static", - "templateUri": "https://raw.githubusercontent.com/sebassem/MS-learn-Workbooks/main/Deploy-Tag.json", - "templateParameters": [ - { - "name": "DiskID", - "source": "static", - "value": "DiskId", - "kind": "stringValue" - } - ], - "titleSource": "static", - "title": "Remove Idle Disk", - "descriptionSource": "static", - "description": "# Description\r\nThis ARM Template will remove the selected disk.\r\n\r\n# Actions:\r\n- Click \"Remove Idle Disk\" to remove the selected item.\r\n- Click View Template to examine the template and parameters used during deployment\r\n\r\n\r\n\r\n", - "runLabelSource": "static", - "runLabel": "Remove Idle Disk" - }, - "armActionContext": { - "path": "/{DiskID}?api-version=2021-04-01", - "headers": [], - "params": [ - { - "key": "DiskID", - "value": "" - } - ], - "httpMethod": "DELETE", - "title": "Remove Idle Disks", - "description": "# Disk Deletion Warning: {DiskName}\r\n\r\n**Attention!**\r\n\r\nThis action will permanently remove the disk with the name **{DiskName}**. Please ensure that this disk is not currently in use and that you are deleting the correct disk.\r\n\r\n**Resource Details:**\r\n\r\n- Disk Name: {DiskName}\r\n- Resource Group: {ResourceGroup}\r\n\r\n### Required RBAC Permissions\r\n\r\nTo perform this action, you need to have **Contributor** permissions on the Resource Group where the disk is located.\r\n\r\nPlease review the information carefully before proceeding with the deletion.\r\n", - "actionName": "Removing Idle Dsk", - "runLabel": "I understand, remove disk {DiskName}" - } - } - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - } - ], - "rowLimit": 1000, - "labelSettings": [ - { - "columnId": "DiskId", - "label": "Resource ID" - }, - { - "columnId": "DiskName", - "label": "Name" - }, - { - "columnId": "DiskSizeGB", - "label": "Disk Size (GB)" - }, - { - "columnId": "SKUName", - "label": "SKU" - }, - { - "columnId": "SKUTier", - "label": "SKU Tier" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" - }, - { - "columnId": "QuickFix", - "label": "Delete disk?" - }, - { - "columnId": "Location", - "label": "Location" - }, - { - "columnId": "TimeCreated", - "label": "Time Created" - }, - { - "columnId": "subscriptionId", - "label": "Subscription Name" - } - ] - } + "showDefault": false }, - "name": "Get-Idle-Disk" + "defaultValue": "value::all", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" }, { + "id": "4fea3013-df84-4930-a453-8a6bd0375130", + "version": "KqlParameterItem/1.0", + "name": "SingleSubHidden", "type": 1, - "content": { - "json": "## Old Managed Disks snapshots\r\n\r\nReview Managed Disks snapshots that are older than 30 days\r\n", - "style": "upsell" + "isRequired": true, + "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", + "crossComponentResources": [ + "{Subscription}" + ], + "isHiddenWhenLocked": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Hidden Subscription" + }, + { + "id": "8412f39d-ee67-4979-b887-47463b8848c2", + "version": "KqlParameterItem/1.0", + "name": "TagName", + "type": 2, + "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Name" + }, + { + "id": "50c68f38-13a0-4aff-a259-4426c83b7cc0", + "version": "KqlParameterItem/1.0", + "name": "TagValue", + "type": 2, + "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [] + }, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "value": null, + "label": "Tag Value" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "75", + "conditionalVisibilities": [ + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "CostInformation" + }, + { + "parameterName": "SelectedTab", + "comparison": "isNotEqualTo", + "value": "Welcome" + } + ], + "name": "parameters - Filters" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "eae8a0d2-14e6-4cd1-a2d2-fd6b207cf517", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] }, - "name": "text - 4" - }, + "timeContext": { + "durationMs": 86400000 + }, + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" + }, + "name": "parameters - location" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ { - "type": 3, + "type": 9, "content": { - "version": "KqlItem/1.0", - "query": "resources\r\n| where type == 'microsoft.compute/snapshots'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend TimeCreated = properties.timeCreated\r\n| extend resourceGroup=strcat(\"/subscriptions/\",subscriptionId,\"/resourceGroups/\",resourceGroup)\r\n| where TimeCreated < ago(30d)\r\n| order by id asc \r\n| project id, resourceGroup, location, TimeCreated ,subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", - "size": 0, - "title": "Disk Snapshots with + 30 Days", - "noDataMessage": "No Snapshots with more than 30 days.", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "resourceGroup", - "formatter": 14, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "TimeCreated", - "formatter": 1 - }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "id1", - "formatter": 5 - } - ], - "labelSettings": [ - { - "columnId": "id", - "label": "Name" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" - }, - { - "columnId": "location", - "label": "Location" + "version": "KqlParameterItem/1.0", + "crossComponentResources": [ + "{Subscription}" + ], + "parameters": [ + { + "id": "eae8a0d2-14e6-4cd1-a2d2-fd6b207cf517", + "version": "KqlParameterItem/1.0", + "name": "Location", + "type": 2, + "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", + "crossComponentResources": [ + "{Subscription}" + ], + "typeSettings": { + "additionalResourceOptions": [ + "value::1" + ] }, - { - "columnId": "TimeCreated", - "label": "Time Created" + "timeContext": { + "durationMs": 86400000 }, - { - "columnId": "subscriptionId", - "label": "Subscription Name" - } - ] - } + "defaultValue": "value::1", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "label": "Resource Location" + } + ], + "style": "pills", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources" + }, + "customWidth": "25", + "conditionalVisibility": { + "parameterName": "SelectedTab", + "comparison": "isEqualTo", + "value": "AHB" }, - "name": "Get-Old-Snapshots" + "name": "parameters - location" }, { - "type": 1, + "type": 11, "content": { - "json": "## Managed Disks snapshots using Premium storage\r\n\r\nTo save 60% of cost, we recommend storing your snapshots in Standard Storage, regardless of the storage type of the parent disk. It is the default option for Managed Disks snapshots. Migrate your snapshot from Premium to Standard Storage.\r\n", - "style": "upsell" + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "0211f413-9f36-4750-9ef2-d382ba30ba6c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Storage Accounts", + "subTarget": "Storage", + "preText": "VM", + "style": "link" + }, + { + "id": "dbe9a7fb-6ab1-4de1-a98b-4ec8a9af906c", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Managed Disks", + "subTarget": "Disks", + "style": "link" + }, + { + "id": "86ff248b-1ce4-4194-8cd4-b1e0a9956b5d", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Backup", + "subTarget": "Backup", + "style": "link" + }, + { + "id": "6d563f46-7150-458c-9ee4-0558abe8e29b", + "cellValue": "SelectedSubTab", + "linkTarget": "parameter", + "linkLabel": "Advisor recommendations", + "subTarget": "advisorStorage", + "style": "link" + } + ] }, - "name": "text - 5" + "name": "links - Storage" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "resources\r\n| where type == 'microsoft.compute/snapshots'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend StorageSku = tostring(sku.tier), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup),diskSize=tostring(properties.diskSizeGB)\r\n| where StorageSku == \"Premium\"\r\n| project id,name,StorageSku,diskSize,location,resourceGroup,subscriptionId\r\n| join kind = innerunique(\r\n    resources\r\n    | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n    | extend replaced_tags = parse_json(replaced_tags)\r\n    | mv-expand replaced_tags\r\n    | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n    | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n    | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n    | distinct id\r\n    )\r\n    on id\r\n", - "size": 0, - "title": "Snapshots using premium storage", - "noDataMessage": "No snapshots are using Premium storage", - "noDataMessageStyle": 3, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "resourceGroup", - "formatter": 14, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Idle backups\r\n\r\nReview protected items backup activity to determine if there are items that have not been backed up in the last 90 days. This could either mean that the underlying resource that's being backed up doesn't exist anymore or there's some issue with the resource that's preventing backups from being taken reliably.\r\n", + "style": "upsell" }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true + "name": "text - idleBackup" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "recoveryservicesresources\r\n| where type =~ 'microsoft.recoveryservices/vaults/backupfabrics/protectioncontainers/protecteditems'\r\n| extend vaultId = tostring(properties.vaultId),resourceId = tostring(properties.sourceResourceId),idleBackup= datetime_diff('day', now(), todatetime(properties.lastBackupTime)) > 90, resourceType=tostring(properties.workloadType), protectionState=tostring(properties.protectionState),lastBackupTime=tostring(properties.lastBackupTime), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup),lastBackupDate=todatetime(properties.lastBackupTime)\r\n| where idleBackup != 0\r\n| project resourceId,vaultId,idleBackup,lastBackupDate,resourceType,protectionState,lastBackupTime,location,resourceGroup,subscriptionId\r\n| join kind = inner(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | extend vaultId = id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | project vaultId\r\n )\r\n on vaultId\r\n | project-away vaultId1", + "size": 0, + "title": "Idle backups", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "idleBackup", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": ">", + "thresholdValue": "0", + "representation": "2", + "text": "No backup in the last 90 days" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "success", + "text": "" + } + ] + } + }, + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "labelSettings": [ + { + "columnId": "resourceId", + "label": "Resource ID" + }, + { + "columnId": "idleBackup", + "label": "Backup activity" + }, + { + "columnId": "lastBackupDate", + "label": "Last backup date" + }, + { + "columnId": "resourceType", + "label": "Resource type" + }, + { + "columnId": "protectionState", + "label": "Protection state" + }, + { + "columnId": "lastBackupTime", + "label": "Last backup time" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + } + ] + }, + "sortBy": [] + }, + "name": "query - idleBackups" + }, + { + "type": 1, + "content": { + "json": "## Backup storage redundancy settings\r\n\r\nBy default, when you configure backup for resources, geo-redundant storage (GRS) replication is applied to these backups. While this is the recommended storage replication option as it creates more redundancy for your critical data, you can choose to protect items using locally-redundant storage (LRS) if that meets your backup availability needs for dev-test workloads. Using LRS instead of GRS halves the cost of your backup storage. \r\n\r\n🖱️Click on each vault to see the configured storage replication\r\n", + "style": "upsell" + }, + "name": "text - backupReplication" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "Resources\r\n| where type == 'microsoft.recoveryservices/vaults'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend skuTier = tostring(sku['tier']), skuName = tostring(sku['name']), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup),redundancySettings = tostring(properties.redundancySettings['standardTierStorageRedundancy'])\r\n| order by id asc\r\n| project id,redundancySettings, resourceGroup, location,subscriptionId, skuTier, skuName\r\n| join kind = innerunique (\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | extend vaultId = id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | project id\r\n)\r\non id\r\n| project-away id1\r\n", + "size": 0, + "title": "Recovery vaults storage replication ", + "exportedParameters": [ + { + "fieldName": "RGVault", + "parameterName": "resourceGroupVault", + "parameterType": 1 + }, + { + "fieldName": "subscriptionId", + "parameterName": "subscriptionId", + "parameterType": 1 + }, + { + "fieldName": "name", + "parameterName": "vaultName", + "parameterType": 1 + } + ], + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "redundancySettings", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "GeoRedundant", + "representation": "Globe", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "ResourceFlat", + "text": "{0}{1}" + } + ] + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "RGVault", + "formatter": 5 + }, + { + "columnMatch": "name", + "formatter": 5 + } + ] } }, - { - "columnMatch": "id1", - "formatter": 5 - } - ], - "labelSettings": [ - { - "columnId": "id", - "label": "Resource Id" - }, - { - "columnId": "name", - "label": "Name" - }, - { - "columnId": "StorageSku", - "label": "SKU" - }, - { - "columnId": "diskSize", - "label": "Disk Size (GB)" - }, - { - "columnId": "location", - "label": "Location" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" - }, - { - "columnId": "subscriptionId", - "label": "Subscription Id" - } - ] - } - }, - "name": "query - Snapshots using premium storage" - }, - { - "type": 1, - "content": { - "json": "## Orphaned Managed Disks snapshots\r\n\r\nReview snapshots with deleted source disks.\r\n", - "style": "upsell" - }, - "name": "text - 6" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "resources\r\n| where type == 'microsoft.compute/snapshots'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend parentDisk = properties.creationData.sourceResourceId, diskSize=tostring(properties.diskSizeGB),resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id,parentDisk,diskSize,location,resourceGroup,subscriptionId\r\n| join kind = innerunique(\r\n    resources\r\n    | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n    | extend replaced_tags = parse_json(replaced_tags)\r\n    | mv-expand replaced_tags\r\n    | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n    | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n    | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n    | distinct id\r\n    )\r\n    on id\r\n", - "size": 0, - "title": "All Managed Disks snapshots", - "noDataMessage": "No snapshots found", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "labelSettings": [ - { - "columnId": "id", - "label": "Resource Id" - }, - { - "columnId": "parentDisk", - "label": "Parent Disk Resource Id" - }, - { - "columnId": "diskSize", - "label": "Disk size (GB)" - }, - { - "columnId": "location", - "label": "Location" - }, - { - "columnId": "resourceGroup", - "label": "Resource Group" - }, - { - "columnId": "subscriptionId", - "label": "Subscription Id" - } - ] - } + "name": "query - backupStorageReplication" + } + ] }, "conditionalVisibility": { - "parameterName": "IsVisible", + "parameterName": "SelectedSubTab", "comparison": "isEqualTo", - "value": "true" + "value": "Backup" }, - "name": "query - Retrieve all snapshots" + "name": "group - Backup" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "resources\r\n| where type == 'microsoft.compute/disks'\r\n| project id\r\n", - "size": 0, - "title": "All managed disks", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "labelSettings": [ - { - "columnId": "id", - "label": "Resource Id" - } - ] - } + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "# Storage accounts\r\nGeneral-purpose v2 storage accounts support the latest Azure Storage features and incorporate all of the functionality of general-purpose v1 and Blob storage accounts. General-purpose v2 accounts are recommended for most storage scenarios.\r\n\r\n1. General-purpose v2 accounts deliver the lowest per-gigabyte capacity prices for Azure Storage, as well as industry-competitive transaction prices.\r\n2. General-purpose v2 accounts support default account access tiers of hot or cool and blob level tiering between hot, cool, or archive.\r\n3. General-purpose v2 accounts allows you to also use lifecycle management to optimize your storage cost", + "style": "upsell" + }, + "name": "Storage accounts" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'Microsoft.Storage/StorageAccounts' and kind !='StorageV2' and kind !='FileStorage'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend StorageAccountName=name, SAKind=kind,AccessTier=tostring(properties.accessTier),SKUName=sku.name, SKUTier=sku.tier, Location=location\r\n| order by id asc\r\n| project id,StorageAccountName, SKUName, SKUTier, SAKind,AccessTier, resourceGroup, Location, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Storage accounts which are not v2", + "noDataMessage": "All storage accounts are General-purpose v2", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + }, + { + "columnMatch": "storageaccount", + "formatter": 13, + "formatOptions": { + "linkTarget": "Resource", + "subTarget": "insights", + "linkIsContextBlade": true, + "showIcon": true + } + } + ], + "sortBy": [ + { + "itemKey": "$gen_link_id_0", + "sortOrder": 1 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource ID" + }, + { + "columnId": "StorageAccountName", + "label": "Name" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "SAKind", + "label": "Kind" + }, + { + "columnId": "AccessTier", + "label": "Access Tier" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + } + ] + }, + "sortBy": [ + { + "itemKey": "$gen_link_id_0", + "sortOrder": 1 + } + ] + }, + "name": "Get-Storagev1" + } + ] }, "conditionalVisibility": { - "parameterName": "isVisible", + "parameterName": "SelectedSubTab", "comparison": "isEqualTo", - "value": "True" + "value": "Storage" }, - "name": "query - Retrieve all managed disks" + "name": "group - StorageAccount" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\",\"mergeType\":\"leftanti\",\"leftTable\":\"query - Retrieve all snapshots\",\"rightTable\":\"query - Retrieve all managed disks\",\"leftColumn\":\"parentDisk\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[query - Retrieve all snapshots].id\",\"mergedName\":\"Resource Id\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].parentDisk\",\"mergedName\":\"Parent Disk Resource Id\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].diskSize\",\"mergedName\":\"Disk size (GB)\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].location\",\"mergedName\":\"Location\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].subscriptionId\",\"mergedName\":\"Subscription Id\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].id1\",\"mergedName\":\"id1\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"}]}", - "size": 0, - "title": "Snapshots with deleted source disk", - "noDataMessage": "No orphaned snapshots found", - "noDataMessageStyle": 3, - "queryType": 7, - "gridSettings": { - "formatters": [ - { - "columnMatch": "Parent Disk Resource Id", - "formatter": 5 - }, - { - "columnMatch": "Subscription Id", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - }, - { - "columnMatch": "id1", - "formatter": 5 - } - ], - "labelSettings": [ - { - "columnId": "Resource Id", - "label": "Resource Id" - }, - { - "columnId": "Parent Disk Resource Id", - "label": "Parent Disk resource Id" - }, - { - "columnId": "Disk size (GB)", - "label": "Disk size (GB)" - }, - { - "columnId": "Location", - "label": "Location" + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 1, + "content": { + "json": "## Unattached Managed Disks\r\n\r\nReview Managed Disks that are not attached to any Virtual machine.\r\n\r\n## Last Modified Date\r\nClick on a cell in the specified row to view the last modified date. This may help identify when the disk became idle.\r\n\r\n", + "style": "upsell" }, - { - "columnId": "Resource Group", - "label": "Resource Group" + "name": "text - 3" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "47b2c951-49ef-4352-a52e-fc42fbd77f3c", + "version": "KqlParameterItem/1.0", + "name": "UpdateTime", + "label": "Last update", + "type": 2, + "isRequired": true, + "query": "{\"version\":\"1.0.0\",\"content\":\"[\\r\\n { \\\"value\\\":\\\"0d\\\", \\\"label\\\":\\\"All\\\", \\\"selected\\\":true },\\r\\n { \\\"value\\\":\\\"7d\\\", \\\"label\\\":\\\"> 7 days\\\" },\\r\\n { \\\"value\\\":\\\"14d\\\", \\\"label\\\":\\\"> 14 days\\\" },\\r\\n\\t{ \\\"value\\\":\\\"30d\\\", \\\"label\\\":\\\"> 1 month\\\" },\\r\\n\\t{ \\\"value\\\":\\\"60d\\\", \\\"label\\\":\\\"> 2 months\\\" },\\r\\n\\t{ \\\"value\\\":\\\"90d\\\", \\\"label\\\":\\\"> 3 months\\\" },\\r\\n\\t{ \\\"value\\\":\\\"180d\\\", \\\"label\\\":\\\"> 6 months\\\" },\\r\\n\\t{ \\\"value\\\":\\\"365d\\\", \\\"label\\\":\\\"> 1 year\\\" },\\r\\n\\t{ \\\"value\\\":\\\"730d\\\", \\\"label\\\":\\\"> 2 years\\\" }\\r\\n]\",\"transformers\":null}", + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 8 + }, + { + "version": "KqlParameterItem/1.0", + "name": "DiskSize", + "label": "Disk size", + "type": 2, + "isRequired": true, + "query": "{\"version\":\"1.0.0\",\"content\":\"[\\r\\n { \\\"value\\\":\\\"0\\\", \\\"label\\\":\\\"All\\\", \\\"selected\\\":true },\\r\\n { \\\"value\\\":\\\"64\\\", \\\"label\\\":\\\"> 64 GB\\\" },\\r\\n { \\\"value\\\":\\\"128\\\", \\\"label\\\":\\\"> 128 GB\\\" },\\r\\n\\t{ \\\"value\\\":\\\"256\\\", \\\"label\\\":\\\"> 256 GB\\\" },\\r\\n\\t{ \\\"value\\\":\\\"512\\\", \\\"label\\\":\\\"> 512 GB\\\" },\\r\\n\\t{ \\\"value\\\":\\\"1024\\\", \\\"label\\\":\\\"> 1024 GB\\\" }\\r\\n]\",\"transformers\":null}", + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "timeContext": { + "durationMs": 86400000 + }, + "queryType": 8, + "id": "ec556d4a-4306-4da2-8e58-9326a7a5e3ea" + } + ], + "style": "above", + "queryType": 8 + }, + "name": "idle disks parameters" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources \r\n| where type =~ 'microsoft.compute/disks' and managedBy == \"\"\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend diskState = tostring(properties.diskState)\r\n| where managedBy == \"\" and diskState != 'ActiveSAS'\r\nor diskState == 'Unattached' and diskState != 'ActiveSAS' \r\nand tags !contains 'ASR-ReplicaDisk' and tags !contains 'asrseeddisk'\r\n| extend DiskId=id, DiskIDfull=id, DiskName=name, SKUName=sku.name, SKUTier=sku.tier, DiskSizeGB=toint(properties.diskSizeGB), Location=location, TimeCreated= properties.timeCreated, QuickFix=id, SubId=subscriptionId, LastUpdate = properties.LastOwnershipUpdateTime\r\n| where ago({UpdateTime}) > LastUpdate or LastUpdate == ''\r\n| where DiskSizeGB > {DiskSize}\r\n| order by DiskId asc \r\n| project DiskId, DiskIDfull, DiskName, DiskSizeGB, SKUName, SKUTier, resourceGroup, QuickFix, Location, TimeCreated, LastUpdate, subscriptionId,SubId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | extend DiskId = id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct DiskId\r\n )\r\n on DiskId", + "size": 0, + "title": "Unattached disks", + "noDataMessage": "There aren't any unattached disks!", + "noDataMessageStyle": 3, + "exportedParameters": [ + { + "fieldName": "DiskIDfull", + "parameterName": "DiskID" + }, + { + "fieldName": "DiskName", + "parameterName": "DiskName", + "parameterType": 1 + }, + { + "fieldName": "resourceGroup", + "parameterName": "ResourceGroup", + "parameterType": 1 + }, + { + "fieldName": "SubId", + "parameterName": "subscriptionId", + "parameterType": 1 + } + ], + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "DiskIDfull", + "formatter": 5 + }, + { + "columnMatch": "QuickFix", + "formatter": 7, + "formatOptions": { + "linkTarget": "ArmAction", + "linkLabel": "Remove Idle Disk", + "linkIsContextBlade": true, + "templateRunContext": { + "componentIdSource": "column", + "componentId": "DiskId", + "templateUriSource": "static", + "templateUri": "https://raw.githubusercontent.com/sebassem/MS-learn-Workbooks/main/Deploy-Tag.json", + "templateParameters": [ + { + "name": "DiskID", + "source": "static", + "value": "DiskId", + "kind": "stringValue" + } + ], + "titleSource": "static", + "title": "Remove Idle Disk", + "descriptionSource": "static", + "description": "# Description\r\nThis ARM Template will remove the selected disk.\r\n\r\n# Actions:\r\n- Click \"Remove Idle Disk\" to remove the selected item.\r\n- Click View Template to examine the template and parameters used during deployment\r\n\r\n\r\n\r\n", + "runLabelSource": "static", + "runLabel": "Remove Idle Disk" + }, + "armActionContext": { + "path": "/{DiskID}?api-version=2021-04-01", + "headers": [], + "params": [ + { + "key": "DiskID", + "value": "" + } + ], + "httpMethod": "DELETE", + "title": "Remove Idle Disks", + "description": "# Disk Deletion Warning: {DiskName}\r\n\r\n**Attention!**\r\n\r\nThis action will permanently remove the disk with the name **{DiskName}**. Please ensure that this disk is not currently in use and that you are deleting the correct disk.\r\n\r\n**Resource Details:**\r\n\r\n- Disk Name: {DiskName}\r\n- Resource Group: {ResourceGroup}\r\n\r\n### Required RBAC Permissions\r\n\r\nTo perform this action, you need to have **Contributor** permissions on the Resource Group where the disk is located.\r\n\r\nPlease review the information carefully before proceeding with the deletion.\r\n", + "actionName": "Removing Idle Dsk", + "runLabel": "I understand, remove disk {DiskName}" + } + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "rowLimit": 1000, + "labelSettings": [ + { + "columnId": "DiskId", + "label": "Resource ID" + }, + { + "columnId": "DiskName", + "label": "Name" + }, + { + "columnId": "DiskSizeGB", + "label": "Disk Size (GB)" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "QuickFix", + "label": "Delete disk?" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "TimeCreated", + "label": "Time Created" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + } + ] + }, + "sortBy": [] + }, + "customWidth": "80", + "name": "Get-Idle-Disk" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"ARMEndpoint/1.0\",\"data\":null,\"headers\":[],\"method\":\"GET\",\"path\":\"/subscriptions/{subscriptionId}/resources?\",\"urlParams\":[{\"key\":\"api-version\",\"value\":\"2021-04-01\"},{\"key\":\"$expand\",\"value\":\"createdTime,changedTime,provisioningState\"},{\"key\":\"$filter\",\"value\":\"name eq '{DiskName}' and resourceGroup eq'{ResourceGroup}'\"}],\"batchDisabled\":false,\"transformers\":[{\"type\":\"jsonpath\",\"settings\":{\"tablePath\":\"$.value\",\"columns\":[{\"path\":\"$..id\",\"columnid\":\"id\"},{\"path\":\"$..createdTime\",\"columnid\":\"createdTime\"},{\"path\":\"$..changedTime\",\"columnid\":\"changedTime\"},{\"path\":\"$.name\",\"columnid\":\"name\"}]}}]}", + "size": 0, + "title": "Disk last modified date", + "showExportToExcel": true, + "queryType": 12, + "gridSettings": { + "formatters": [ + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "createdTime", + "formatter": 5 + }, + { + "columnMatch": "name", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Name" + }, + { + "columnId": "createdTime", + "label": "Created time" + }, + { + "columnId": "changedTime", + "label": "Last change time" + } + ] + } + }, + "customWidth": "20", + "conditionalVisibility": { + "parameterName": "DiskID", + "comparison": "isNotEqualTo", + "value": "" + }, + "name": "IdleDisk date" + } + ] }, - { - "columnId": "Subscription Id", - "label": "Subscription Id" - } - ] - } - }, - "showPin": false, - "name": "query - orphaned snapshots" - } - ] - }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "Disks" - }, - "name": "Managed Disks Group" - }, - { - "type": 12, - "content": { - "version": "NotebookGroup/1.0", - "groupType": "editable", - "items": [ - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=tostring(properties.category), SubCategory=tostring(properties.impactedField), Impact=tostring(properties.impact),resourceGroup,subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| where SubCategory has \"Microsoft.Storage\"\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=tostring(properties.category), SubCategory=tostring(properties.impactedField), Recommendation=tostring(properties.shortDescription.problem), Impact=tostring(properties.impact),resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2\r\n| where resourceGroup in ({ResourceGroup})", - "size": 0, - "title": "Azure Advisor Cost recommendations", - "noDataMessage": "You are following all of our cost recommendations for Storage", - "noDataMessageStyle": 3, - "showExportToExcel": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "gridSettings": { - "formatters": [ - { - "columnMatch": "Group", - "formatter": 1 + "name": "Idle Disks Group" + }, + { + "type": 1, + "content": { + "json": "## Old Managed Disks snapshots\r\n\r\nReview Managed Disks snapshots that are older than 30 days\r\n", + "style": "upsell" }, - { - "columnMatch": "subscriptionId", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true + "name": "text - 4" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == 'microsoft.compute/snapshots'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend TimeCreated = properties.timeCreated\r\n| extend resourceGroup=strcat(\"/subscriptions/\",subscriptionId,\"/resourceGroups/\",resourceGroup)\r\n| where TimeCreated < ago(30d)\r\n| order by id asc \r\n| project id, resourceGroup, location, TimeCreated ,subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id", + "size": 0, + "title": "Disk Snapshots with + 30 Days", + "noDataMessage": "No Snapshots with more than 30 days.", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "TimeCreated", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Name" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "TimeCreated", + "label": "Time Created" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Name" + } + ] } }, - { - "columnMatch": "id", - "formatter": 5 - }, - { - "columnMatch": "stableId", - "formatter": 5 - }, - { - "columnMatch": "recommendationTypeId", - "formatter": 5 - }, - { - "columnMatch": "maxCpuP95", - "formatter": 5 - }, - { - "columnMatch": "excludeRecomm", - "formatter": 5 - }, - { - "columnMatch": "lowCpuThreshold", - "formatter": 5 + "name": "Get-Old-Snapshots" + }, + { + "type": 1, + "content": { + "json": "## Managed Disks snapshots using Premium storage\r\n\r\nTo save 60% of cost, we recommend storing your snapshots in Standard Storage, regardless of the storage type of the parent disk. It is the default option for Managed Disks snapshots. Migrate your snapshot from Premium to Standard Storage.\r\n", + "style": "upsell" }, - { - "columnMatch": "AdditionaInfo", - "formatter": 5, - "formatOptions": { - "customColumnWidthSetting": "19ch" + "name": "text - 5" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == 'microsoft.compute/snapshots'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend StorageSku = tostring(sku.tier), resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup),diskSize=tostring(properties.diskSizeGB)\r\n| where StorageSku == \"Premium\"\r\n| project id,name,StorageSku,diskSize,location,resourceGroup,subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id\r\n", + "size": 0, + "title": "Snapshots using premium storage", + "noDataMessage": "No snapshots are using Premium storage", + "noDataMessageStyle": 3, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "resourceGroup", + "formatter": 14, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "id", + "label": "Resource Id" + }, + { + "columnId": "name", + "label": "Name" + }, + { + "columnId": "StorageSku", + "label": "SKU" + }, + { + "columnId": "diskSize", + "label": "Disk Size (GB)" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Id" + } + ] } }, - { - "columnMatch": "isActive1", - "formatter": 5 - }, - { - "columnMatch": "excludeProperty", - "formatter": 5 - } - ], - "rowLimit": 1000, - "filter": true, - "hierarchySettings": { - "treeType": 1, - "groupBy": ["Recommendation"], - "expandTopLevel": true + "name": "query - Snapshots using premium storage" }, - "labelSettings": [ - { - "columnId": "AffectedResource", - "label": "Affected Resource" + { + "type": 1, + "content": { + "json": "## Orphaned Managed Disks snapshots\r\n\r\nReview snapshots with deleted source disks.\r\n", + "style": "upsell" }, - { - "columnId": "Category", - "label": "Recommendation Category" + "name": "text - 6" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == 'microsoft.compute/snapshots'\r\n| where resourceGroup in ({ResourceGroup})\r\n| extend parentDisk = properties.creationData.sourceResourceId, diskSize=tostring(properties.diskSizeGB),resourceGroup=strcat('/subscriptions/',subscriptionId,'/resourceGroups/',resourceGroup)\r\n| project id,parentDisk,diskSize,location,resourceGroup,subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n )\r\n on id\r\n", + "size": 0, + "title": "All Managed Disks snapshots", + "noDataMessage": "No snapshots found", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "labelSettings": [ + { + "columnId": "id", + "label": "Resource Id" + }, + { + "columnId": "parentDisk", + "label": "Parent Disk Resource Id" + }, + { + "columnId": "diskSize", + "label": "Disk size (GB)" + }, + { + "columnId": "location", + "label": "Location" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription Id" + } + ] + } }, - { - "columnId": "SubCategory", - "label": "Affected Resource Type" + "conditionalVisibility": { + "parameterName": "IsVisible", + "comparison": "isEqualTo", + "value": "true" }, - { - "columnId": "Recommendation", - "label": "Recommendation" + "name": "query - Retrieve all snapshots" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "resources\r\n| where type == 'microsoft.compute/disks'\r\n| project id\r\n", + "size": 0, + "title": "All managed disks", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "labelSettings": [ + { + "columnId": "id", + "label": "Resource Id" + } + ] + } }, - { - "columnId": "Impact", - "label": "Impact" + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "True" }, - { - "columnId": "resourceGroup", - "label": "Resource Group" + "name": "query - Retrieve all managed disks" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\",\"mergeType\":\"leftanti\",\"leftTable\":\"query - Retrieve all snapshots\",\"rightTable\":\"query - Retrieve all managed disks\",\"leftColumn\":\"parentDisk\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[query - Retrieve all snapshots].id\",\"mergedName\":\"Resource Id\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].parentDisk\",\"mergedName\":\"Parent Disk Resource Id\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].diskSize\",\"mergedName\":\"Disk size (GB)\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].location\",\"mergedName\":\"Location\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].subscriptionId\",\"mergedName\":\"Subscription Id\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"},{\"originalName\":\"[query - Retrieve all snapshots].id1\",\"mergedName\":\"id1\",\"fromId\":\"d0a11ffb-579b-4259-827d-7ea62e3021fe\"}]}", + "size": 0, + "title": "Snapshots with deleted source disk", + "noDataMessage": "No orphaned snapshots found", + "noDataMessageStyle": 3, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Parent Disk Resource Id", + "formatter": 5 + }, + { + "columnMatch": "Subscription Id", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id1", + "formatter": 5 + } + ], + "labelSettings": [ + { + "columnId": "Resource Id", + "label": "Resource Id" + }, + { + "columnId": "Parent Disk Resource Id", + "label": "Parent Disk resource Id" + }, + { + "columnId": "Disk size (GB)", + "label": "Disk size (GB)" + }, + { + "columnId": "Location", + "label": "Location" + }, + { + "columnId": "Resource Group", + "label": "Resource Group" + }, + { + "columnId": "Subscription Id", + "label": "Subscription Id" + } + ] + } }, - { - "columnId": "subscriptionId", - "label": "Subscription ID" - } - ] - } - }, - "conditionalVisibility": { - "parameterName": "isVisible", - "comparison": "isEqualTo", - "value": "true" - }, - "name": "Get-AdvisorRecommendations-Storage" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where type has \"Microsoft.Storage\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", - "size": 0, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "showPin": false, + "name": "query - orphaned snapshots" + } + ] }, "conditionalVisibility": { - "parameterName": "isVisible", + "parameterName": "SelectedSubTab", "comparison": "isEqualTo", - "value": "true" + "value": "Disks" }, - "name": "query - tags - list all storageresources" + "name": "Managed Disks Group" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"e84cba0d-e501-4f55-a761-9126fb305030\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Storage\",\"rightTable\":\"query - tags - list all storageresources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Storage].AffectedResource\",\"mergedName\":\"Affected Resource\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].Category\",\"mergedName\":\"Recommendation Category\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].SubCategory\",\"mergedName\":\"Affected Resource Type\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].subscriptionId\",\"mergedName\":\"Subscription ID\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[query - tags - list all storageresources].id\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].excludeProperty\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].isActive1\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].AdditionaInfo\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].lowCpuThreshold\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].excludeRecomm\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].recommendationTypeId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].maxCpuP95\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].stableId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].id\"}]}", - "size": 0, - "title": "Azure Advisor Cost recommendations", - "noDataMessage": "You are following all of our cost recommendations for Storage", - "noDataMessageStyle": 3, - "queryType": 7, - "gridSettings": { - "formatters": [ - { - "columnMatch": "Affected Resource Type", - "formatter": 5 + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "advisorresources\r\n| where type =~ 'microsoft.advisor/recommendations'\r\n| where resourceGroup in ({ResourceGroup})\r\n| where properties.category == 'Cost' and properties.lastUpdated >= ago(1d)\r\n| extend AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=tostring(properties.category), SubCategory=tostring(properties.impactedField), Impact=tostring(properties.impact),resourceGroup,subscriptionId,Recommendation=tostring(properties.shortDescription.problem), id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| where SubCategory has \"Microsoft.Storage\"\r\n| join kind = leftouter\r\n(advisorresources | where type=~'microsoft.advisor/suppressions'\r\n| extend tokens = split(id, '/')\r\n| extend stableId = iff(array_length(tokens) > 3, tokens[(array_length(tokens)-3)], '')\r\n| extend expirationTimeStamp = todatetime(iff(strcmp(tostring(properties.ttl), '-1') == 0, '9999-12-31', properties.expirationTimeStamp))\r\n| where expirationTimeStamp > now()\r\n| project stableId, expirationTimeStamp)\r\non stableId\r\n| where isempty(expirationTimeStamp)\r\n| project AffectedResource=tostring(properties.resourceMetadata.resourceId), Category=tostring(properties.category), SubCategory=tostring(properties.impactedField), Recommendation=tostring(properties.shortDescription.problem), Impact=tostring(properties.impact),resourceGroup,subscriptionId, id, stableId = name, recommendationTypeId = tostring(properties.recommendationTypeId), maxCpuP95 = properties.extendedProperties.MaxCpuP95\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isempty(resourceGroup) == true\r\n| project subscriptionId, excludeRecomm = properties.exclude, lowCpuThreshold = properties.lowCpuThreshold, AffectedResource=tostring(properties.resourceMetadata.resourceId),Impact=properties.impact,resourceGroup,AdditionaInfo=properties.extendedProperties,Recommendation=tostring(properties.shortDescription.problem))\r\non subscriptionId\r\n| extend isActive1 = iff(isnull(excludeRecomm), true, tobool(excludeRecomm) == false)\r\n| extend isActive2 = iff(recommendationTypeId == 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974', iff((isnotempty(lowCpuThreshold) and isnotempty(maxCpuP95)), toint(maxCpuP95) < toint(lowCpuThreshold), iff((isempty(maxCpuP95) or toint(maxCpuP95) < 5), true, false)), true)\r\n| where isActive1 == true and isActive2 == true\r\n| join kind = leftouter\r\n(advisorresources | where type =~ 'microsoft.advisor/configurations' | where isnotempty(resourceGroup) == true\r\n| project subscriptionId, resourceGroup, excludeProperty = properties.exclude)\r\non subscriptionId, resourceGroup\r\n| extend isActive3 = iff(isnull(excludeProperty), true, tobool(excludeProperty) == false)\r\n| where isActive3 == true\r\n| project-away subscriptionId1, subscriptionId2, AffectedResource1, isActive2, isActive3, Impact1, Recommendation1, resourceGroup1, resourceGroup2\r\n| where resourceGroup in ({ResourceGroup})", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Storage", + "noDataMessageStyle": 3, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "gridSettings": { + "formatters": [ + { + "columnMatch": "Group", + "formatter": 1 + }, + { + "columnMatch": "subscriptionId", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "id", + "formatter": 5 + }, + { + "columnMatch": "stableId", + "formatter": 5 + }, + { + "columnMatch": "recommendationTypeId", + "formatter": 5 + }, + { + "columnMatch": "maxCpuP95", + "formatter": 5 + }, + { + "columnMatch": "excludeRecomm", + "formatter": 5 + }, + { + "columnMatch": "lowCpuThreshold", + "formatter": 5 + }, + { + "columnMatch": "AdditionaInfo", + "formatter": 5, + "formatOptions": { + "customColumnWidthSetting": "19ch" + } + }, + { + "columnMatch": "isActive1", + "formatter": 5 + }, + { + "columnMatch": "excludeProperty", + "formatter": 5 + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "AffectedResource", + "label": "Affected Resource" + }, + { + "columnId": "Category", + "label": "Recommendation Category" + }, + { + "columnId": "SubCategory", + "label": "Affected Resource Type" + }, + { + "columnId": "Recommendation", + "label": "Recommendation" + }, + { + "columnId": "Impact", + "label": "Impact" + }, + { + "columnId": "resourceGroup", + "label": "Resource Group" + }, + { + "columnId": "subscriptionId", + "label": "Subscription ID" + } + ] + } }, - { - "columnMatch": "Subscription ID", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "Get-AdvisorRecommendations-Storage" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": " resources\r\n | where resourceGroup in ({ResourceGroup})\r\n | where type has \"Microsoft.Storage\"\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend AffectedResource=id,ResourceRG=resourceGroup\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}'])\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct id\r\n | project id", + "size": 0, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "isVisible", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "query - tags - list all storageresources" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"e84cba0d-e501-4f55-a761-9126fb305030\",\"mergeType\":\"innerunique\",\"leftTable\":\"Get-AdvisorRecommendations-Storage\",\"rightTable\":\"query - tags - list all storageresources\",\"leftColumn\":\"AffectedResource\",\"rightColumn\":\"id\"}],\"projectRename\":[{\"originalName\":\"[Get-AdvisorRecommendations-Storage].AffectedResource\",\"mergedName\":\"Affected Resource\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].Category\",\"mergedName\":\"Recommendation Category\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].SubCategory\",\"mergedName\":\"Affected Resource Type\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].Recommendation\",\"mergedName\":\"Recommendation\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].Impact\",\"mergedName\":\"Impact\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].resourceGroup\",\"mergedName\":\"Resource Group\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].subscriptionId\",\"mergedName\":\"Subscription ID\",\"fromId\":\"e84cba0d-e501-4f55-a761-9126fb305030\"},{\"originalName\":\"[query - tags - list all storageresources].id\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].excludeProperty\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].isActive1\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].AdditionaInfo\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].lowCpuThreshold\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].excludeRecomm\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].recommendationTypeId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].maxCpuP95\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].stableId\"},{\"originalName\":\"[Get-AdvisorRecommendations-Storage].id\"}]}", + "size": 0, + "title": "Azure Advisor Cost recommendations", + "noDataMessage": "You are following all of our cost recommendations for Storage", + "noDataMessageStyle": 3, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "Affected Resource Type", + "formatter": 5 + }, + { + "columnMatch": "Subscription ID", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "rowLimit": 1000, + "filter": true, + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "Recommendation" + ] + } } - } - ], - "rowLimit": 1000, - "filter": true, - "hierarchySettings": { - "treeType": 1, - "groupBy": ["Recommendation"] + }, + "showPin": false, + "name": "query - Merge - Storage Advisor recommendations" } - } + ] + }, + "conditionalVisibility": { + "parameterName": "SelectedSubTab", + "comparison": "isEqualTo", + "value": "advisorStorage" }, - "showPin": false, - "name": "query - Merge - Storage Advisor recommendations" + "name": "AdvisorGroupStorage" } ] }, - "conditionalVisibility": { - "parameterName": "SelectedSubTab", - "comparison": "isEqualTo", - "value": "advisorStorage" - }, - "name": "AdvisorGroupStorage" + "name": "group - 0" } ] }, @@ -3755,7 +4851,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "94bd2bd0-5aa8-4df6-8cf7-603407f4e2d8", @@ -3767,7 +4865,9 @@ "quote": "'", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "includeAll": false, "showDefault": false }, @@ -3788,9 +4888,13 @@ "quote": "'", "delimiter": ",", "query": "resources\r\n| distinct resourceGroup", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "defaultValue": "value::all", @@ -3804,7 +4908,9 @@ "type": 1, "isRequired": true, "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "isHiddenWhenLocked": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", @@ -3816,7 +4922,9 @@ "name": "TagName", "type": 2, "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -3831,7 +4939,9 @@ "name": "TagValue", "type": 2, "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -3864,7 +4974,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "1fc44b9a-2dd3-4b1f-bebd-b89d4ba6dfec", @@ -3872,9 +4984,13 @@ "name": "Location", "type": 2, "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::1"] + "additionalResourceOptions": [ + "value::1" + ] }, "timeContext": { "durationMs": 86400000 @@ -3964,7 +5080,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -4000,7 +5118,9 @@ "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -4044,7 +5164,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -4103,7 +5225,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["Recommendation"], + "groupBy": [ + "Recommendation" + ], "expandTopLevel": true }, "labelSettings": [ @@ -4153,7 +5277,9 @@ "size": 0, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibility": { "parameterName": "isVisible", @@ -4243,7 +5369,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["Recommendation"] + "groupBy": [ + "Recommendation" + ] } } }, @@ -4281,7 +5409,9 @@ "size": 0, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -4456,7 +5586,9 @@ "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["value::all"], + "crossComponentResources": [ + "value::all" + ], "visualization": "piechart", "chartSettings": { "seriesLabelSettings": [ @@ -4485,7 +5617,9 @@ "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["value::all"], + "crossComponentResources": [ + "value::all" + ], "visualization": "piechart", "chartSettings": { "seriesLabelSettings": [ @@ -4572,7 +5706,9 @@ "quote": "'", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "includeAll": false, "showDefault": false }, @@ -4581,7 +5717,9 @@ }, "defaultValue": "value::all", "label": " Subscription", - "value": ["value::all"] + "value": [ + "value::all" + ] } ], "style": "pills", @@ -4595,7 +5733,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "2b8ca845-75ba-4f4b-acad-54ee50d66d54", @@ -4641,15 +5781,21 @@ "quote": "'", "delimiter": ",", "query": "AdvisorResources \r\n| where type == 'microsoft.advisor/recommendations' \r\n| where properties.category == 'Cost' and properties.shortDescription.solution contains \"Reserved Instance\"\r\n| extend reservedResourceType=tostring(properties.extendedProperties.reservedResourceType)\r\n| distinct reservedResourceType", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "defaultValue": "value::all", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "value": ["value::all"] + "value": [ + "value::all" + ] } ], "style": "pills", @@ -4669,7 +5815,9 @@ "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "categoricalbar", "gridSettings": { "filter": true, @@ -4702,7 +5850,9 @@ }, "chartSettings": { "xAxis": "reservedResourceType", - "yAxis": ["sum_savings"], + "yAxis": [ + "sum_savings" + ], "group": "reservedResourceType", "createOtherGroup": 0, "showLegend": true, @@ -4730,7 +5880,9 @@ "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -4761,7 +5913,10 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["subscription", "reservedResourceType"], + "groupBy": [ + "subscription", + "reservedResourceType" + ], "expandTopLevel": false }, "labelSettings": [ @@ -4801,7 +5956,9 @@ }, "chartSettings": { "xAxis": "reservedResourceType", - "yAxis": ["sum_savings"], + "yAxis": [ + "sum_savings" + ], "group": "reservedResourceType", "createOtherGroup": 0, "showLegend": true, @@ -4864,7 +6021,9 @@ "quote": "'", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "includeAll": false, "showDefault": false }, @@ -4873,7 +6032,9 @@ }, "defaultValue": "value::all", "label": " Subscription", - "value": ["value::all"] + "value": [ + "value::all" + ] } ], "style": "pills", @@ -4887,7 +6048,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "2b8ca845-75ba-4f4b-acad-54ee50d66d54", @@ -4940,14 +6103,18 @@ "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "categoricalbar", "gridSettings": { "filter": true }, "chartSettings": { "xAxis": "name", - "yAxis": ["sum_savings"], + "yAxis": [ + "sum_savings" + ], "group": "reservedResourceType", "createOtherGroup": 0, "showLegend": true, @@ -4975,7 +6142,9 @@ "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -5014,7 +6183,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["id"] + "groupBy": [ + "id" + ] }, "labelSettings": [ { @@ -5041,7 +6212,9 @@ }, "chartSettings": { "xAxis": "reservedResourceType", - "yAxis": ["sum_savings"], + "yAxis": [ + "sum_savings" + ], "group": "reservedResourceType", "createOtherGroup": 0, "showLegend": true, @@ -5089,7 +6262,9 @@ "quote": "'", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "includeAll": false, "showDefault": false }, @@ -5098,7 +6273,9 @@ }, "defaultValue": "value::all", "label": " Subscription", - "value": ["value::all"] + "value": [ + "value::all" + ] }, { "id": "03fbf28a-892d-4b68-929c-3ba5056f4b94", @@ -5111,9 +6288,13 @@ "quote": "'", "delimiter": ",", "query": "resources\r\n| distinct resourceGroup", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "showDefault": false }, "defaultValue": "value::all", @@ -5127,7 +6308,9 @@ "type": 1, "isRequired": true, "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "isHiddenWhenLocked": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", @@ -5139,7 +6322,9 @@ "name": "TagName", "type": 2, "query": "Resources\r\n| where tags != '' and tags != '[]'\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| distinct tagName\r\n| sort by tagName asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -5154,7 +6339,9 @@ "name": "TagValue", "type": 2, "query": "Resources\r\n| mvexpand tags\r\n| extend tagName = tostring(bag_keys(tags)[0])\r\n| extend tagValue = tostring(tags[tagName])\r\n| where tags != '' and tags != '[]' and tostring(bag_keys(tags)[0]) == '{TagName}'\r\n| distinct tagValue\r\n| sort by tagValue asc", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { "additionalResourceOptions": [] }, @@ -5180,7 +6367,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "f74bc7f5-2b16-4440-8053-106e040b73b6", @@ -5188,9 +6377,13 @@ "name": "Location", "type": 2, "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project name, location\r\n| summarize count () by location\r\n| project location", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "typeSettings": { - "additionalResourceOptions": ["value::1"] + "additionalResourceOptions": [ + "value::1" + ] }, "timeContext": { "durationMs": 86400000 @@ -5233,7 +6426,9 @@ "type": 9, "content": { "version": "KqlParameterItem/1.0", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "parameters": [ { "id": "0c58188b-5c09-45aa-b738-f7122d0e0a19", @@ -5244,7 +6439,9 @@ "description": "Select the region where the VMs are located. Different Regions might have different SKUs", "isRequired": true, "query": "Resources\r\n| where type =~ 'Microsoft.Compute/virtualMachines'\r\n| project location\r\n| take 1\r\n\r\n", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "isHiddenWhenLocked": true, "timeContext": { "durationMs": 86400000 @@ -5259,7 +6456,9 @@ "type": 1, "isRequired": true, "query": "resourcecontainers\r\n| where type==\"microsoft.resources/subscriptions\"\r\n| take 1\r\n| project subscriptionId", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "isHiddenWhenLocked": true, "timeContext": { "durationMs": 86400000 @@ -5381,14 +6580,16 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets'\r\n| where tostring(properties.storageProfile.imageReference.publisher ) == \"MicrosoftWindowsServer\" or tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType) == 'Windows'\r\n| where tostring(properties.['licenseType']) !has 'Windows' and tostring(properties.virtualMachineProfile.['licenseType']) !has 'Windows'\r\n| extend WindowsId=id, VMIDFull=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.storageProfile.imageReference.offer), OsVersion = tostring(properties.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.['licenseType']), VMSSize=tostring(sku.name), QuickFix=id\r\n ) on subscriptionId \r\n| order by type asc \r\n| project WindowsId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OsVersion,LicenseType, subscriptionId, QuickFix, VMIDFull\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines' or type =~ 'microsoft.compute/virtualMachineScaleSets'\r\n| where tostring(properties.storageProfile.imageReference.publisher ) == \"MicrosoftWindowsServer\" or tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType) == 'Windows' or tostring(properties.storageProfile.imageReference.publisher ) == \"microsoftsqlserver\"\r\n| where tostring(properties.['licenseType']) !has 'Windows' and tostring(properties.virtualMachineProfile.['licenseType']) != 'Windows_Server'\r\n| extend WindowsId=id, VMIDFull=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.storageProfile.imageReference.offer), OsVersion = tostring(properties.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.['licenseType']), VMSSize=tostring(sku.name), QuickFix=id\r\n ) on subscriptionId \r\n| order by type asc \r\n| project WindowsId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OsVersion,LicenseType, subscriptionId, QuickFix, VMIDFull\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId", "size": 0, "title": "AHB Disabled", "noDataMessage": "All of your VMs have AHB enabled.", "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -5413,14 +6614,16 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines'\r\n| where tostring(properties.storageProfile.imageReference.publisher ) == \"MicrosoftWindowsServer\"\r\n| where tostring(properties.['licenseType']) has \"Windows\"\r\n| extend WindowsId=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.storageProfile.imageReference.offer), OsVersion = tostring(properties.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.['licenseType']), VMSSize=tostring(sku.name)\r\n) on subscriptionId \r\n| order by type asc \r\n| project WindowsId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OsVersion,LicenseType, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources \r\n| where resourceGroup in ({ResourceGroup})\r\n| where type =~ 'microsoft.compute/virtualmachines'\r\n| where tostring(properties.storageProfile.imageReference.publisher ) == \"MicrosoftWindowsServer\" or tostring(properties.virtualMachineProfile.storageProfile.osDisk.osType) == 'Windows' or tostring(properties.storageProfile.imageReference.publisher ) == \"microsoftsqlserver\"\r\n| where tostring(properties.['licenseType']) has \"Windows\"\r\n| extend WindowsId=id, VMName=name, VMLocation=location, VMRG=resourceGroup, OSType=tostring(properties.storageProfile.imageReference.offer), OsVersion = tostring(properties.storageProfile.imageReference.sku), VMSize=tostring (properties.hardwareProfile.vmSize), LicenseType = tostring(properties.['licenseType']), VMSSize=tostring(sku.name)\r\n) on subscriptionId \r\n| order by type asc \r\n| project WindowsId,VMName,VMRG,VMSize, VMSSize, VMLocation,OSType, OsVersion,LicenseType, subscriptionId\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), WindowsId=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct WindowsId\r\n )\r\n on WindowsId", "size": 0, "title": "AHB Enabled", "noDataMessage": "None of your VMs have AHB enabled.", "noDataMessageStyle": 4, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "gridSettings": { "formatters": [ { @@ -5458,7 +6661,9 @@ "noDataMessage": "AHB was not enabled in the last 7 days.", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibility": { "parameterName": "AlwaysHidden", @@ -5518,7 +6723,9 @@ "showExportToExcel": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "table", "gridSettings": { "sortBy": [ @@ -5584,7 +6791,9 @@ "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart" }, "customWidth": "50", @@ -5652,7 +6861,9 @@ ] }, "chartSettings": { - "yAxis": ["Consumed Cores per VM"], + "yAxis": [ + "Consumed Cores per VM" + ], "group": "Prioritize AHB?", "createOtherGroup": null } @@ -5696,7 +6907,9 @@ "type": 0 }, "chartSettings": { - "yAxis": ["ConsumedCores"], + "yAxis": [ + "ConsumedCores" + ], "group": "VMName", "createOtherGroup": null } @@ -5748,7 +6961,9 @@ ] }, "chartSettings": { - "yAxis": ["Consumed Cores per VM"], + "yAxis": [ + "Consumed Cores per VM" + ], "group": "Prioritize AHB?", "createOtherGroup": null } @@ -5955,7 +7170,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true }, "labelSettings": [ @@ -6109,7 +7326,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true }, "labelSettings": [ @@ -6161,7 +7380,9 @@ }, "chartSettings": { "xAxis": "VM Name", - "yAxis": ["Consumed Cores per VM"], + "yAxis": [ + "Consumed Cores per VM" + ], "group": null, "createOtherGroup": 0, "seriesLabelSettings": [ @@ -6213,7 +7434,9 @@ }, "chartSettings": { "xAxis": "VM Name", - "yAxis": ["Consumed Cores per VM"], + "yAxis": [ + "Consumed Cores per VM" + ], "group": null, "createOtherGroup": 0, "seriesLabelSettings": [ @@ -6303,14 +7526,16 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) != 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", "size": 0, "title": "SQL VM AHB Disabled", "noDataMessage": "All of your VMs have AHB enabled.", "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibility": { "parameterName": "AlwaysHidden", @@ -6323,14 +7548,16 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) == 'AHB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and tostring(properties.['sqlServerLicenseType']) == 'AHUB' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n ) on subscriptionId \r\n| join (\r\n resources\r\n | where type =~ 'Microsoft.Compute/virtualmachines'\r\n | project VMName = tolower(name), VMSize = tostring(properties.hardwareProfile.vmSize)\r\n ) on VMName\r\n| order by id asc \r\n| project SQLID,VMName,VMRG, VMLocation, VMSize, SQLVersion, SQLSKU, SQLAgentType, LicenseType, SubscriptionName\r\n| where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID", "size": 0, "title": "SQL VM AHB Enabled", "noDataMessage": "None of your VMs have AHB enabled.", "noDataMessageStyle": 5, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibility": { "parameterName": "AlwaysHidden", @@ -6378,13 +7605,15 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n | where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID\r\n | extend CheckSQLVMAHB = case(\r\n type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines', iif((properties.['sqlServerLicenseType'])\r\n !has 'AHB', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by SubscriptionName, CheckSQLVMAHB", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n | where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID\r\n | extend CheckSQLVMAHB = case(\r\n type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines', iif((properties.['sqlServerLicenseType'])\r\n !has 'AHUB', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by SubscriptionName, CheckSQLVMAHB", "size": 0, "title": "Summary of SQL on VMs with or without AHB per Subscription", "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "table", "gridSettings": { "formatters": [ @@ -6461,14 +7690,16 @@ "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n | where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID\r\n | extend CheckSQLVMAHB = case(\r\n type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines', iif((properties.['sqlServerLicenseType'])\r\n !has 'AHB', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by CheckSQLVMAHB", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines' and resourceGroup in ({ResourceGroup})\r\n | extend SQLID=id, VMName = name, VMRG = resourceGroup, VMLocation = location, LicenseType = tostring(properties.['sqlServerLicenseType']), OSType=tostring(properties.storageProfile.imageReference.offer), SQLAgentType = tostring(properties.['sqlManagement']), SQLVersion = tostring(properties.['sqlImageOffer']), SQLSKU=tostring(properties.['sqlImageSku'])\r\n | where SQLSKU != \"Developer\" and SQLSKU != \"Express\"\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLID\r\n )\r\n on SQLID\r\n | extend CheckSQLVMAHB = case(\r\n type =~ 'Microsoft.SqlVirtualMachine/SqlVirtualMachines', iif((properties.['sqlServerLicenseType'])\r\n !has 'AHUB', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by CheckSQLVMAHB", "size": 0, "title": "Summary SQL Enabled and Disabled", "noDataMessage": "You don't have any SQL VM", "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart" }, "customWidth": "50", @@ -6536,7 +7767,9 @@ ] }, "chartSettings": { - "yAxis": ["Consumed Cores"], + "yAxis": [ + "Consumed Cores" + ], "group": "VMName", "createOtherGroup": null } @@ -6641,7 +7874,9 @@ }, "sortBy": [], "chartSettings": { - "yAxis": ["Consumed Cores"], + "yAxis": [ + "Consumed Cores" + ], "showMetrics": false, "showLegend": true } @@ -6760,7 +7995,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["SubscriptionName"], + "groupBy": [ + "SubscriptionName" + ], "expandTopLevel": true }, "labelSettings": [ @@ -6890,7 +8127,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["SubscriptionName"], + "groupBy": [ + "SubscriptionName" + ], "expandTopLevel": true }, "labelSettings": [ @@ -6972,42 +8211,6 @@ "groupType": "editable", "title": "SQL Database", "items": [ - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and tostring(properties.['licenseType']) == 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])\r\n| extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/databases', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n )\r\n) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, CheckSQLDBAHB,SQLLocation, LicenseType, StorageAccountType, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n", - "size": 0, - "title": "AHB Disabled", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] - }, - "conditionalVisibility": { - "parameterName": "AlwaysHidden", - "comparison": "isEqualTo", - "value": "true" - }, - "name": "SQLDB AHB Disabled" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and tostring(properties.['licenseType']) != 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=sku.name, SKUTier=sku.tier, SQLLocation = location, vCores=tostring(sku.capacity), LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, SQLLocation, LicenseType, StorageAccountType, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n\r\n", - "size": 0, - "title": "AHB Enabled", - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] - }, - "conditionalVisibility": { - "parameterName": "AlwaysHidden", - "comparison": "isEqualTo", - "value": "true" - }, - "name": "SQLDB AHB Enabled" - }, { "type": 1, "content": { @@ -7016,401 +8219,677 @@ "name": "SQL Databases AHB" }, { - "type": 1, - "content": { - "json": "Apply to SQL Server 1 to 4 vCPUs exchange: For every 1 core of SQL Server Enterprise Edition, you get 4 vCPUs of SQL Managed Instance or Azure SQL Database general purpose and Hyperscale tiers, or 4 vCPUs of SQL Server Standard edition on Azure VMs.\r\n\r\nThe SQL virtual machines (VMs) with less than 4 cores are categorized as **Low Priority**, while those with 8 or more cores are classified as **High Priority**. In situations where there are insufficient Azure Hybrid benefit licenses to cover all the VMs in the environment, it is recommended to prioritize the High Priority VMs.\r\n\r\nFor Azure SQL Database, Azure Hybrid Benefit is only available when using the provisioned compute tier of the vCore-based purchasing model. Azure Hybrid Benefit doesn't apply to DTU-based purchasing models or the serverless compute tier.", - "style": "info" - }, - "name": "Apply to SQL Server 1 to 4 vCPUs " - }, - { - "type": 1, + "type": 11, "content": { - "json": "### AHB Overview\r\nSummary of all SQL Databases with and without SQL AHB.", - "style": "info" + "version": "LinkItem/1.0", + "style": "tabs", + "links": [ + { + "id": "e4aa368f-dcf2-44a6-88f9-a395c04eb21f", + "cellValue": "SQLType", + "linkTarget": "parameter", + "linkLabel": "SQL Database", + "subTarget": "SQLDatabase", + "style": "link" + }, + { + "id": "a94e8dc2-34be-4d97-934d-c27e1816c4fe", + "cellValue": "SQLType", + "linkTarget": "parameter", + "linkLabel": "SQL ElasticPool", + "subTarget": "SQLElastic", + "style": "link" + } + ] }, - "name": " AHB Overview SQL DB" + "name": "links - 8" }, { "type": 12, "content": { "version": "NotebookGroup/1.0", "groupType": "editable", - "loadType": "explicit", - "loadButtonText": "Load SQL DB Info", "items": [ + { + "type": 1, + "content": { + "json": "SQLDB" + }, + "name": "text - 0" + }, { "type": 3, "content": { "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and kind contains 'vcore' and kind !contains \"serverless\"\r\n | extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=sku.name, SKUTier=sku.tier, SQLLocation = location, LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/databases', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by SubscriptionName, CheckSQLDBAHB", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and tostring(properties.['licenseType']) == 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\" and tostring(sku.name) != \"ElasticPool\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])\r\n| extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/databases', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n )\r\n) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, CheckSQLDBAHB,SQLLocation, LicenseType, StorageAccountType, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n", "size": 0, - "title": "Summary of SQL Databases with or without AHB per Subscription", - "showRefreshButton": true, + "title": "AHB Disabled", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "visualization": "table", - "gridSettings": { - "formatters": [ - { - "columnMatch": "Prioritize AHB?", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ - { - "operator": "==", - "thresholdValue": "High Priority", - "representation": "Sev0", - "text": "{0}{1}" - }, - { - "operator": "==", - "thresholdValue": "Low Priority", - "representation": "2", - "text": "{0}{1}" - }, - { - "operator": "Default", - "thresholdValue": null, - "representation": "unknown", - "text": "{0}{1}" - } - ] - } - } - ], - "labelSettings": [ - { - "columnId": "SubscriptionName", - "label": "Subscription Name" - }, - { - "columnId": "CheckSQLDBAHB", - "label": "Is AHB enabled?" - }, - { - "columnId": "count_", - "label": "Number of resources" - } - ] - }, - "tileSettings": { - "titleContent": { - "columnMatch": "CheckSQLDBAHB", - "formatter": 1 - }, - "subtitleContent": { - "columnMatch": "SubscriptionName", - "formatter": 1 - }, - "leftContent": { - "columnMatch": "count_", - "formatter": 12, - "formatOptions": { - "palette": "auto" - } - }, - "showBorder": false, - "size": "auto" - }, - "chartSettings": { - "xAxis": "SubscriptionName" - } + "crossComponentResources": [ + "{Subscription}" + ] }, - "customWidth": "50", - "name": "Summary of SQL DBs with or without AHB per subs" + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLDB AHB Disabled" }, { "type": 3, "content": { - "version": "KqlItem/1.0", - "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and kind contains 'vcore' and kind !contains \"serverless\"\r\n | extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=sku.name, SKUTier=sku.tier, SQLLocation = location, LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/databases', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by CheckSQLDBAHB", - "size": 0, - "title": "Summary of SQL Databases with or without AHB", - "showRefreshButton": true, - "queryType": 1, - "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], - "visualization": "piechart", - "gridSettings": { - "formatters": [ - { - "columnMatch": "Prioritize AHB?", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ - { - "operator": "==", - "thresholdValue": "High Priority", - "representation": "Sev0", - "text": "{0}{1}" - }, - { - "operator": "==", - "thresholdValue": "Low Priority", - "representation": "2", - "text": "{0}{1}" - }, - { - "operator": "Default", - "thresholdValue": null, - "representation": "unknown", - "text": "{0}{1}" - } - ] - } - } - ] - }, - "tileSettings": { - "titleContent": { - "columnMatch": "CheckSQLDBAHB", - "formatter": 1 - }, - "subtitleContent": { - "columnMatch": "SubscriptionName", - "formatter": 1 - }, - "leftContent": { - "columnMatch": "count_", - "formatter": 12, - "formatOptions": { - "palette": "auto" - } - }, - "showBorder": false, - "size": "auto" - }, - "chartSettings": { - "xAxis": "SubscriptionName" - } + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and tostring(properties.['licenseType']) != 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\" and tostring(sku.name) != \"ElasticPool\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=sku.name, SKUTier=sku.tier, SQLLocation = location, vCores=tostring(sku.capacity), LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, SQLLocation, LicenseType, StorageAccountType, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n\r\n", + "size": 0, + "title": "AHB Enabled", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] }, - "customWidth": "50", - "name": "Summary of SQL DBs with or without AHB " + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLDB AHB Enabled" }, { "type": 1, "content": { - "json": "### Consumed Licenses\r\nTotal number of SQL licenses cores consumed by all SQL Databases\r\n", + "json": "Apply to SQL Server 1 to 4 vCPUs exchange: For every 1 core of SQL Server Enterprise Edition, you get 4 vCPUs of SQL Managed Instance or Azure SQL Database general purpose and Hyperscale tiers, or 4 vCPUs of SQL Server Standard edition on Azure VMs.\r\n\r\nThe SQL virtual machines (VMs) with less than 4 cores are categorized as **Low Priority**, while those with 8 or more cores are classified as **High Priority**. In situations where there are insufficient Azure Hybrid benefit licenses to cover all the VMs in the environment, it is recommended to prioritize the High Priority VMs.\r\n\r\nFor Azure SQL Database, Azure Hybrid Benefit is only available when using the provisioned compute tier of the vCore-based purchasing model. Azure Hybrid Benefit doesn't apply to DTU-based purchasing models or the serverless compute tier.", "style": "info" }, - "customWidth": "50", - "name": "Total number of SQL licenses cores consumed" + "name": "Apply to SQL Server 1 to 4 vCPUs " }, { "type": 1, "content": { - "json": "### Number of required Cores to enable SQL Azure Hybrid Benefit\r\nNumber of cores required to enable SQL AHB across the entire environment.\r\n\r\n\r\n", + "json": "### AHB Overview\r\nSummary of all SQL Databases with and without SQL AHB.", "style": "info" }, - "customWidth": "50", - "name": "Text SQL DB" + "name": " AHB Overview SQL DB" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", - "size": 0, - "title": "SQL DB AHB Consumed Cores per VM", - "noDataMessage": "None of your SQL DB have AHB enabled.", - "noDataMessageStyle": 4, - "showRefreshButton": true, - "queryType": 7, - "visualization": "piechart", - "gridSettings": { - "formatters": [ - { - "columnMatch": "Prioritize AHB?", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "version": "NotebookGroup/1.0", + "groupType": "editable", + "loadType": "explicit", + "loadButtonText": "Load SQL DB Info", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and kind contains 'vcore' and kind !contains \"serverless\" and tostring(sku.name) != \"ElasticPool\"\r\n | extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=sku.name, SKUTier=sku.tier, SQLLocation = location, LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/databases', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by SubscriptionName, CheckSQLDBAHB", + "size": 0, + "title": "Summary of SQL Databases with or without AHB per Subscription", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ { - "operator": "==", - "thresholdValue": "High Priority", - "representation": "Sev0", - "text": "{0}{1}" + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ], + "labelSettings": [ + { + "columnId": "SubscriptionName", + "label": "Subscription Name" }, { - "operator": "==", - "thresholdValue": "Low Priority", - "representation": "2", - "text": "{0}{1}" + "columnId": "CheckSQLDBAHB", + "label": "Is AHB enabled?" }, { - "operator": "Default", - "thresholdValue": null, - "representation": "unknown", - "text": "{0}{1}" + "columnId": "count_", + "label": "Number of resources" + } + ] + }, + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLDBAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" + } + }, + "customWidth": "50", + "name": "Summary of SQL DBs with or without AHB per subs" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\n resources | where type =~ 'Microsoft.Sql/servers/databases' and name != 'master' and kind contains 'vcore' and kind !contains \"serverless\" and tostring(sku.name) != \"ElasticPool\"\r\n | extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=sku.name, SKUTier=sku.tier, SQLLocation = location, LicenseType = tostring(properties.['licenseType']), StorageAccountType=tostring(properties.['storageAccountType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/databases', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by CheckSQLDBAHB", + "size": 0, + "title": "Summary of SQL Databases with or without AHB", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLDBAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" + } + }, + "customWidth": "50", + "name": "Summary of SQL DBs with or without AHB " + }, + { + "type": 1, + "content": { + "json": "### Consumed Licenses\r\nTotal number of SQL licenses cores consumed by all SQL Databases\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Total number of SQL licenses cores consumed" + }, + { + "type": 1, + "content": { + "json": "### Number of required Cores to enable SQL Azure Hybrid Benefit\r\nNumber of cores required to enable SQL AHB across the entire environment.\r\n\r\n\r\n", + "style": "info" + }, + "customWidth": "50", + "name": "Text SQL DB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Consumed Cores per VM", + "noDataMessage": "None of your SQL DB have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores" + ], + "group": "SQLName", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLDB+SKU AHB Enabled - per VM" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Consumed Cores per Priority", + "noDataMessage": "None of your SQL DB have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } } ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null } + }, + "customWidth": "33", + "name": "Summary SQLDB+SKU AHB Enabled - per Priority" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Cores not enabled per AHB Priority", + "noDataMessage": "All of your SQL DB have AHB enabled.", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "Consumed Cores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } + }, + "customWidth": "33", + "name": "Summary SQLDB+SKU AHB Disabled - per Priority" + } + ] + }, + "name": "SQL DB Info" + }, + { + "type": 9, + "content": { + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", + "version": "KqlParameterItem/1.0", + "name": "SQLDBHUBEnabled", + "label": "See SQL DBs with AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 } - ] - }, - "chartSettings": { - "yAxis": ["Consumed Cores"], - "group": "SQLName", - "createOtherGroup": null - } + }, + { + "id": "f1ac5e53-253c-4afb-8bc5-b1ba2efea3eb", + "version": "KqlParameterItem/1.0", + "name": "SQLDBAHBDisabled", + "label": "See SQL DBs without AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false + }, + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" }, - "customWidth": "33", - "name": "Summary SQLDB+SKU AHB Enabled - per VM" + "name": "SQL DB Without AHB" }, { - "type": 3, + "type": 12, "content": { - "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", - "size": 0, - "title": "SQL DB AHB Consumed Cores per Priority", - "noDataMessage": "None of your SQL DB have AHB enabled.", - "noDataMessageStyle": 4, - "showRefreshButton": true, - "queryType": 7, - "visualization": "piechart", - "gridSettings": { - "formatters": [ - { - "columnMatch": "Prioritize AHB?", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071ed\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Disabled", + "noDataMessage": "All of your SQL DBs have AHB enabled.", + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ { - "operator": "==", - "thresholdValue": "High Priority", - "representation": "Sev0", - "text": "{0}{1}" + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } }, { - "operator": "==", - "thresholdValue": "Low Priority", - "representation": "2", - "text": "{0}{1}" + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } }, { - "operator": "Default", - "thresholdValue": null, - "representation": "unknown", - "text": "{0}{1}" + "columnMatch": "SubscriptionName", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "SubscriptionName" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "SQLDBID", + "label": "Database Name" + }, + { + "columnId": "SQLName", + "label": "Server Name" + }, + { + "columnId": "SQLRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "vCores", + "label": "Number of vCore" + }, + { + "columnId": "CheckSQLDBAHB", + "label": "Is AHB enabled?" + }, + { + "columnId": "SQLLocation", + "label": "Location" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "StorageAccountType", + "label": "Storage Account Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "SQLDBID1", + "label": "Resource ID" } ] } - } - ] - }, - "chartSettings": { - "yAxis": ["Consumed Cores"], - "group": "Prioritize AHB?", - "createOtherGroup": null - } - }, - "customWidth": "33", - "name": "Summary SQLDB+SKU AHB Enabled - per Priority" - }, - { - "type": 3, - "content": { - "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", - "size": 0, - "title": "SQL DB AHB Cores not enabled per AHB Priority", - "noDataMessage": "All of your SQL DB have AHB enabled.", - "noDataMessageStyle": 3, - "showRefreshButton": true, - "queryType": 7, - "visualization": "piechart", - "gridSettings": { - "formatters": [ - { - "columnMatch": "Prioritize AHB?", - "formatter": 18, - "formatOptions": { - "thresholdsOptions": "icons", - "thresholdsGrid": [ + }, + "conditionalVisibility": { + "parameterName": "SQLDBAHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL DB Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071f9\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "size": 0, + "title": "SQL DB AHB Enabled", + "noDataMessage": "None of you SQL DBs have AHB enabled.", + "noDataMessageStyle": 4, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "labelSettings": [ { - "operator": "==", - "thresholdValue": "High Priority", - "representation": "Sev0", - "text": "{0}{1}" + "columnId": "SQLDBID", + "label": "Name" }, { - "operator": "==", - "thresholdValue": "Low Priority", - "representation": "2", - "text": "{0}{1}" + "columnId": "SQLName", + "label": "Database Name" }, { - "operator": "Default", - "thresholdValue": null, - "representation": "unknown", - "text": "{0}{1}" + "columnId": "SQLRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "SQLLocation", + "label": "Location" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "StorageAccountType", + "label": "Storage Account Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "vCores", + "label": "Number of vCore" + }, + { + "columnId": "SQLDBID1", + "label": "Resource ID" } ] } - } - ] - }, - "chartSettings": { - "yAxis": ["Consumed Cores"], - "group": "Prioritize AHB?", - "createOtherGroup": null - } + }, + "conditionalVisibility": { + "parameterName": "SQLDBHUBEnabled", + "comparison": "isEqualTo", + "value": "Yes" + }, + "name": "SQL DB AHB Enabled" + } + ] }, - "customWidth": "33", - "name": "Summary SQLDB+SKU AHB Disabled - per Priority" + "name": "Load SQL DB Detailed Info" } ] }, - "name": "SQL DB Info" - }, - { - "type": 9, - "content": { - "version": "KqlParameterItem/1.0", - "parameters": [ - { - "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", - "version": "KqlParameterItem/1.0", - "name": "SQLDBHUBEnabled", - "label": "See SQL DBs with AHB", - "type": 2, - "isRequired": true, - "typeSettings": { - "additionalResourceOptions": [], - "showDefault": false - }, - "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", - "timeContext": { - "durationMs": 86400000 - }, - "value": "Yes" - }, - { - "id": "f1ac5e53-253c-4afb-8bc5-b1ba2efea3eb", - "version": "KqlParameterItem/1.0", - "name": "SQLDBAHBDisabled", - "label": "See SQL DBs without AHB", - "type": 2, - "isRequired": true, - "typeSettings": { - "additionalResourceOptions": [], - "showDefault": false - }, - "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", - "value": "Yes" - } - ], - "style": "pills", - "queryType": 0, - "resourceType": "microsoft.operationalinsights/workspaces" + "conditionalVisibility": { + "parameterName": "SQLType", + "comparison": "isEqualTo", + "value": "SQLDatabase" }, - "name": "SQL DB Without AHB" + "name": "SQLDatabase" }, { "type": 12, @@ -7418,177 +8897,639 @@ "version": "NotebookGroup/1.0", "groupType": "editable", "items": [ + { + "type": 1, + "content": { + "json": "SQL Elastic Pool" + }, + "name": "text - 0" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources\r\n| where type =~ 'Microsoft.Sql/servers/elasticPools' and tostring(properties.['licenseType']) == 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType'])\r\n| extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/elasticPools', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n )\r\n) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, CheckSQLDBAHB,SQLLocation, LicenseType, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n", + "size": 0, + "title": "AHB Disabled", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLElastic AHB Disabled" + }, { "type": 3, "content": { "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071ed\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\"\r\n| extend SubscriptionName=name | join (resources\r\n| where type =~ 'Microsoft.Sql/servers/elasticPools' and tostring(properties.['licenseType']) != 'LicenseIncluded' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType'])\r\n| extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/elasticPools', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n\"Not SQL DB\"\r\n ) \r\n ) on subscriptionId \r\n| project SQLDBID,SQLName,SQLRG, SKUName, SKUTier, vCores, SQLLocation, LicenseType, CheckSQLDBAHB, SubscriptionName\r\n| join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n\r\n", "size": 0, - "title": "SQL DB AHB Disabled", - "noDataMessage": "All of your SQL DBs have AHB enabled.", - "showExportToExcel": true, - "queryType": 7, - "gridSettings": { - "formatters": [ - { - "columnMatch": "$gen_group", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true + "title": "AHB Enabled", + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ] + }, + "conditionalVisibility": { + "parameterName": "AlwaysHidden", + "comparison": "isEqualTo", + "value": "true" + }, + "name": "SQLElastic AHB Enabled" + }, + { + "type": 1, + "content": { + "json": "Apply to SQL Server 1 to 4 vCPUs exchange: For every 1 core of SQL Server Enterprise Edition, you get 4 vCPUs of SQL Managed Instance or Azure SQL Database general purpose and Hyperscale tiers, or 4 vCPUs of SQL Server Standard edition on Azure VMs.\r\n\r\nThe SQL virtual machines (VMs) with less than 4 cores are categorized as **Low Priority**, while those with 8 or more cores are classified as **High Priority**. In situations where there are insufficient Azure Hybrid benefit licenses to cover all the VMs in the environment, it is recommended to prioritize the High Priority VMs.\r\n\r\nFor Azure SQL Database, Azure Hybrid Benefit is only available when using the provisioned compute tier of the vCore-based purchasing model. Azure Hybrid Benefit doesn't apply to DTU-based purchasing models or the serverless compute tier.", + "style": "info" + }, + "name": "Apply to SQL Elastic Server 1 to 4 vCPUs " + }, + { + "type": 1, + "content": { + "json": "### AHB Overview\r\nSummary of all SQL Databases with and without SQL AHB.", + "style": "info" + }, + "name": " AHB Overview SQL Elastic" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "loadType": "explicit", + "loadButtonText": "Load SQL DB Info", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources\r\n| where type =~ 'Microsoft.Sql/servers/elasticPools' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/elasticPools', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by SubscriptionName, CheckSQLDBAHB", + "size": 0, + "title": "Summary of SQL Databases with or without AHB per Subscription", + "showRefreshButton": true, + "showExportToExcel": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "table", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ], + "labelSettings": [ + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "CheckSQLDBAHB", + "label": "Is AHB enabled?" + }, + { + "columnId": "count_", + "label": "Number of resources" + } + ] + }, + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLDBAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" } }, - { - "columnMatch": "Group", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true + "customWidth": "50", + "name": "Summary of SQL Elastic with or without AHB per subs" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "ResourceContainers | where type =~ 'Microsoft.Resources/subscriptions' | where tostring (properties.subscriptionPolicies.quotaId) !has \"MSDNDevTest_2014-09-01\" | extend SubscriptionName=name \r\n| join (\r\nresources\r\n| where type =~ 'Microsoft.Sql/servers/elasticPools' and kind contains 'vcore' and kind !contains \"serverless\"\r\n| extend SQLDBID=id,SQLName = name, SQLRG = resourceGroup, SKUName=tostring(sku.name), SKUTier=tostring(sku.tier), vCores=tostring(sku.capacity), SQLLocation = location, LicenseType = tostring(properties.['licenseType'])\r\n | join kind = innerunique(\r\n resources\r\n | extend replaced_tags = replace('{}', 'null', tostring(tags))\r\n | extend replaced_tags = parse_json(replaced_tags)\r\n | mv-expand replaced_tags\r\n | extend tagName = tostring(bag_keys(replaced_tags)[0])\r\n | extend tagValue = tostring(replaced_tags['{TagName}']), SQLDBID=id\r\n | where tagName has '{TagName}' and tagValue has '{TagValue}'\r\n | distinct SQLDBID\r\n )\r\n on SQLDBID\r\n | extend CheckSQLDBAHB = case(\r\n type =~ 'Microsoft.Sql/servers/elasticPools', iif((properties.['licenseType'])\r\n has 'LicenseIncluded', \"AHB Not Enabled\", \"AHB Enabled\"),\r\n \"Not SQL DB\"\r\n )\r\n ) on subscriptionId \r\n| summarize count() by CheckSQLDBAHB", + "size": 0, + "title": "Summary of SQL Databases with or without AHB", + "showRefreshButton": true, + "queryType": 1, + "resourceType": "microsoft.resourcegraph/resources", + "crossComponentResources": [ + "{Subscription}" + ], + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "tileSettings": { + "titleContent": { + "columnMatch": "CheckSQLDBAHB", + "formatter": 1 + }, + "subtitleContent": { + "columnMatch": "SubscriptionName", + "formatter": 1 + }, + "leftContent": { + "columnMatch": "count_", + "formatter": 12, + "formatOptions": { + "palette": "auto" + } + }, + "showBorder": false, + "size": "auto" + }, + "chartSettings": { + "xAxis": "SubscriptionName" } }, - { - "columnMatch": "SubscriptionName", - "formatter": 15, - "formatOptions": { - "linkTarget": null, - "showIcon": true - } - } - ], - "hierarchySettings": { - "treeType": 1, - "groupBy": ["SubscriptionName"], - "expandTopLevel": true + "customWidth": "50", + "name": "Summary of SQL DBs with or without AHB " }, - "labelSettings": [ - { - "columnId": "SQLDBID", - "label": "Database Name" - }, - { - "columnId": "SQLName", - "label": "Server Name" - }, - { - "columnId": "SQLRG", - "label": "Resource Group" - }, - { - "columnId": "SKUName", - "label": "SKU" - }, - { - "columnId": "SKUTier", - "label": "SKU Tier" - }, - { - "columnId": "vCores", - "label": "Number of vCore" + { + "type": 1, + "content": { + "json": "### Consumed Licenses\r\nTotal number of SQL licenses cores consumed by all SQL Databases\r\n", + "style": "info" }, - { - "columnId": "CheckSQLDBAHB", - "label": "Is AHB enabled?" - }, - { - "columnId": "SQLLocation", - "label": "Location" + "customWidth": "50", + "name": "Total number of SQL licenses cores consumed" + }, + { + "type": 1, + "content": { + "json": "### Number of required Cores to enable SQL Azure Hybrid Benefit\r\nNumber of cores required to enable SQL AHB across the entire environment.\r\n\r\n\r\n", + "style": "info" }, - { - "columnId": "LicenseType", - "label": "License Type" + "customWidth": "50", + "name": "Text SQL DB" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"}]}", + "size": 0, + "title": "SQL DB Elastic Pools AHB Consumed Cores per VM", + "noDataMessage": "None of your SQL DB Elastic Pools have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "vCores" + ], + "group": "SQLName", + "createOtherGroup": null + } }, - { - "columnId": "StorageAccountType", - "label": "Storage Account Type" + "customWidth": "33", + "name": "Summary SQLElastic+SKU AHB Enabled - per VM" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"}]}", + "size": 0, + "title": "SQL Elastic AHB Consumed Cores per Priority", + "noDataMessage": "None of your SQL DB Elastic Pools have AHB enabled.", + "noDataMessageStyle": 4, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "vCores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } }, - { - "columnId": "SubscriptionName", - "label": "Subscription Name" + "customWidth": "33", + "name": "Summary SQLElastic+SKU AHB Enabled - per Priority" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071c2\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[Added column]\",\"mergedName\":\"Consumed Cores\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"4\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"round([\\\"vCPUs\\\"] / 4) * 4\"}}]},{\"originalName\":\"[Added column]\",\"mergedName\":\"Prioritize AHB?\",\"fromId\":null,\"isNewItem\":true,\"newItemData\":[{\"criteriaContext\":{\"leftOperand\":\"vCores\",\"operator\":\"<\",\"rightValType\":\"static\",\"rightVal\":\"4\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"Low Priority\\\"\"}},{\"criteriaContext\":{\"operator\":\"Default\",\"rightValType\":\"column\",\"resultValType\":\"expression\",\"resultVal\":\"\\\"High Priority\\\"\"}}]},{\"originalName\":\"[SQLElastic AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071c2\"}]}", + "size": 0, + "title": "SQL DB AHB Cores not enabled per AHB Priority", + "noDataMessage": "All of your SQL DB have AHB enabled.", + "noDataMessageStyle": 3, + "showRefreshButton": true, + "queryType": 7, + "visualization": "piechart", + "gridSettings": { + "formatters": [ + { + "columnMatch": "Prioritize AHB?", + "formatter": 18, + "formatOptions": { + "thresholdsOptions": "icons", + "thresholdsGrid": [ + { + "operator": "==", + "thresholdValue": "High Priority", + "representation": "Sev0", + "text": "{0}{1}" + }, + { + "operator": "==", + "thresholdValue": "Low Priority", + "representation": "2", + "text": "{0}{1}" + }, + { + "operator": "Default", + "thresholdValue": null, + "representation": "unknown", + "text": "{0}{1}" + } + ] + } + } + ] + }, + "chartSettings": { + "yAxis": [ + "vCores" + ], + "group": "Prioritize AHB?", + "createOtherGroup": null + } }, - { - "columnId": "SQLDBID1", - "label": "Resource ID" - } - ] - } - }, - "conditionalVisibility": { - "parameterName": "SQLDBAHBDisabled", - "comparison": "isEqualTo", - "value": "Yes" + "customWidth": "33", + "name": "Summary SQLDB+SKU AHB Disabled - per Priority" + } + ] }, - "name": "SQL DB Disabled" + "name": "SQL Elastic Info" }, { - "type": 3, + "type": 9, "content": { - "version": "KqlItem/1.0", - "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071f9\",\"mergeType\":\"table\",\"leftTable\":\"SQLDB AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].StorageAccountType\",\"mergedName\":\"StorageAccountType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"}]}", - "size": 0, - "title": "SQL DB AHB Enabled", - "noDataMessage": "None of you SQL DBs have AHB enabled.", - "noDataMessageStyle": 4, - "showExportToExcel": true, - "queryType": 7, - "gridSettings": { - "labelSettings": [ - { - "columnId": "SQLDBID", - "label": "Name" - }, - { - "columnId": "SQLName", - "label": "Database Name" - }, - { - "columnId": "SQLRG", - "label": "Resource Group" - }, - { - "columnId": "SKUName", - "label": "SKU" - }, - { - "columnId": "SKUTier", - "label": "SKU Tier" + "version": "KqlParameterItem/1.0", + "parameters": [ + { + "id": "ae5e8765-47ef-46a6-803b-6b7124c098d2", + "version": "KqlParameterItem/1.0", + "name": "SQLDBHUBEnabled", + "label": "See SQL DBs with AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false }, - { - "columnId": "SQLLocation", - "label": "Location" + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n", + "timeContext": { + "durationMs": 86400000 + } + }, + { + "id": "f1ac5e53-253c-4afb-8bc5-b1ba2efea3eb", + "version": "KqlParameterItem/1.0", + "name": "SQLDBAHBDisabled", + "label": "See SQL DBs without AHB", + "type": 2, + "isRequired": true, + "typeSettings": { + "additionalResourceOptions": [], + "showDefault": false }, - { - "columnId": "LicenseType", - "label": "License Type" + "jsonData": "[\r\n {\"value\":\"Yes\"},\r\n {\"value\":\"No\", \"selected\":true}\r\n]\r\n\r\n" + } + ], + "style": "pills", + "queryType": 0, + "resourceType": "microsoft.operationalinsights/workspaces" + }, + "name": "SQL DB Without AHB" + }, + { + "type": 12, + "content": { + "version": "NotebookGroup/1.0", + "groupType": "editable", + "items": [ + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071ed\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Disabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"},{\"originalName\":\"[SQLElastic AHB Disabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071ed\"}]}", + "size": 0, + "title": "SQL DB AHB Disabled", + "noDataMessage": "All of your SQL DBs have AHB enabled.", + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "formatters": [ + { + "columnMatch": "$gen_group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "Group", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + }, + { + "columnMatch": "SubscriptionName", + "formatter": 15, + "formatOptions": { + "linkTarget": null, + "showIcon": true + } + } + ], + "hierarchySettings": { + "treeType": 1, + "groupBy": [ + "SubscriptionName" + ], + "expandTopLevel": true + }, + "labelSettings": [ + { + "columnId": "SQLDBID", + "label": "Database Name" + }, + { + "columnId": "SQLName", + "label": "Server Name" + }, + { + "columnId": "SQLRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "vCores", + "label": "Number of vCore" + }, + { + "columnId": "CheckSQLDBAHB", + "label": "Is AHB enabled?" + }, + { + "columnId": "SQLLocation", + "label": "Location" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "SQLDBID1", + "label": "Resource ID" + } + ] + } }, - { - "columnId": "StorageAccountType", - "label": "Storage Account Type" + "conditionalVisibility": { + "parameterName": "SQLDBAHBDisabled", + "comparison": "isEqualTo", + "value": "Yes" }, - { - "columnId": "SubscriptionName", - "label": "Subscription Name" + "name": "SQL DB Disabled" + }, + { + "type": 3, + "content": { + "version": "KqlItem/1.0", + "query": "{\"version\":\"Merge/1.0\",\"merges\":[{\"id\":\"f65bea23-bb49-4498-b331-c20c618071f9\",\"mergeType\":\"table\",\"leftTable\":\"SQLElastic AHB Enabled\"}],\"projectRename\":[{\"originalName\":\"[SQLDB AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLDB AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"unknown\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID\",\"mergedName\":\"SQLDBID\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLName\",\"mergedName\":\"SQLName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLRG\",\"mergedName\":\"SQLRG\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUName\",\"mergedName\":\"SKUName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SKUTier\",\"mergedName\":\"SKUTier\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].vCores\",\"mergedName\":\"vCores\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLLocation\",\"mergedName\":\"SQLLocation\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].LicenseType\",\"mergedName\":\"LicenseType\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].CheckSQLDBAHB\",\"mergedName\":\"CheckSQLDBAHB\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SubscriptionName\",\"mergedName\":\"SubscriptionName\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"},{\"originalName\":\"[SQLElastic AHB Enabled].SQLDBID1\",\"mergedName\":\"SQLDBID1\",\"fromId\":\"f65bea23-bb49-4498-b331-c20c618071f9\"}]}", + "size": 0, + "title": "SQL DB AHB Enabled", + "noDataMessage": "None of you SQL DBs have AHB enabled.", + "noDataMessageStyle": 4, + "showExportToExcel": true, + "queryType": 7, + "gridSettings": { + "labelSettings": [ + { + "columnId": "SQLDBID", + "label": "Name" + }, + { + "columnId": "SQLName", + "label": "Database Name" + }, + { + "columnId": "SQLRG", + "label": "Resource Group" + }, + { + "columnId": "SKUName", + "label": "SKU" + }, + { + "columnId": "SKUTier", + "label": "SKU Tier" + }, + { + "columnId": "vCores", + "label": "Number of vCore" + }, + { + "columnId": "SQLLocation", + "label": "Location" + }, + { + "columnId": "LicenseType", + "label": "License Type" + }, + { + "columnId": "SubscriptionName", + "label": "Subscription Name" + }, + { + "columnId": "SQLDBID1", + "label": "Resource ID" + } + ] + } }, - { - "columnId": "vCores", - "label": "Number of vCore" + "conditionalVisibility": { + "parameterName": "SQLDBHUBEnabled", + "comparison": "isEqualTo", + "value": "Yes" }, - { - "columnId": "SQLDBID1", - "label": "Resource ID" - } - ] - } - }, - "conditionalVisibility": { - "parameterName": "SQLDBHUBEnabled", - "comparison": "isEqualTo", - "value": "Yes" + "name": "SQL DB AHB Enabled" + } + ] }, - "name": "SQL DB AHB Enabled" + "name": "Load SQL DB Detailed Info" } ] }, - "name": "Load SQL DB Detailed Info" + "conditionalVisibility": { + "parameterName": "SQLType", + "comparison": "isEqualTo", + "value": "SQLElastic" + }, + "name": "SQLElasticPool" } ] }, @@ -7614,7 +9555,9 @@ "size": 0, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibility": { "parameterName": "AlwaysHidden", @@ -7631,7 +9574,9 @@ "size": 0, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibility": { "parameterName": "AlwaysHidden", @@ -7681,7 +9626,9 @@ "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "table", "tileSettings": { "titleContent": { @@ -7708,7 +9655,9 @@ "showBorder": false }, "chartSettings": { - "yAxis": ["count_"], + "yAxis": [ + "count_" + ], "group": "CheckSQLMIAHB", "createOtherGroup": null } @@ -7726,10 +9675,14 @@ "showRefreshButton": true, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart", "chartSettings": { - "yAxis": ["count_"], + "yAxis": [ + "count_" + ], "group": "CheckSQLMIAHB", "createOtherGroup": null } @@ -7799,7 +9752,9 @@ ] }, "chartSettings": { - "yAxis": ["vCores"], + "yAxis": [ + "vCores" + ], "group": "SQLName", "createOtherGroup": null } @@ -7851,7 +9806,9 @@ ] }, "chartSettings": { - "yAxis": ["vCores"], + "yAxis": [ + "vCores" + ], "group": "Prioritize AHB?", "createOtherGroup": null } @@ -7903,7 +9860,9 @@ ] }, "chartSettings": { - "yAxis": ["vCores"], + "yAxis": [ + "vCores" + ], "group": "Prioritize AHB?", "createOtherGroup": null } @@ -8001,7 +9960,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["SubscriptionName"], + "groupBy": [ + "SubscriptionName" + ], "expandTopLevel": true } } @@ -8101,7 +10062,9 @@ "noDataMessageStyle": 4, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibility": { "parameterName": "AlwaysHidden", @@ -8121,7 +10084,9 @@ "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibility": { "parameterName": "AlwaysHidden", @@ -8139,7 +10104,9 @@ "title": "Summary of Linux VMs with or without AHB", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"], + "crossComponentResources": [ + "{Subscription}" + ], "visualization": "piechart" }, "customWidth": "50", @@ -8182,7 +10149,9 @@ "type": 0 }, "chartSettings": { - "yAxis": ["ConsumedCores"], + "yAxis": [ + "ConsumedCores" + ], "group": "VMName", "createOtherGroup": null } @@ -8310,7 +10279,9 @@ }, "chartSettings": { "xAxis": "VM Name", - "yAxis": ["Consumed Cores per VM"], + "yAxis": [ + "Consumed Cores per VM" + ], "group": null, "createOtherGroup": 0, "seriesLabelSettings": [ @@ -8405,7 +10376,9 @@ }, "chartSettings": { "xAxis": "VM Name", - "yAxis": ["Consumed Cores per VM"], + "yAxis": [ + "Consumed Cores per VM" + ], "group": null, "createOtherGroup": 0, "seriesLabelSettings": [ @@ -8469,7 +10442,9 @@ "size": 0, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibilities": [ { @@ -8493,7 +10468,9 @@ "size": 0, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{Subscription}"] + "crossComponentResources": [ + "{Subscription}" + ] }, "conditionalVisibilities": [ { @@ -8579,7 +10556,9 @@ ] }, "chartSettings": { - "yAxis": ["Consumed Cores per VM"], + "yAxis": [ + "Consumed Cores per VM" + ], "group": "Prioritize AHB?", "createOtherGroup": null } @@ -8670,7 +10649,9 @@ "type": 0 }, "chartSettings": { - "yAxis": ["ConsumedCores"], + "yAxis": [ + "ConsumedCores" + ], "group": "VMName", "createOtherGroup": null } @@ -8804,7 +10785,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true } } @@ -8875,7 +10858,9 @@ ], "hierarchySettings": { "treeType": 1, - "groupBy": ["subscriptionId"], + "groupBy": [ + "subscriptionId" + ], "expandTopLevel": true } } @@ -8994,14 +10979,18 @@ "quote": "'", "delimiter": ",", "typeSettings": { - "additionalResourceOptions": ["value::all"], + "additionalResourceOptions": [ + "value::all" + ], "includeAll": false }, "timeContext": { "durationMs": 86400000 }, "defaultValue": "value::all", - "value": ["value::all"] + "value": [ + "value::all" + ] } ], "style": "pills", @@ -9039,7 +11028,9 @@ "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{mapSubscriptions}"], + "crossComponentResources": [ + "{mapSubscriptions}" + ], "gridSettings": { "formatters": [ { @@ -9095,7 +11086,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["Impact"] + "groupBy": [ + "Impact" + ] } } }, @@ -9203,7 +11196,9 @@ "title": "Resource distribution per region", "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{mapSubscriptions}"], + "crossComponentResources": [ + "{mapSubscriptions}" + ], "visualization": "map", "mapSettings": { "locInfo": "AzureLoc", @@ -9256,7 +11251,9 @@ "noDataMessageStyle": 3, "queryType": 1, "resourceType": "microsoft.resourcegraph/resources", - "crossComponentResources": ["{mapSubscriptions}"], + "crossComponentResources": [ + "{mapSubscriptions}" + ], "gridSettings": { "formatters": [ { @@ -9312,7 +11309,9 @@ "filter": true, "hierarchySettings": { "treeType": 1, - "groupBy": ["Impact"] + "groupBy": [ + "Impact" + ] } } }, @@ -9335,14 +11334,16 @@ "name": "group - reliabilityRecommendations" } ], - "fallbackResourceIds": ["Azure Monitor"], + "fallbackResourceIds": [ + "Azure Monitor" + ], "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json" }, "version": "", "workbookJson": "[string(variables('$fxv#0'))]", "workbookId": "0b2", "telemetryId": "[format('00f120b5-2007-6120-0000-{0}30126b006', variables('workbookId'))]", - "finOpsToolkitVersion": "0.3", + "finOpsToolkitVersion": "0.4", "resourceTags": "[union(parameters('tags'), createObject('ftk-version', variables('finOpsToolkitVersion'), 'ftk-tool', format('{0} workbook', parameters('displayName'))))]" }, "resources": [ @@ -9399,4 +11400,4 @@ "value": "[format('{0}/#view/AppInsightsExtension/UsageNotebookBlade/ComponentId/Azure%20Monitor/ConfigurationId/{1}/Type/{2}/WorkbookTemplateName/{3}', environment().portal, uriComponent(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName')))), reference(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))), '2022-04-01').category, uriComponent(reference(resourceId('Microsoft.Insights/workbooks', guid(resourceGroup().id, 'Microsoft.Insights/workbooks', parameters('displayName'))), '2022-04-01').displayName))]" } } -} +} \ No newline at end of file