From a0f5cf8796069e4132e00a6fae16346ee9c0001e Mon Sep 17 00:00:00 2001 From: "Alexander (Sasha) Nosov" Date: Fri, 9 May 2025 12:50:03 -0700 Subject: [PATCH] Renamed the arc LT script and added add'l parameters. --- .../modify-arc-license-type/README.md | 149 ++++++++ .../modify-arc-license-type.ps1 | 360 ++++++++++++++++++ .../modify-license-type/README.md | 11 + 3 files changed, 520 insertions(+) create mode 100644 samples/manage/azure-arc-enabled-sql-server/modify-arc-license-type/README.md create mode 100644 samples/manage/azure-arc-enabled-sql-server/modify-arc-license-type/modify-arc-license-type.ps1 diff --git a/samples/manage/azure-arc-enabled-sql-server/modify-arc-license-type/README.md b/samples/manage/azure-arc-enabled-sql-server/modify-arc-license-type/README.md new file mode 100644 index 00000000..85253360 --- /dev/null +++ b/samples/manage/azure-arc-enabled-sql-server/modify-arc-license-type/README.md @@ -0,0 +1,149 @@ +--- +services: Azure Arc-enabled SQL Server +platforms: Azure +author: anosov1960 +ms.author: sashan +ms.date: 05/01/2025 +--- + + +# Overview + +This script provides a scaleable solution to set or change the license type and/or enable or disable the ESU policy on all Azure-connected SQL Servers in a specified scope. + +You can specify a single subscription to scan, or provide a list of subscriptions as a .CSV file. +If not specified, all subscriptions your role has access to are scanned. + +# Prerequisites + +- You must have at least a *Azure Connected Machine Resource Administrator* role in each subscription you modify. +- The Azure extension for SQL Server is updated to version 1.1.2230.58 or newer. +- You must be connected to Azure AD and logged in to your Azure account. If your account have access to multiple tenants, make sure to log in with a specific tenant ID. + + +# Launching the script + +The script accepts the following command line parameters: + +| **Parameter**                                         | **Value**                                                                       | **Description** | +|:--|:--|:--| +|`-SubId`|subscription_id *or* a file_name|*Optional*: Subscription id or a .csv file with the list of subscriptions1. If not specified all subscriptions will be scanned| +|`-ResourceGroup` |resource_group_name|*Optional*: Limits the scope to a specific resource group| +|`-MachineName` |machine_name|*Optional*: Limits the scope to a specific machine| +|`-LicenseType` | "Paid", "PAYG" or "LicenseOnly"| *Optional*: Sets the license type to the specified value | +|`-ConsentToRecurringPAYG` | "Yes" or "No" |*Optional*. Consents to enabling the recurring PAYG billing. LicenseType must be "PAYG". Applies to CSP subscriptions only.| +|`-UsePcoreLicense` | "Yes", "No" | *Optional*. Enables unlimited virtualization license if the value is "Yes" or disables it if the value is "No". To enable, the license type must be "Paid" or "PAYG"| +|`-EnableESU` | "Yes", "No" | *Optional*. Enables the ESU policy the value is "Yes" or disables it if the value is "No". To enable, the license type must be "Paid" or "PAYG"| +|`-Force`| |*Optional*. Forces the change of the license type to the specified value on all installed extensions. If `-Force` is not specified, the `-LicenseType` value is set only if undefined. Ignored if `-LicenseType` is not specified| + +1You can create a .csv file using the following command and then edit to remove the subscriptions you don't want to scan. +```PowerShell +Get-AzSubscription | Export-Csv .\mysubscriptions.csv -NoTypeInformation +``` + +## Example 1 + +The following command will scan all the subscriptions to which the user has access to, and set the license type to "Paid" on all servers where license type is undefined. + +```PowerShell +.\modify-license-type.ps1 -LicenseType Paid +``` + +## Example 2 + +The following command will scan the subscription `` and set the license type value to "Paid" on all servers. + +```PowerShell +.\modify-license-type.ps1 -SubId -LicenseType Paid -Force +``` + +## Example 3 + +The following command will scan resource group `` in the subscription ``, set the license type value to "PAYG" and enable unlimited virtualization license on all servers in the specified resource group. + +```PowerShell +.\modify-license-type.ps1 -SubId -ResourceGroup -LicenseType PAYG -UsePcoreLicense Yes -Force +``` + +## Example 4 + +The following command will set License Type to "Paid" and enables ESU on all servers in the subscriptions `` and the resource group ``. + +```console +.\modify-license-type.ps1 -SubId -ResourceGroup -LicenseType Paid -EnableESU Yes -Force +``` + +## Example 5 + +The following command will disable ESU on all servers in the subscriptions ``. + +```console +.\modify-license-type.ps1 -SubId -EnableESU No +``` + +## Example 6 + +The following command will scan all subscriptions in the account, set the license type value to "PAYG" and consents to enabling recurring billing on all servers in the account. + +```PowerShell +.\modify-license-type.ps1 -LicenseType PAYG -ConsentToRecurringPAYG Yes -Force +``` +> [!NOTE] +> The recurring billing only supported in the CSP accounts. + +# 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 `` if you have access to more than one AAD tenants. + + ```console + Connect-AzureAD -TenantID + ``` + +1. Upload the script to your cloud shell using the following command: + + ```console + curl https://raw.githubusercontent.com/microsoft/sql-server-samples/master/samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 -o modify-license-type.ps1 + ``` + +1. Run the script. + +> [!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. + +# Running the script from a PC + + +Use the following steps to run the script in a PowerShell session on your PC. + +1. Copy the script to your current folder: + + ```console + curl https://raw.githubusercontent.com/microsoft/sql-server-samples/master/samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 -o modify-license-type.ps1 + ``` + +1. Make sure the NuGet package provider is installed: + + ```console + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + Install-packageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force + ``` + +1. Make sure the the Az module is installed. For more information, see [Install the Azure Az PowerShell module](https://learn.microsoft.com/powershell/azure/install-az-ps): + + ```console + Install-Module Az -Scope CurrentUser -Repository PSGallery -Force + ``` + +1. Connect to Azure AD and log in to your Azure account. You must specify `` if you have access to more than one AAD tenants. + + ```console + Connect-AzureAD -TenantID + Connect-AzAccount -TenantID (Get-AzureADTenantDetail).ObjectId + ``` + +1. Run the script. diff --git a/samples/manage/azure-arc-enabled-sql-server/modify-arc-license-type/modify-arc-license-type.ps1 b/samples/manage/azure-arc-enabled-sql-server/modify-arc-license-type/modify-arc-license-type.ps1 new file mode 100644 index 00000000..3d685c34 --- /dev/null +++ b/samples/manage/azure-arc-enabled-sql-server/modify-arc-license-type/modify-arc-license-type.ps1 @@ -0,0 +1,360 @@ + +param ( + [Parameter (Mandatory=$false)] + [string] $SubId, + [Parameter (Mandatory= $false)] + [string] $ResourceGroup, + [Parameter (Mandatory= $false)] + [string] $MachineName, + [Parameter (Mandatory= $false)] + [ValidateSet("PAYG","Paid","LicenseOnly", IgnoreCase=$false)] + [string] $LicenseType, + [Parameter (Mandatory= $false)] + [ValidateSet("Yes","No", IgnoreCase=$false)] + [string] $UsePcoreLicense, + [Parameter (Mandatory= $false)] + [ValidateSet("Yes","No", IgnoreCase=$false)] + [string] $EnableESU, + [Parameter (Mandatory= $false)] + [switch] $Force, + [Parameter (Mandatory= $false)] + [object] $ExclusionTags, + [Parameter (Mandatory= $false)] + [string] $TenantId, + [Parameter (Mandatory= $false)] + [switch] $ReportOnly +) + +function Connect-Azure { + [CmdletBinding()] + param( + [switch]$UseManagedIdentity + ) + + # 1) Detect environment + $envType = "Local" + if ($env:AZUREPS_HOST_ENVIRONMENT -and $env:AZUREPS_HOST_ENVIRONMENT -like 'cloud-shell*') { + $envType = "CloudShell" + } + elseif (($env:AZUREPS_HOST_ENVIRONMENT -and $env:AZUREPS_HOST_ENVIRONMENT -like 'AzureAutomation*') -or $PSPrivateMetadata.JobId) { + $envType = "AzureAutomation" + $UseManagedIdentity=$true + } + Write-Verbose "Environment detected: $envType" + + # 2) Ensure Az.PowerShell context + try { + $ctx = Get-AzContext -ErrorAction Stop + if (-not $ctx.Account) { throw } + Write-Output "Already connected to Azure PowerShell as: $($ctx.Account)" + } + catch { + Write-Output "Not connected to Azure PowerShell. Running Connect-AzAccount..." + if ($UseManagedIdentity -or $envType -eq 'AzureAutomation') { + Connect-AzAccount -Identity -ErrorAction Stop | Out-Null + } + else { + Connect-AzAccount -ErrorAction Stop | Out-Null + } + $ctx = Get-AzContext + Write-Output "Connected to Azure PowerShell as: $($ctx.Account)" + } + + # 3) Sync Azure CLI if available + if (Get-Command az -ErrorAction SilentlyContinue) { + try { + Write-Output "Check if az CLI is loged on..." + $acct = az account show --output json | ConvertFrom-Json + Write-Output "az: $($acct)" + if($null -eq $acct) + { + Write-Output "Azure CLI not logged in. Running az login..." + if ($UseManagedIdentity -or $envType -eq 'AzureAutomation') { + az login --identity | Out-Null + } + else { + az login | Out-Null + } + $acct = az account show --output json | ConvertFrom-Json + } + } + catch { + Write-Output "Azure CLI not logged in. Running az login..." + if ($UseManagedIdentity -or $envType -eq 'AzureAutomation') { + az login --identity | Out-Null + } + else { + az login | Out-Null + } + $acct = az account show --output json | ConvertFrom-Json + } + } + Write-Output "Azure CLI logged in as: $($acct.user.name)" + +} + +# Convert to hashtable explicitly +$tagTable = @{} +if($null -ne $ExclusionTags){ +if($ExclusionTags.GetType().Name -eq "Hashtable"){ + $tagTable = $ExclusionTags +}else{ + ($ExclusionTags | ConvertFrom-Json).PSObject.Properties | ForEach-Object { + $tagTable[$_.Name] = $_.Value + } +} +} + +# Ensure connection with both PowerShell and CLI. +Connect-Azure +$context = Get-AzContext -ErrorAction SilentlyContinue +Write-Output "Connected to Azure as: $($context.Account)" + +if (-not $TenantId) { + $TenantId = $context.Tenant.Id + Write-Output "No TenantId provided. Using current context TenantId: $TenantId" +} else { + Write-Output "Using provided TenantId: $TenantId" +} + +try{ + Import-Module AzureAD -UseWindowsPowerShell +} +catch{ + Write-Output "Can't import module AzureAD" +} +try{ + Import-Module Az.Accounts +}catch{ + Write-Output "Can't import module Az.Accounts" +} +try{ + Import-Module Az.ConnectedMachine +} +catch{ + Write-Output "Can't import module Az.ConnectedMachine" +} +try{ + Import-Module Az.ResourceGraph +} +catch{ + Write-Output "Can't import module Az.ResourceGraph" +} + +$modifiedResources = @() + +if ($SubId -like "*.csv") { + $subscriptions = Import-Csv $SubId +}elseif($SubId -ne "") { + Write-Output "Passed Subscription $($SubId)" + $subscriptions = [PSCustomObject]@{SubscriptionId = $SubId} | Get-AzSubscription -TenantID $TenantId +}else { + $subscriptions = Get-AzSubscription -TenantID $TenantId +} + +Write-Host ([Environment]::NewLine + "-- Scanning subscriptions --") + +foreach ($sub in $subscriptions) { + if ($sub.State -ne "Enabled") {continue} + + try { + Set-AzContext -SubscriptionId $sub.Id #Removed TenantID by Sunil + }catch { + write-host "Invalid subscription: $($sub.Id)" + {continue} + } + + # Consent tag enforcement on the CSP subscriptions + # Add or update ConsentToRecurringPAYG setting if applicable + if ($ConsentToRecurringPAYG -eq "Yes") { + $isPayg = ($LicenseType -eq "PAYG") -or ($settings["LicenseType"] -eq "PAYG") + if ($isPayg) { + if (-not $settings.ContainsKey("ConsentToRecurringPAYG") -or -not $settings["ConsentToRecurringPAYG"]["Consented"]) { + $settings["ConsentToRecurringPAYG"] = @{ + "Consented" = $true; + "ConsentTimestamp" = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + } + $WriteSettings = $true + } + } + } + + Write-Output "Collecting list of resources to update" + $query = " + resources + | where type =~ 'microsoft.hybridcompute/machines/extensions' + | where subscriptionId =~ '$($sub.Id)' + | extend extensionPublisher = tostring(properties.publisher), + extensionType = tostring(properties.type), provisioningState = tostring(properties.provisioningState) + | parse id with * '/providers/Microsoft.HybridCompute/machines/' machineName '/extensions/' * + | where extensionPublisher =~ 'Microsoft.AzureData' + | where provisioningState =~ 'Succeeded' + | where properties.settings.LicenseType!='$LicenseType' + | join kind=leftouter ( + resources + | where type == 'microsoft.azurearcdata/sqlserverinstances' + | project machineName= name, edition = properties.edition, mytags = tags" + + <#if($tagTable.Keys.Count -gt 0) { + $query += "| where " + $tagcount = $tagTable.Keys.Count + foreach ($tag in $tagTable.Keys) { + $tagcount -- + $query += "(mytags['$($tag)'] != '$($tagTable[$tag])')" + if($tagcount -gt 0) { + $query += " and " + } + } + }#> + + $query += ") on machineName" + + if ($ResourceGroup) { + $query += "| where resourceGroup =~ '$($ResourceGroup)'" + } + + if ($MachineName) { + $query += "| where machineName =~ '$($MachineName)'" + } + + $query += " + | project machineName, extensionName = name, resourceGroup, location, subscriptionId, extensionPublisher, extensionType, properties,provisioningState, edition + " + $query + $resources = Search-AzGraph -Query "$($query)" + Write-Output "Found $($resources.Count) resource(s) to update" + $count = $resources.Count + + while($count -gt 0) { + $count-=1 + Write-Output "VM-$($count)" + write-Output "VM - $($resources[$count].MachineName)" + $setID = @{ + MachineName = $resources[$count].MachineName + Name = $resources[$count].extensionName + ResourceGroup = $resources[$count].resourceGroup + Location = $resources[$count].location + SubscriptionId = $resources[$count].subscriptionId + Publisher = $resources[$count].extensionPublisher + ExtensionType = $resources[$count].extensionType + Edition = $resources[$count].edition + } + + write-Output "VM - $($setID.MachineName)" + write-Output " ResourceGroup - $($setID.ResourceGroup)" + write-Output " Location - $($setID.Location)" + write-Output " SubscriptionId - $($setID.SubscriptionId)" + write-Output " ExtensionType - $($setID.ExtensionType)" + + # Get connected machine info + $sqlvm = Get-AzConnectedMachine -Name $setID.MachineName -ResourceGroup $setID.ResourceGroup | Select-Object Name, Tags, Status + + # Collect data before modification + $modifiedResources += [PSCustomObject]@{ + TenantID = $TenantId + SubID = $setID.SubscriptionId + ResourceName = $setID.MachineName + ResourceType = $setID.ExtensionType + Status = $sqlvm.Status + OriginalLicenseType = $settings.LicenseType + ResourceGroup = $setID.ResourceGroup + Location = $setID.Location + Cores = $sqlvm.Tags["NumberOfCores"] # Adjust if needed + } + + $excludedByTags = $false + foreach ($tag in $tagTable.Keys){ + if($sqlvm.Tags.ContainsKey($tag)) + { + if($sqlvm.Tags[$tag] -eq $tagTable[$tag]){ + $excludedByTags=$true + $value = $tagTable[$tag] + write-Output "Exclusion tag $($tag):$value. Skipping..." + Break; + } + } + } + if(!$excludedByTags){ + + + $WriteSettings = $false + $settings = $resources[$count].properties.settings | ConvertTo-Json | ConvertFrom-Json + $ext = Get-AzConnectedMachineExtension -Name $setID.Name -ResourceGroupName $setID.ResourceGroup -MachineName $setID.MachineName + if($ext.ProvisioningState -ne "Succeeded") { + write-Output "Extension is not in a valid state. Skipping..." + {continue} + } else { + $LO_Allowed = (!$settings["enableExtendedSecurityUpdates"] -and !$EnableESU) -or ($EnableESU -eq "No") + + write-Output " LicenseType - $($settings.LicenseType)" + + if ($LicenseType) { + if (($LicenseType -eq "LicenseOnly") -and !$LO_Allowed) { + write-Output "ESU must be disabled before license type can be set to $($LicenseType)" + } else { + if ($ext.Setting["LicenseType"]) { + if ($Force) { + $ext.Setting["LicenseType"] = $LicenseType + $WriteSettings = $true + } + } else { + $ext.Setting["LicenseType"] = $LicenseType + $WriteSettings = $true + } + } + } + + if ($setID.Edition -eq "Express") { + $LicenseType = "LicenseOnly" + } + + if ($EnableESU) { + if (($ext.Setting["LicenseType"] -in ("Paid","PAYG")) -or ($EnableESU -eq "No")) { + $ext.Setting["enableExtendedSecurityUpdates"] = ($EnableESU -eq "Yes") + $ext.Setting["esuLastUpdatedTimestamp"] = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + $WriteSettings = $true + } else { + write-Output "The configured license type does not support ESUs" + } + } + + if ($UsePcoreLicense) { + if (($ext.Setting["LicenseType"] -in ("Paid","PAYG")) -or ($UsePcoreLicense -eq "No")) { + $ext.Setting["UsePhysicalCoreLicense"] = @{ + "IsApplied" = ($UsePcoreLicense -eq "Yes"); + "LastUpdatedTimestamp" = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + } + $WriteSettings = $true + } else { + write-Output "The configured license type does not support ESUs" + } + } + write-Output " Write Settings - $($WriteSettings)" + if (-not $ReportOnly) { + If ($WriteSettings) { + try { + $ext | Set-AzConnectedMachineExtension -Name $setID.Name -ResourceGroupName $setID.ResourceGroup -MachineName $setID.MachineName -NoWait -ErrorAction SilentlyContinue | Out-Null + Write-Output "Updated -- Resource group: [$($setID.ResourceGroup)], Connected machine: [$($setID.MachineName)]" + } catch { + write-Output "The request to modify the extension object failed with the following error:" + continue + } + } + } else { + Write-Output "ReportOnly mode enabled. Skipping modification for: $($setID.MachineName)" + } + } + } + } +} + +# Export modified resource data to CSV +if ($modifiedResources.Count -gt 0) { + $csvPath = "ModifiedResources_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" + $modifiedResources | Export-Csv -Path $csvPath -NoTypeInformation + Write-Output "CSV report saved to: $csvPath" +} else { + Write-Output "No resources were marked for modification. No CSV generated." +} + +write-Output "Arc Update Script completed" diff --git a/samples/manage/azure-arc-enabled-sql-server/modify-license-type/README.md b/samples/manage/azure-arc-enabled-sql-server/modify-license-type/README.md index 2a86d276..1ed33ab8 100644 --- a/samples/manage/azure-arc-enabled-sql-server/modify-license-type/README.md +++ b/samples/manage/azure-arc-enabled-sql-server/modify-license-type/README.md @@ -6,6 +6,17 @@ ms.author: sashan ms.date: 05/01/2025 --- +# About this sample + +- **Applies to:** Arc-enabled SQL Server +- **Workload:** n/a +- **Programming Language:** PowerShell +- **Authors:** Alexander (Sasha) Nosov +- **Update history:** + + 05/01/2025 - added the consent parameter for teh CSP-managed subscriptions + + 05/09/2025 - added *-ReportOnly* and *-TenandId* parameters # Overview