Merge branch 'master' into PAYG-transition

This commit is contained in:
Alexander (Sasha) Nosov
2025-05-14 12:30:34 -07:00
committed by GitHub
6 changed files with 1343 additions and 177 deletions
@@ -8,8 +8,641 @@
"position": {
"x": 0,
"y": 0,
"colSpan": 20,
"rowSpan": 11
"colSpan": 6,
"rowSpan": 2
},
"metadata": {
"inputs": [
{
"name": "chartType",
"isOptional": true
},
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type == \"microsoft.hybridcompute/machines\"\n| where properties.detectedProperties.mssqldiscovered =~ 'true' or properties.mssqldiscovered =~ 'true'\n| summarize SqlServerMachines = count()",
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQuerySingleValueTile",
"settings": {},
"partHeader": {
"title": "Machines Hosting SQL Server",
"subtitle": "The count of machines that have SQL Server installed"
}
}
},
{
"position": {
"x": 6,
"y": 0,
"colSpan": 6,
"rowSpan": 2
},
"metadata": {
"inputs": [
{
"name": "chartType",
"isOptional": true
},
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type == \"microsoft.hybridcompute/machines/extensions\" \n| where properties.type in (\"WindowsAgent.SqlServer\", \"LinuxAgent.SqlServer\")\n| summarize SQLServerExtensions = count()",
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQuerySingleValueTile",
"settings": {},
"partHeader": {
"title": "SQL Server Extensions",
"subtitle": "The count of SQL Server extensions. Should be the same as the count of Machines Hosting SQL Server."
}
}
},
{
"position": {
"x": 12,
"y": 0,
"colSpan": 6,
"rowSpan": 2
},
"metadata": {
"inputs": [
{
"name": "chartType",
"isOptional": true
},
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type == \"microsoft.hybridcompute/machines/extensions\"\n| where properties.type in (\"WindowsAgent.SqlServer\", \"LinuxAgent.SqlServer\")\n| extend targetMachineName = tolower(tostring(split(id, '/')[8]))\n| join kind=leftouter (\n resources\n | where type == \"microsoft.hybridcompute/machines\"\n | project machineId = id, MachineName = name, subscriptionId, resourceGroup, MachineStatus = properties.status, MachineProvisioningStatus = properties.provisioningState, MachineErrors = properties.errorDetails\n) on $left.targetMachineName == $right.MachineName and $left.resourceGroup == $right.resourceGroup and $left.subscriptionId == $right.subscriptionId\n| extend extractedDate = todatetime(extract(\"timestampUTC : (\\\\d{4}-\\\\d{2}-\\\\d{2})\", 1, tostring(properties.instanceView.status.message)))\n| extend isNotInDateRange = extractedDate < ago(3d) or isnull(extractedDate)\n| where properties.instanceView.status.message !contains \"SQL Server Extension Agent: Healthy\"\n or isNotInDateRange\n or properties.instanceView.status.message !contains \"uploadStatus : OK\"\n or properties.provisioningState != \"Succeeded\"\n or MachineStatus != \"Connected\"\n| project ID = id, MachineName, ResourceGroup = resourceGroup, SubscriptionID = subscriptionId, Location = location, \n LastReportedExtensionHealth = iif(properties.instanceView.status.message contains \"SQL Server Extension Agent: Healthy\", \"Healthy\", \"Unhealthy\"),\n LastExtensionUploadTimestamp = tostring(extractedDate),\n LastExtensionUploadStatus = iif(properties.instanceView.status.message contains \"uploadStatus : OK\", \"OK\", \"Unhealthy\"),\n ExtensionProvisioningState = properties.provisioningState,\n MachineStatus, MachineErrors, MachineProvisioningStatus\n| project OverallHealth = iif(LastReportedExtensionHealth == \"Healthy\" and LastExtensionUploadStatus == \"OK\" and ExtensionProvisioningState == \"Succeeded\" and MachineStatus == \"Connected\" and MachineProvisioningStatus == \"Succeeded\", \"Healthy\", \"Unhealthy\")\n| where OverallHealth == \"Healthy\"\n| summarize HealthyExtensions = count()",
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQuerySingleValueTile",
"settings": {},
"partHeader": {
"title": "Overall Healthy SQL Server Extensions",
"subtitle": "The count of overall healthy SQL Server extensions should be the same as the count of SQL Server extensions."
}
}
},
{
"position": {
"x": 0,
"y": 2,
"colSpan": 6,
"rowSpan": 4
},
"metadata": {
"inputs": [
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "chartType",
"value": 2,
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
},
{
"name": "query",
"value": "resources\n | where type =~ 'microsoft.hybridcompute/machines/extensions'\n | where properties.type in ('WindowsAgent.SqlServer','LinuxAgent.SqlServer')\n | parse id with * '/providers/Microsoft.HybridCompute/machines/' machineName '/extensions/' *\n | parse properties with * 'uploadStatus : ' uploadStatus ';' *\n | project uploadStatus, subscriptionId, resourceGroup, machineName\n | summarize count() by uploadStatus",
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQueryChartTile",
"settings": {},
"partHeader": {
"title": "SQL Extension DPS Upload Status",
"subtitle": "'OK' is good. Anaything else should be investigated."
}
}
},
{
"position": {
"x": 6,
"y": 2,
"colSpan": 6,
"rowSpan": 4
},
"metadata": {
"inputs": [
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
},
{
"name": "chartType",
"value": 2,
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type =~ 'microsoft.hybridcompute/machines/extensions'\n| where properties.type in ('WindowsAgent.SqlServer', 'LinuxAgent.SqlServer')\n| parse id with * '/providers/Microsoft.HybridCompute/machines/' machineName '/extensions/' *\n| extend extractedDateString = extract(\"timestampUTC : (\\\\d{4}-\\\\d{2}-\\\\d{2})\", 1, tostring(properties.instanceView.status.message))\n| extend extractedDateString = case(\n isnull(extractedDateString) or extractedDateString == '', \n '1900-01-01T00:00:00Z', \n extractedDateString\n)\n| extend extractedDate = todatetime(extractedDateString)\n| extend isRecent = case(extractedDate > ago(1d), \"Less than 1 day ago\", extractedDate > ago(3d), \"1 to 3 days ago\", \"More than 3 days ago\")\n| summarize count() by isRecent",
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQueryChartTile",
"settings": {},
"partHeader": {
"title": "SQL Extension Status Update Time",
"subtitle": "Status should be updated every 5 minutes."
}
}
},
{
"position": {
"x": 12,
"y": 2,
"colSpan": 6,
"rowSpan": 4
},
"metadata": {
"inputs": [
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "chartType",
"value": 2,
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type == \"microsoft.hybridcompute/machines/extensions\"\n| where properties.type in (\"WindowsAgent.SqlServer\", \"LinuxAgent.SqlServer\")\n| summarize count() by tostring(properties.provisioningState)",
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQueryChartTile",
"settings": {},
"partHeader": {
"title": "SQL Extension Provisioning State",
"subtitle": "Provisioning State should normally be 'Succeeded'"
}
}
},
{
"position": {
"x": 0,
"y": 6,
"colSpan": 6,
"rowSpan": 4
},
"metadata": {
"inputs": [
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type == \"microsoft.hybridcompute/machines/extensions\" \n| where properties.type in (\"WindowsAgent.SqlServer\", \"LinuxAgent.SqlServer\")\n| summarize count() by tostring(properties.typeHandlerVersion)",
"isOptional": true
},
{
"name": "chartType",
"value": 2,
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQueryChartTile",
"settings": {},
"partHeader": {
"title": "SQL Extension Versions",
"subtitle": "Extension versions should be on a recent version, at least within one year. https://aka.ms/arcsqlreleasenotes"
}
}
},
{
"position": {
"x": 6,
"y": 6,
"colSpan": 6,
"rowSpan": 4
},
"metadata": {
"inputs": [
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type == \"microsoft.hybridcompute/machines/extensions\"\n| where properties.type in (\"WindowsAgent.SqlServer\", \"LinuxAgent.SqlServer\")\n| project GeneralHealth = iif(properties.instanceView.status.message contains \"SQL Server Extension Agent: Healthy\", \"Healthy\", \"Unhealthy\")\n| summarize count() by GeneralHealth",
"isOptional": true
},
{
"name": "chartType",
"value": 2,
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQueryChartTile",
"settings": {},
"partHeader": {
"title": "SQL Extension General Health",
"subtitle": "The health as reported in the extension of the extension itself."
}
}
},
{
"position": {
"x": 12,
"y": 6,
"colSpan": 6,
"rowSpan": 4
},
"metadata": {
"inputs": [
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type == \"microsoft.hybridcompute/machines/extensions\" \n| where properties.type in (\"WindowsAgent.SqlServer\", \"LinuxAgent.SqlServer\") \n| extend targetMachineName = tolower(tostring(split(id, '/')[8])) // Extract the machine name from the extension's id\n| join kind=leftouter (\n resources\n | where type == \"microsoft.hybridcompute/machines\"\n | project machineId = id, MachineName = name, subscriptionId, LowerMachineName = tolower(name), resourceGroup , MachineStatus= properties.status , MachineProvisioningStatus= properties.provisioningState\n) on $left.targetMachineName == $right.LowerMachineName and $left.resourceGroup == $right.resourceGroup and $left.subscriptionId == $right.subscriptionId // Join Based on MachineName in the id and the machine's name, the resource group, and the subscription. This join allows us to present the data of the machine as well as the extension in the final output.\n| summarize count() by tostring(MachineProvisioningStatus)",
"isOptional": true
},
{
"name": "chartType",
"value": 2,
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQueryChartTile",
"settings": {},
"partHeader": {
"title": "Machine Provisioning State",
"subtitle": "Machines should have a provisioning state of 'Succeeded'"
}
}
},
{
"position": {
"x": 0,
"y": 10,
"colSpan": 6,
"rowSpan": 4
},
"metadata": {
"inputs": [
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type == \"microsoft.hybridcompute/machines/extensions\" \n| where properties.type in (\"WindowsAgent.SqlServer\", \"LinuxAgent.SqlServer\") \n| extend targetMachineName = tolower(tostring(split(id, '/')[8])) // Extract the machine name from the extension's id\n| join kind=leftouter (\n resources\n | where type == \"microsoft.hybridcompute/machines\"\n | project machineId = id, MachineName = name, subscriptionId, LowerMachineName = tolower(name), resourceGroup , MachineStatus= properties.status , MachineProvisioningStatus= properties.provisioningState\n) on $left.targetMachineName == $right.LowerMachineName and $left.resourceGroup == $right.resourceGroup and $left.subscriptionId == $right.subscriptionId // Join Based on MachineName in the id and the machine's name, the resource group, and the subscription. This join allows us to present the data of the machine as well as the extension in the final output.\n| summarize count() by tostring(MachineStatus)",
"isOptional": true
},
{
"name": "chartType",
"value": 2,
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQueryChartTile",
"settings": {},
"partHeader": {
"title": "Machine Connectivity Status",
"subtitle": "Machines should have a state of 'Connected'"
}
}
},
{
"position": {
"x": 6,
"y": 10,
"colSpan": 6,
"rowSpan": 4
},
"metadata": {
"inputs": [
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "chartType",
"value": 2,
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
},
{
"name": "query",
"value": "resources\n| where type == \"microsoft.hybridcompute/machines/extensions\" \n| where properties.type in (\"WindowsAgent.SqlServer\", \"LinuxAgent.SqlServer\") \n| extend targetMachineName = tolower(tostring(split(id, '/')[8])) // Extract the machine name from the extension's id\n| join kind=leftouter (\n resources\n | where type == \"microsoft.hybridcompute/machines\"\n | project machineId = id, MachineName = name, subscriptionId, LowerMachineName = tolower(name), resourceGroup , MachineExtensionServiceStatus = properties.serviceStatuses.extensionService.status\n) on $left.targetMachineName == $right.LowerMachineName and $left.resourceGroup == $right.resourceGroup and $left.subscriptionId == $right.subscriptionId // Join Based on MachineName in the id and the machine's name, the resource group, and the subscription. This join allows us to present the data of the machine as well as the extension in the final output.\n| summarize count() by tostring(MachineExtensionServiceStatus)",
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQueryChartTile",
"settings": {},
"partHeader": {
"title": "Machine Extension Service Status",
"subtitle": "The Machine Extension Service status should be 'Running'"
}
}
},
{
"position": {
"x": 12,
"y": 10,
"colSpan": 6,
"rowSpan": 4
},
"metadata": {
"inputs": [
{
"name": "isShared",
"isOptional": true
},
{
"name": "queryId",
"isOptional": true
},
{
"name": "formatResults",
"isOptional": true
},
{
"name": "partTitle",
"value": "Query 1",
"isOptional": true
},
{
"name": "query",
"value": "//Machine Guest Configuration Service Status\nresources\n| where type == \"microsoft.hybridcompute/machines/extensions\" \n| where properties.type in (\"WindowsAgent.SqlServer\", \"LinuxAgent.SqlServer\") \n| extend targetMachineName = tolower(tostring(split(id, '/')[8])) // Extract the machine name from the extension's id\n| join kind=leftouter (\n resources\n | where type == \"microsoft.hybridcompute/machines\"\n | project machineId = id, MachineName = name, subscriptionId, LowerMachineName = tolower(name), resourceGroup , GuestConfigurationService = properties.serviceStatuses.guestConfigurationService.status\n) on $left.targetMachineName == $right.LowerMachineName and $left.resourceGroup == $right.resourceGroup and $left.subscriptionId == $right.subscriptionId // Join Based on MachineName in the id and the machine's name, the resource group, and the subscription. This join allows us to present the data of the machine as well as the extension in the final output.\n| summarize count() by tostring(GuestConfigurationService)",
"isOptional": true
},
{
"name": "chartType",
"value": 2,
"isOptional": true
},
{
"name": "queryScope",
"value": {
"scope": 0,
"values": []
},
"isOptional": true
}
],
"type": "Extension/HubsExtension/PartType/ArgQueryChartTile",
"settings": {},
"partHeader": {
"title": "Machine Guest Configuration Service Status",
"subtitle": "The Machine Guest Configuration Status should be 'Running'"
}
}
},
{
"position": {
"x": 0,
"y": 14,
"colSpan": 18,
"rowSpan": 12
},
"metadata": {
"inputs": [
@@ -1,32 +1,10 @@
#
# This script scan each VM in scope to detect if SQL server insstalled and if it is regsitered with IaaS extension.
# The report includes the following information for each VM.
#
# Subscription Name
# Subscription ID
# ResourceGroupName
# Server Name
# Location
# Server SKU
# Size in vCores
# OS Type
# OS Version
# SQL Version
# SQL Edition
# IaaS registration status
#
# The script accepts the following command line parameters:
#
# -SubId [subscription_id] (Optional. If not specified all subscription the user has access to Accepts a .csv file with the list of subscriptions)
# -FilePath [csv_file_name] (Optional. Sprcifies a .csv file to save the data. if not specified, saves it in sql-vcm-inventory.csv)
#
param (
param (
[Parameter (Mandatory= $false)]
[string] $SubId,
[Parameter (Mandatory= $false)]
[PSCredential] $Cred,
[Parameter (Mandatory= $false)]
[string] $FilePath
)
function CheckModule ($m) {
@@ -56,76 +34,10 @@ function CheckModule ($m) {
}
}
function GetVCores {
# This function translates each VM or Host sku type and name into vCores
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]$type,
[Parameter(Mandatory)]
[string]$name
)
if ($global:VM_SKUs.Count -eq 0){
$global:VM_SKUs = Get-AzComputeResourceSku "westus" | where-object {$_.ResourceType -in 'virtualMachines','hostGroups/hosts'}
}
# Select first size and get the VCPus available
$size_info = $global:VM_SKUs | Where-Object {$_.ResourceType.Contains($type) -and ($_.Name -eq $name)} | Select-Object -First 1
# Save the VCPU count
switch ($type) {
"hosts" {$vcpu = $size_info.Capabilities | Where-Object {$_.name -eq "Cores"} }
"virtualMachines" {$vcpu = $size_info.Capabilities | Where-Object {$_.name -eq "vCPUsAvailable"} }
}
if ($vcpu){
return $vcpu.Value
}
else {
return 0
}
}
function DiscoveryOnWindows {
# This script checks if SQL Server is installed on Windows
[string] $SqlInstalled = ""
$regPath = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server'
if (Test-Path $regPath) {
$inst = (get-itemproperty $regPath).InstalledInstances
#$SqlInstalled = ($inst.Count -gt 0)
foreach ($i in $inst) {
# Read registry data
#
$p = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL').$i
$setupValues = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$p\Setup")
$edition = ($setupValues.Edition -split ' ')[0]
$version = ($setupValues.Version)
$SqlInstalled = $version, $edition -join ':'
}
}
Write-Output $SqlInstalled
}
#
# This script checks if SQL Server is installed on Linux
# Suppress warnings
#
#
$DiscoveryOnLinux =
'if ! systemctl is-active --quiet mssql-server.service; then dir
echo "False"
exit
else
echo "True"
fi'
# Ensure that the required modules are imported
# In Runbooks these modules must be added to the automation account manually
Update-AzConfig -DisplayBreakingChangeWarning $false
$requiredModules = @(
"Az.Accounts",
@@ -137,37 +49,27 @@ $requiredModules = @(
)
$requiredModules | Foreach-Object {CheckModule $_}
# Save the function definitions to run in parallel loops
$GetVCoresDef = $function:GetVCores.ToString()
# Create a script file with the SQL server discovery logic
New-Item -ItemType file -path DiscoverSql.ps1 -value $function:DiscoveryOnWindows.ToString() -Force | Out-Null
New-Item -ItemType file -path DiscoverSql.sh -value $DiscoveryOnLinux -Force | Out-Null
# Subscriptions to scan
if ($SubId -like "*.csv") {
$subscriptions = Import-Csv $SubId
}elseif($SubId.length -gt 0){
}elseif($SubId -ne $null){
$subscriptions = [PSCustomObject]@{SubscriptionId = $SubId} | Get-AzSubscription
}else{
$subscriptions = Get-AzSubscription
}
#File setup
#Log file setup
if (!$PSBoundParameters.ContainsKey("FilePath")) {
$FilePath = '.\sql-vm-inventory.csv'
$FilePath = '.\sql-change-log.csv'
}
[System.Collections.ArrayList]$inventoryTable = @()
$inventoryTable += ,(@("Subscription Name", "Subscription ID", "ResourceGroupName", "Name", "Location", "SKU", "vCores", "OS Type", "OS Version", "SQL Version", "SQL Edition", "IaaS registration"))
$global:VM_SKUs = @{} # To hold the VM SKU table for future use
Write-Host ([Environment]::NewLine + "-- Scanning subscriptions --")
# Record the start time
$startTime = Get-Date
Write-Host ("Script execution started at: $startTime")
# Calculate usage for each subscription
foreach ($sub in $subscriptions){
@@ -178,78 +80,90 @@ foreach ($sub in $subscriptions){
Set-AzContext -SubscriptionId $sub.Id
}catch {
write-host "Invalid subscription: " $sub.Id
{continue}
continue
}
# Reset the subtotals
#$subtotal.psobject.properties.name | Foreach-object {$subtotal.$_ = 0}
# Get all resource groups in the subscription
#$rgs = Get-AzResourceGroup
$rgs = Get-AzResourceGroup
# Scan all VMs with SQL server installed using a parallel loop (up to 10 at a time).
# NOTE: ForEach-Object -Parallel requires PS v7.1 or higher
if ($PSVersionTable.PSVersion.Major -ge 7){
#Get-AzVM -Status | Where-Object { $_.powerstate -eq 'VM running' } | ForEach-Object -ThrottleLimit 10 -Parallel {
Get-AzVM -Status | Where-Object { $_.powerstate -eq 'VM running' } | ForEach-Object {
#$function:GetVCores = $using:GetVCoresDef
$SqlEdition = ''
$SqlVersion = ''
$vCores = GetVCores -type 'virtualMachines' -name $_.HardwareProfile.VmSize
$sql_vm = Get-AzSqlVm -ResourceGroupName $_.ResourceGroupName -Name $_.Name -ErrorAction Ignore
# Get all logical servers
$servers = Get-AzSqlServer
if ($sql_vm) {
$RegStatus = 'SQL Server registered'
$SqlEdition = $Sql_vm.Sku
$SqlVersion = $Sql_vm.Offer
}
else {
if ($_.StorageProfile.OSDisk.OSType -eq "Windows"){
$params =@{
ResourceGroupName = $_.ResourceGroupName
Name = $_.Name
CommandId = 'RunPowerShellScript'
ScriptPath = 'DiscoverSql.ps1'
ErrorAction = 'Stop'
}
}
else {
$params =@{
ResourceGroupName = $_.ResourceGroupName
Name = $_.Name
CommandId = 'RunShellScript'
ScriptPath = 'DiscoverSql.sh'
ErrorAction = 'Stop'
}
}
try {
$out = Invoke-AzVMRunCommand @params
if (!$out.Value[0].Message){
$RegStatus = 'SQL Server not installed'
}
else {
$SqlVersion, $SqlEdition = $out.Value[0].Message -split ':', 2
$RegStatus = 'SQL Server not registered'
}
}
catch {
$RegStatus = 'No VM access'
}
}
$inventoryTable += ,(@( $sub.Name, $sub.Id, $_.ResourceGroupName, $_.Name, $_.Location, $_.HardwareProfile.VmSize, $vCores, $_.StorageProfile.ImageReference.Offer, $_.StorageProfile.ImageReference.Sku, $SqlVersion, $SqlEdition, $RegStatus))
#write-host $_.ResourceGroupName $_.Name $_.Location $_.HardwareProfile.VmSize $vCores $_.StorageProfile.ImageReference.Offer $_.StorageProfile.ImageReference.Sku $SqlVersion $SqlEdition $RegStatus
# Scan all vCore-based SQL database resources in the subscription
$servers | Get-AzSqlDatabase | Where-Object { $_.SkuName -ne "ElasticPool" -and $_.Edition -in @("GeneralPurpose", "BusinessCritical", "Hyperscale") } | ForEach-Object {
if ($_.LicenseType -ne "LicenseIncluded") {
Set-AzSqlDatabase -ResourceGroupName $_.ResourceGroupName -ServerName $_.ServerName -DatabaseName $_.DatabaseName -LicenseType "LicenseIncluded"
Write-Host ([Environment]::NewLine + "-- Database $_.DatabaseName is set to \"LicenseIncluded\"")
}
}
[system.gc]::Collect()
# Scan all vCore-based SQL elastic pool resources in the subscription
$servers | Get-AzSqlElasticPool | Where-Object { $_.Edition -in @("GeneralPurpose", "BusinessCritical", "Hyperscale") } | ForEach-Object {
if ($_.LicenseType -ne "LicenseIncluded") {
Set-AzSqlElasticPool -ResourceGroupName $_.ResourceGroupName -ServerName $_.ServerName -ElasticPoolName $_.ElasticPoolName -LicenseType "LicenseIncluded"
Write-Host ([Environment]::NewLine + "-- ElasticPool $_.ElasticPoolName is set to \"LicenseIncluded\"")
}
}
[system.gc]::Collect()
# Scan all SQL managed instance resources in the subscription
Get-AzSqlInstance | Where-Object { $_.InstancePoolName -eq $null } | ForEach-Object {
if ($_.LicenseType -ne "LicenseIncluded") {
Set-AzSqlInstance -ResourceGroupName $_.ResourceGroupName -ServerName $_.ServerName -InstanceName $_.InstanceName -LicenseType "LicenseIncluded"
Write-Host ([Environment]::NewLine + "-- Instance $_.InstanceName is set to \"LicenseIncluded\"")
}
}
[system.gc]::Collect()
# Scan all instance pool resources in the subscription
Get-AzSqlInstancePool | Foreach-Object {
if ($_.LicenseType -ne "LicenseIncluded") {
Set-AzSqlInstancePool -ResourceGroupName $_.ResourceGroupName -ServerName $_.ServerName -InstanceName $_.InstanceName -LicenseType "LicenseIncluded"
Write-Host ([Environment]::NewLine + "-- InstancePool $_.InstanceName is set to \"LicenseIncluded\"")
}
}
[system.gc]::Collect()
# Scan all SSIS integration runtime resources in the subscription
$rgs | Get-AzDataFactoryV2 | Get-AzDataFactoryV2IntegrationRuntime | Where-Object { $_.State -eq "Started" -and $_.NodeSize -ne $null } | ForEach-Object {
if ($_.LicenseType -ne "LicenseIncluded") {
# Set the license type to "LicenseIncluded"
Set-AzDataFactoryV2IntegrationRuntime -ResourceGroupName $_.ResourceGroupName -DataFactoryName $_.DataFactoryName -Name $_.Name -LicenseType "LicenseIncluded"
Write-Host ([Environment]::NewLine + "-- DataFactory $_.DataFactoryName is set to \"LicenseIncluded\"")
}
}
[system.gc]::Collect()
# Scan all SQL VMs in the subscription
$rgs | Get-AzVM | Where-Object { $_.StorageProfile.ImageReference.Offer -like "*sql*" -and $_.ProvisioningState -eq "Succeeded" } | ForEach-Object {
$vmName = $_.Name
$resourceGroupName = $_.ResourceGroupName
# Get the SQL configuration for the VM
$sqlConfig = Get-AzVMExtension -ResourceGroupName $resourceGroupName -VMName $vmName -Name "SqlIaaSAgent"
if ($sqlConfig -ne $null) {
$licenseType = $sqlConfig.Settings.LicenseType
if ($licenseType -ne "LicenseIncluded") {
# Set the license type to "LicenseIncluded"
Set-AzVMExtension -ResourceGroupName $resourceGroupName -VMName $vmName -Name "SqlIaaSAgent" -Publisher "Microsoft.SqlServer.Management" -ExtensionType "SqlIaaSAgent" -TypeHandlerVersion "1.5" -Settings @{ "LicenseType" = "LicenseIncluded" }
Write-Host ([Environment]::NewLine + "-- SQL VM $vmName is set to \"LicenseIncluded\"")
}
}
}
[system.gc]::Collect()
}
# Record the end time
$endTime = Get-Date
Write-Host ("Script execution ended at: $endTime")
# Write usage data to the .csv file
if ($FilePath){
(ConvertFrom-Csv ($inventoryTable | %{$_ -join ','})) | Export-Csv $FilePath -NoType #-Append
Write-Host ([Environment]::NewLine + "-- Added the usage data to $FilePath --")
} else {
Write-Host $inventoryTable
}
# Calculate the duration of script execution
$executionDuration = $endTime - $startTime
Write-Host ("Script execution duration: $executionDuration")
Write-Host ([Environment]::NewLine + "-- Script execution completed --")
@@ -0,0 +1,176 @@
---
services: Azure SQL
platforms: Azure
author: anosov1960,rodrigomonteiro-gbb
ms.author: sashan.romontei
ms.date: 04/08/2025
---
# Overview
This script provides a scaleable solution to change the license type of various Azure SQL resources within the selected scope. It automates the process of modifying license settings for SQL Databases, Elastic Pools, SQL Managed Instances, SQL Instance Pools, SQL Virtual Machines, and DataFactory SSIS Integration Runtimes. The script supports targeting a single subscription, a list of subscriptions defined in a CSV file, or all accessible subscriptions. Optionally, it can also start resources that are stopped (if the -ForceStartOnResources parameter is enabled).
This script is designed to help administrators standardize SQL licensing across their Azure environment by automating license updates. It accepts a subscription ID or CSV file (for a list of subscriptions). If no subscription is specified, it defaults to updating resources in all accessible subscriptions.
# Target Resource Types
- SQL Virtual Machines: Updates license types after checking if the VM is running. If not, it can optionally start the VM to perform the update.
- SQL Managed Instances: Detects instances that are stopped or not in the "Ready" state and can force them to start before updating.
- SQL Databases & Elastic Pools: Scans individual SQL servers to locate databases and elastic pools with a different license type and updates them accordingly.
- SQL Instance Pools: Locates instance pools that require an update.
- DataFactory SSIS Integration Runtimes: Checks for integration runtimes with an out-of-date license setting and updates them.
# Required Permissions
The automation account needs to have the bellow permissions in order to be able to successfully run the Runbook and update all the SQL Server resources license type:
1. **SQL DB Contributor**: *SQL DB Contributor role*.
1. **SQL Managed Instance Contributor**: *SQL Managed Instance Contributor*
1. **SQL Server Contributor**: *SQL Managed Instance Contributor*
1. **Data Factory Contributor**: *Data Factory Contributor role*.
1. **Virtual Machine Contributor**: *Virtual Machine Contributor role*.
A *Subscription Contributor* role has sufficient permissions to mdify any of the above resources.
# Interactive Reporting
The script logs steps along the process and compiles a final report summarizing which resources were updated in each category.
# Integration with Azure Authentication
The scripts is seamlessly integrated with Azure Authentication. It uses managed identity authentication (via Connect-AzAccount -Identity and az login --identity) to connect to your Azure environment securely.
# Script parameters
| **Parameter** &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | **Value** &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; | **Description** |
|:--|:--|:--|
|-SubId|subscription_id *or* a file_name|Optional: Subscription id or a .csv file with the list of subscriptions<sup>1</sup>. If not specified all subscriptions will be scanned|
|-ResourceGroup |resource_group_name|Optional: Limits the scope to a specific resource group|
|-LicenseType | "LicenseIncluded" (default) or "BasePrice" | Optional: Sets the license type to the specified value |
|-ForceStartOnResources| |(Optional) When enabled, the script will attempt to start SQL VMs and SQL Managed Instances if they are not running before applying the update.|
# Logging & Error Handling
The script logs key actions to the console and captures error messages using Write-Error. Check the console output for a summary report detailing which resources were updated.
# Customizations
You might want to customize the scripts logging or incorporate additional logging (e.g., writing to a file or Azure Log Analytics) to integrate seamlessly with your monitoring and reporting workflow.
# Script execution examples
## Example 1
The following command will scan all the subscriptions to which the user has access to, and set the license type to pay-as-you-go on all SQL resources in each subscription that the user has access to.
```PowerShell
.\modify-license-type.ps1 -LicenseType LicenseIncluded
```
## Example 2
The following command will scan a specific subscription, and set the license type to pay-as-you-go on all SQL resources.
```PowerShell
.\modify-license-type.ps1 -SubId <sub_id> -LicenseType LicenseIncluded
```
## Example 3
The following command will scan the resource group `<resource_group_name>` in the subscription `<sub_id>`, set the license type value to pay-as-you-go. If the resource group has SQL VMs in the offline state, it will start each VM before applying the change, and then stop it.
```PowerShell
.\modify-license-type.ps1 -SubId <sub_id> -ResourceGroup <resource_group_name> -LicenseType LicenseIncluded -ForceStartOnResources
```
# Running the script from your PC
1. Connect to Azure AD. You must specify `<tenant_id>` if you have access to more than one AAD tenants.
```console
Connect-AzureAD -TenantID <tenant_id>
```
1. Open a command shell on your device and download the script the script to your local folder.
```console
curl https://raw.githubusercontent.com/microsoft/sql-server-samples/refs/heads/master/samples/manage/azure-hybrid-benefit/modify-license-type/modify-license-type.ps1
```
1. Execute the command as shown by the examples
# Running the script using Cloud Shell
This option is recommended because Cloud shell has the Azure PowerShell modules pre-installed and you are automatically authenticated. Use the following steps to run the script in Cloud Shell.
1. Launch the [Cloud Shell](https://shell.azure.com/). For details, [read more about PowerShell in Cloud Shell](https://aka.ms/pscloudshell/docs).
1. Connect to Azure AD. You must specify `<tenant_id>` if you have access to more than one AAD tenants.
```console
Connect-AzureAD -TenantID <tenant_id>
```
1. Upload the script to your cloud shell using the following command:
```console
curl https://raw.githubusercontent.com/microsoft/sql-server-samples/refs/heads/master/samples/manage/azure-hybrid-benefit/modify-license-type/modify-license-type.ps1
```
1. Execute the command as shown by the examples.
> [!NOTE]
> - To paste the commands into the shell, use `Ctrl-Shift-V` on Windows or `Cmd-v` on MacOS.
> - The script will be uploaded directly to the home folder associated with your Cloud Shell session.
# Schedule the script execution using Azure Runbook
You can schedule the command to run as a runbook. Follow these steps using the Azure Portal:
### 1. Download the Script
Open a command shell on your device and dowload the script to to your current folder.
```console
curl https://raw.githubusercontent.com/microsoft/sql-server-samples/refs/heads/master/samples/manage/azure-hybrid-benefit/modify-license-type/modify-license-type.ps1
```
### 2. Create or Use an Existing Automation Account
[Create a new automation account](https://ms.portal.azure.com/#create/Microsoft.AutomationAccount) or open an existing one. In the Advanced section, ensure that System assigned identity is selected.
### 3. Import the Runbook
- Navigate to the Process Automation group and select **Runbooks**.
- Click on the **Import a runbook** tab and configure it:
**File**: Select the file you downloaded in Step 1.
**Name**: Enter a name for the runbook.
**Type**: Set to PowerShell.
**Runtime Version**: Choose 7.2.
- Click **Import**.
### 4. Publish the Runbook
After the runbook is imported, click the **Publish** button to make it available for scheduling.
### 5. Link the Runbook to a Schedule
- Once the runbook status is *Published*, click on the **Link to schedule** button.
- Select *Link a schedule to your runbook* and click **+ Add**.
- Configure the schedule:
**Name**: Provide a name for the schedule.
**Start Time**: Set the desired start time.
**Recurrence**: Choose the recurrence need it.
- Click **Create**.
### 6. Configure Runbook Parameters
- Return to the **Schedule runbook** page.
- Click on **Parameters** and run settings.
- Paste the license type value into the appropriate field.
- Click **OK** to link the schedule, then **OK** again to create the job.
### 7. Verify the Runbook Execution
On the runbook Overview page:
- Open a recent job that was completed after the scheduled start time.
- Click on the **Output tab** and verify that you see: `Properties.activationState=Activated `
Your license is now active.
For more information about the runbooks, see the [Runbook tutorial](https://docs.microsoft.com/en-us/azure/automation/learn/automation-tutorial-runbook-textual-powershell)
@@ -0,0 +1,442 @@
<#
.SYNOPSIS
Updates the license type for Azure SQL resources (SQL DBs, Elastic Pools, Managed Instances, Instance Pools, SQL VMs)
to a specified model ("LicenseIncluded" or "BasePrice"). Optionally starts resources if needed.
.DESCRIPTION
The script updates Azure SQL License types across subscriptions by modifying the license settings for a variety of SQL resources. It supports processing resources in one of the following ways:
Single Subscription:
Run against a specified subscription ID.
CSV List of Subscriptions:
Process multiple subscriptions provided in a CSV file.
All Accessible Subscriptions:
Automatically detect and update all subscriptions that you have access to.
For specific resource types like SQL Virtual Machines and SQL Managed Instances, the script can optionally start the resource if it is in a stopped state (when the -ForceStartOnResources parameter is enabled) before applying the license update.
The script processes several types of Azure SQL resources including:
SQL Virtual Machines (SQL VMs)
SQL Managed Instances
SQL Databases
Elastic Pools
SQL Instance Pools
DataFactory SSIS Integration Runtimes
This automation helps ensure that your licensing configuration is consistent across your environment without manual intervention.
.PARAMETER SubId
A single subscription ID or a CSV file name containing a list of subscriptions.
.PARAMETER ResourceGroup
Optional. Limit the scope to a specific resource group.
.PARAMETER LicenseType
Optional. License type to set. Allowed values: "LicenseIncluded" (default) or "BasePrice".
.PARAMETER ForceStartOnResources
Optional. If true, starts SQL VMs and SQL Managed Instances before updating their license type.
#>
param (
[Parameter(Mandatory = $false)]
[string] $SubId,
[Parameter(Mandatory = $false)]
[string] $ResourceGroup,
[Parameter(Mandatory = $false)]
[ValidateSet("LicenseIncluded", "BasePrice", IgnoreCase = $false)]
[string] $LicenseType = "LicenseIncluded",
[Parameter(Mandatory = $false)]
[bool] $ForceStartOnResources = $false
)
# Suppress unnecessary logging output
$VerbosePreference = "SilentlyContinue"
$DebugPreference = "SilentlyContinue"
$ProgressPreference = "SilentlyContinue"
$InformationPreference = "SilentlyContinue"
$WarningPreference = "SilentlyContinue"
function Connect-Azure {
<#
.SYNOPSIS
Connects to Azure using either Managed Identity or interactive user login.
.DESCRIPTION
This function first attempts to authenticate to Azure using a Managed Identity by calling
Connect-AzAccount with the -Identity switch. If the Managed Identity login is successful,
it then connects the Azure CLI using the --identity option. If any of the Managed Identity
login attempts fail, the function falls back to interactive (user) login for both Azure
PowerShell and the Azure CLI.
.EXAMPLE
PS C:\> Connect-Azure
Attempts to connect using Managed Identity; if not available, it will prompt for interactive login.
#>
# Variable to track if Managed Identity authentication was successful.
$ManagedIdentityLoginSuccessful = $false
Write-Output "Attempting to connect using Managed Identity for Azure PowerShell..."
try {
Connect-AzAccount -Identity -ErrorAction Stop | Out-Null
Write-Output "Azure PowerShell connected using Managed Identity."
$ManagedIdentityLoginSuccessful = $true
}
catch {
Write-Output "Managed Identity login failed for Azure PowerShell. Falling back to interactive login..."
try {
Connect-AzAccount -ErrorAction Stop | Out-Null
Write-Output "Azure PowerShell connected using interactive login."
}
catch {
Write-Error "Failed to connect to Azure PowerShell: $_"
return
}
}
# Attempt Azure CLI connection.
if ($ManagedIdentityLoginSuccessful) {
Write-Output "Attempting to connect Azure CLI using Managed Identity..."
try {
az login --identity --output none
Write-Output "Azure CLI connected using Managed Identity."
}
catch {
Write-Output "Managed Identity login failed for Azure CLI. Falling back to interactive login..."
try {
az login --output none
Write-Output "Azure CLI connected using interactive login."
}
catch {
Write-Error "Failed to connect to Azure CLI interactively: $_"
}
}
}
else {
Write-Output "Attempting to connect Azure CLI using interactive login..."
try {
az login --output none
Write-Output "Azure CLI connected using interactive login."
}
catch {
Write-Error "Failed to connect to Azure CLI interactively: $_"
}
}
}
# Initialize final status and report counters.
$finalStatus = @()
$report = @{
"SQLVMUpdated" = @()
"SQLMIUpdated" = @()
"SQLDBUpdated" = @()
"ElasticPoolUpdated" = @()
"InstancePoolUpdated" = @()
"ADFSSISUpdated" = @()
}
# Ensure connection with both PowerShell and CLI.
Connect-Azure
$context = Get-AzContext -ErrorAction SilentlyContinue
Write-Output "Connected to Azure as: $($context.Account)"
# Map License Types for SQL VMs: LicenseIncluded -> PAYG, BasePrice -> AHUB.
$SqlVmLicenseType = if ($LicenseType -eq "LicenseIncluded") { "PAYG" } else { "AHUB" }
# Determine the subscriptions to process: CSV file, single subscription, or all accessible subscriptions.
try {
if ($SubId -and $SubId -like "*.csv") {
Write-Output "Gathering subscriptions from CSV file: $SubId"
$subscriptions = Import-Csv -Path $SubId
}
elseif ($SubId) {
Write-Output "Gathering subscription details for: $SubId"
$subscriptions = @(az account show --subscription $SubId --output json | ConvertFrom-Json)
}
else {
Write-Output "Gathering all accessible subscriptions..."
$subscriptions = az account list --output json | ConvertFrom-Json
}
}
catch {
Write-Error "Error determining subscriptions: $_"
exit 1
}
# Build resource group filter if specified.
$rgFilter = if ($ResourceGroup) { "resourceGroup=='$ResourceGroup'" } else { "" }
$scriptStartTime = Get-Date
Write-Output "Our adventure begins at: $scriptStartTime`n"
# Process each subscription.
foreach ($sub in $subscriptions) {
try {
Write-Output "===== Entering the realm of Subscription: $($sub.name) ====="
Write-Output "Switching context to subscription: $($sub.name)"
az account set --subscription $sub.id
# --- Section: Update SQL Virtual Machines ---
try {
Write-Output "Seeking SQL Virtual Machines that require a license update..."
$sqlVmQuery = if ($rgFilter) {
"[?sqlServerLicenseType!='${SqlVmLicenseType}' && $rgFilter]"
}
else {
"[?sqlServerLicenseType!='${SqlVmLicenseType}']"
}
$sqlVMs = az sql vm list --query $sqlVmQuery -o json | ConvertFrom-Json
$sqlVmsToUpdate = [System.Collections.ArrayList]::new()
foreach ($sqlvm in $sqlVMs) {
$vmStatus = az vm get-instance-view --resource-group $sqlvm.resourceGroup --name $sqlvm.name --query "{Name:name, ResourceGroup:resourceGroup, PowerState:instanceView.statuses[?starts_with(code, 'PowerState/')].displayStatus | [0]}" -o json | ConvertFrom-Json
if ($vmStatus.PowerState -eq "VM running") {
Write-Output "Updating SQL VM '$($sqlvm.name)' in RG '$($sqlvm.resourceGroup)' to license type '$SqlVmLicenseType'..."
$result = az sql vm update -n $sqlvm.name -g $sqlvm.resourceGroup --license-type $SqlVmLicenseType -o json | ConvertFrom-Json
$finalStatus += $result
$report["SQLVMUpdated"] += $sqlvm.name
}
else {
if ($ForceStartOnResources) {
Write-Output "SQL VM '$($sqlvm.name)' is not running. Forcing start to update license..."
az vm start --resource-group $sqlvm.resourceGroup --name $sqlvm.name --no-wait yes
$sqlVmsToUpdate.Add($sqlvm) | Out-Null
}
}
}
}
catch {
Write-Error "An error occurred while updating SQL VMs: $_"
}
# --- Section: Update SQL Managed Instances (Stopped then Ready) ---
$sqlMIsToUpdate = [System.Collections.ArrayList]::new()
try {
if ($ForceStartOnResources) {
Write-Output "Seeking SQL Managed Instances that are stopped and require an update..."
$miQuery = if ($rgFilter) {
"[?licenseType!='${LicenseType}' && state!='Ready' && $rgFilter].{Name:name, State:state, ResourceGroup:resourceGroup}"
}
else {
"[?licenseType!='${LicenseType}' && state!='Ready'].{Name:name, State:state, ResourceGroup:resourceGroup}"
}
$offSQLMIs = az sql mi list --query $miQuery -o json | ConvertFrom-Json
foreach ($mi in $offSQLMIs) {
if ($mi.State -eq "Stopped") {
Write-Output "Starting SQL Managed Instance '$($mi.Name)' in RG '$($mi.ResourceGroup)'..."
az sql mi start --mi $mi.Name -g $mi.ResourceGroup --no-wait yes
}
$sqlMIsToUpdate.Add($mi) | Out-Null
}
}
Write-Output "Processing SQL Managed Instances that are running..."
$miRunningQuery = if ($rgFilter) {
"[?licenseType!='${LicenseType}' && state=='Ready' && $rgFilter]"
}
else {
"[?licenseType!='${LicenseType}' && state=='Ready']"
}
$runningMIs = az sql mi list --query $miRunningQuery -o json | ConvertFrom-Json
foreach ($mi in $runningMIs) {
Write-Output "Updating SQL Managed Instance '$($mi.name)' in RG '$($mi.resourceGroup)' to license type '$LicenseType'..."
$result = az sql mi update --name $mi.name --resource-group $mi.resourceGroup --license-type $LicenseType -o json | ConvertFrom-Json
$finalStatus += $result
$report["SQLMIUpdated"] += $mi.name
}
}
catch {
Write-Error "An error occurred while updating SQL Managed Instances: $_"
}
# --- Section: Update SQL Databases and Elastic Pools ---
try {
Write-Output "Querying SQL Servers within this subscription..."
$serverQuery = if ($rgFilter) { "[?$rgFilter]" } else { "[]" }
$servers = az sql server list --query $serverQuery -o json | ConvertFrom-Json
foreach ($server in $servers) {
# Update SQL Databases
Write-Output "Scanning SQL Databases on server '$($server.name)'..."
$dbs = az sql db list --resource-group $server.resourceGroup --server $server.name --query "[?licenseType!='$($LicenseType)' && licenseType!=null]" -o json | ConvertFrom-Json
foreach ($db in $dbs) {
Write-Output "Updating SQL Database '$($db.name)' on server '$($server.name)' to license type '$LicenseType'..."
$result = az sql db update --name $db.name --server $server.name --resource-group $server.resourceGroup --set licenseType=$LicenseType -o json | ConvertFrom-Json
$finalStatus += $result
$report["SQLDBUpdated"] += $db.name
}
# Update Elastic Pools
try {
Write-Output "Scanning Elastic Pools on server '$($server.name)'..."
$elasticPools = az sql elastic-pool list --resource-group $server.resourceGroup --server $server.name --query "[?licenseType!='$($LicenseType)' && licenseType!=null]" --only-show-errors -o json | ConvertFrom-Json
foreach ($pool in $elasticPools) {
Write-Output "Updating Elastic Pool '$($pool.name)' on server '$($server.name)' to license type '$LicenseType'..."
$result = az sql elastic-pool update --name $pool.name --server $server.name --resource-group $server.resourceGroup --set licenseType=$LicenseType --only-show-errors -o json | ConvertFrom-Json -ErrorAction SilentlyContinue
$finalStatus += $result
$report["ElasticPoolUpdated"] += $pool.name
}
}
catch {
Write-Output "Encountered an issue while updating Elastic Pools on server '$($server.name)'. Continuing..."
}
}
}
catch {
Write-Error "An error occurred while processing SQL Databases or Elastic Pools: $_"
}
# --- Section: Update SQL Instance Pools ---
try {
Write-Output "Searching for SQL Instance Pools that require a license update..."
$instancePoolsQuery = if ($rgFilter) { "[?$rgFilter]" } else { "[]" }
$instancePools = az sql instance-pool list --query $instancePoolsQuery -o json | ConvertFrom-Json
$poolsToUpdate = $instancePools | Where-Object { $_.licenseType -ne $LicenseType }
foreach ($pool in $poolsToUpdate) {
Write-Output "Updating SQL Instance Pool '$($pool.name)' in RG '$($pool.resourceGroup)' to license type '$LicenseType'..."
$result = az sql instance-pool update --name $pool.name --resource-group $pool.resourceGroup --license-type $LicenseType -o json | ConvertFrom-Json
$finalStatus += $result
$report["InstancePoolUpdated"] += $pool.name
}
}
catch {
Write-Error "An error occurred while updating SQL Instance Pools: $_"
}
# --- Section: Update DataFactory SSIS Integration Runtimes ---
try {
Write-Output "Processing DataFactory SSIS Integration Runtime resources..."
Get-AzDataFactoryV2 | Where-Object { $_.ProvisioningState -eq "Succeeded" } | ForEach-Object {
Get-AzDataFactoryV2IntegrationRuntime -ResourceGroupName $_.ResourceGroupName -DataFactoryName $_.DataFactoryName | Where-Object { $_.Type -eq "Managed" -and $_.State -ne "Starting" } | ForEach-Object {
if ($_.LicenseType -ne $LicenseType) {
# Update the license type to $LicenseType.
$result = Set-AzDataFactoryV2IntegrationRuntime -ResourceGroupName $_.ResourceGroupName -DataFactoryName $_.DataFactoryName -Name $_.Name -LicenseType $LicenseType -Force
$finalStatus += $result
$report["ADFSSISUpdated"] += $_.Name
Write-Host ([Environment]::NewLine + "-- DataFactory '$($_.DataFactoryName)' integration runtime updated to license type $LicenseType")
}
}
}
}
catch {
Write-Error "An error occurred while updating DataFactory SSIS Integration Runtimes: $_"
}
# --- Section: Finalize SQL VM updates for those that were started on-demand ---
$sqlvm = ""
try {
$updated = $true
while ($sqlVmsToUpdate.Count -gt 0) {
$sqlvm = $sqlVmsToUpdate[0]
$vmStatus = az vm get-instance-view --resource-group $sqlvm.resourceGroup --name $sqlvm.name --query "{PowerState:instanceView.statuses[?starts_with(code, 'PowerState/')].displayStatus | [0]}" -o json | ConvertFrom-Json
if ($vmStatus.PowerState -eq "VM running") {
Write-Output "Now updating SQL VM '$($sqlvm.name)' after forced start..."
$result = az sql vm update -n $sqlvm.name -g $sqlvm.resourceGroup --license-type $SqlVmLicenseType -o json | ConvertFrom-Json
$finalStatus += $result
$report["SQLVMUpdated"] += $sqlvm.name
Write-Output "Deallocating SQL VM '$($sqlvm.name)' post-update..."
az vm deallocate --resource-group $sqlvm.resourceGroup --name $sqlvm.name --no-wait yes
$sqlVmsToUpdate.RemoveAt(0)
$updated = $true
}
else {
if ($updated) {
Write-Host "Waiting for SQL VM '$($sqlvm.name)' to start..."
$updated = $false
}
else {
Write-Host "." -NoNewline
}
Start-Sleep -Seconds 30
}
}
}
catch {
Write-Error "An error occurred while finalizing SQL VM updates in subscription '$($sub.name)': $sqlvm"
}
# --- Section: Finalize SQL Managed Instance updates for those that were forced-start ---
$mi = ""
try {
$updated = $true
while ($sqlMIsToUpdate.Count -gt 0) {
$mi = $sqlMIsToUpdate[0]
$miStatus = az sql mi show --resource-group $mi.ResourceGroup --name $mi.Name -o json | ConvertFrom-Json
if ($miStatus.state -eq "Ready") {
Write-Output "Updating SQL Managed Instance '$($mi.Name)' after forced start..."
$result = az sql mi update --name $mi.Name --resource-group $mi.ResourceGroup --license-type $LicenseType -o json | ConvertFrom-Json
$finalStatus += $result
$report["SQLMIUpdated"] += $mi.Name
Write-Output "Stopping SQL Managed Instance '$($mi.Name)' post-update..."
az sql mi stop --resource-group $mi.ResourceGroup --mi $mi.Name --no-wait yes
$sqlMIsToUpdate.RemoveAt(0)
$updated = $true
}
else {
if ($updated) {
Write-Host "Waiting for SQL Managed Instance '$($mi.Name)' to be ready..."
$updated = $false
}
else {
Write-Host "." -NoNewline
}
Start-Sleep -Seconds 30
}
}
}
catch {
Write-Error "An error occurred while finalizing SQL Managed Instance updates in subscription '$($sub.name)': $mi"
}
}
catch {
Write-Error "An error occurred while processing subscription '$($sub.name)': $_"
}
}
$scriptEndTime = Get-Date
$totalDuration = $scriptEndTime - $scriptStartTime
# --- Final Report ---
Write-Output "`n===== Final Report ====="
Write-Output "Script started at: $scriptStartTime"
Write-Output "Script ended at: $scriptEndTime"
Write-Output "Total duration: $($totalDuration.ToString())"
Write-Output "`nResources updated by category:"
if ($report["SQLVMUpdated"].Count -gt 0) {
Write-Output "SQL VMs Updated: $($report["SQLVMUpdated"] -join ', ')"
} else {
Write-Output "SQL VMs Updated: None"
}
if ($report["SQLMIUpdated"].Count -gt 0) {
Write-Output "SQL Managed Instances Updated: $($report["SQLMIUpdated"] -join ', ')"
} else {
Write-Output "SQL Managed Instances Updated: None"
}
if ($report["SQLDBUpdated"].Count -gt 0) {
Write-Output "SQL Databases Updated: $($report["SQLDBUpdated"] -join ', ')"
} else {
Write-Output "SQL Databases Updated: None"
}
if ($report["ElasticPoolUpdated"].Count -gt 0) {
Write-Output "Elastic Pools Updated: $($report["ElasticPoolUpdated"] -join ', ')"
} else {
Write-Output "Elastic Pools Updated: None"
}
if ($report["InstancePoolUpdated"].Count -gt 0) {
Write-Output "SQL Instance Pools Updated: $($report["InstancePoolUpdated"] -join ', ')"
} else {
Write-Output "SQL Instance Pools Updated: None"
}
if ($report["ADFSSISUpdated"].Count -gt 0) {
Write-Output "SQL ADF SSIS: $($report["ADFSSISUpdated"] -join ', ')"
} else {
Write-Output "SQL ADF SSIS Updated: None"
}
@@ -78,8 +78,8 @@
}
],
"sku": {
"name": "VpnGw1",
"tier": "VpnGw1"
"name": "VpnGw1AZ",
"tier": "VpnGw1AZ"
},
"gatewayType": "Vpn",
"vpnType": "RouteBased",