From ed771c7203a5e27131ae786dd5e1f4a764e6dd40 Mon Sep 17 00:00:00 2001 From: "Alexander (Sasha) Nosov" Date: Thu, 9 Feb 2023 16:44:32 -0800 Subject: [PATCH 1/2] initial draft --- .../modify-license-type/README.md | 75 ++++++++++ .../modify-license-type.ps1 | 137 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 samples/manage/azure-arc-enabled-sql-server/modify-license-type/README.md create mode 100644 samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 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 new file mode 100644 index 00000000..58252b43 --- /dev/null +++ b/samples/manage/azure-arc-enabled-sql-server/modify-license-type/README.md @@ -0,0 +1,75 @@ +--- +services: Azure Arc-enabled SQL Server +platforms: Azure +author: anosov1960 +ms.author: sashan +ms.date: 2/09/2023 +--- + + +# Overview + + +This script allows you to to set or change the license type on all Azure-connected SQL Servers +in a specific subscription, a list of subscriptions or the entire account. By default, it sets the specified license type value on the servers where it is undefined. But you can request to set it on all servers in 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. + + +# Required permissions + +You must be at least a *Contributor* of each subscription you modify. + +# 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| +|-LicenceType | "Paid" (default), "PAYG" or "LicenseOnly"| Specifies the license type value | +|-All|\$True or \$False (default)|Optional: Set the new license type value only if undefined| + +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 "PAYG". + +```PowerShell +.\update-license-type.ps1 -LicenseType "PAYG" -All +``` + +## Example 2 + +The following command will scan the subscription `` and set the license type value to "Paid" on the servers where it is undefined. + +```PowerShell +.\update-license-type.ps1 -SubId -LicenseType "Paid" +``` + +# Running the script using Cloud Shell + +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). + +2. Upload the script to the 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 + ``` + +3. Run the script. + + ```console + .//modify-license-type.ps1 -LicenseType "Paid" + ``` + +> [!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. + diff --git a/samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 b/samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 new file mode 100644 index 00000000..6772f4b8 --- /dev/null +++ b/samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 @@ -0,0 +1,137 @@ +# +# This script provides a scaleable solution to set or change the license type on all Azure-connected SQL Servers +# in a specific subscription, a list of subscruiptions or the entire account. By default, it sets the new license +# type value only on the servers where it is undefined. +# +# You can specfy a single subscription to scan, or provide subscriptions as a .CSV file with the list of IDs. +# If not specified, all subscriptions your role has access to are scanned. +# +# The script accepts the following command line parameters: +# +# -SubId [subscription_id] | [csv_file_name] (Accepts a .csv file with the list of subscriptions) +# -LicenceType [license_type_value] (Specific LT value) +# -All (Optional. Set the new license type value only if undefined) +# + +param ( + [Parameter (Mandatory= $false)] + [string] $SubId, + [Parameter (Mandatory= $false)] + [string] $LicenseType="Paid", + [Parameter (Mandatory= $false)] + [string] $SkipServers, + [Parameter (Mandatory= $false)] + [boolean] $All=$false +) + +function CheckModule ($m) { + + # This function ensures that the specified module is imported into the session + # If module is already imported - do nothing + + if (!(Get-Module | Where-Object {$_.Name -eq $m})) { + # If module is not imported, but available on disk then import + if (Get-Module -ListAvailable | Where-Object {$_.Name -eq $m}) { + Import-Module $m + } + else { + + # If module is not imported, not available on disk, but is in online gallery then install and import + if (Find-Module -Name $m | Where-Object {$_.Name -eq $m}) { + Install-Module -Name $m -Force -Verbose -Scope CurrentUser + Import-Module $m + } + else { + + # If module is not imported, not available and not in online gallery then abort + write-host "Module $m not imported, not available and not in online gallery, exiting." + EXIT 1 + } + } + } +} + + +# +# Suppress warnings +# +Update-AzConfig -DisplayBreakingChangeWarning $false + +# Load required modules +$requiredModules = @( + "Az.Accounts", + "Az.ConnectedMachine", + "Az.ResourceGraph" +) +$requiredModules | Foreach-Object {CheckModule $_} + +# Subscriptions to scan + +if ($SubId -like "*.csv") { + $subscriptions = Import-Csv $SubId +}elseif($SubId -ne $null){ + $subscriptions = [PSCustomObject]@{SubscriptionId = $SubId} | Get-AzSubscription +}else{ + $subscriptions = Get-AzSubscription +} + + +Write-Host ([Environment]::NewLine + "-- Scanning subscriptions --") + +# Scan arc-enabled servers in each subscription + +foreach ($sub in $subscriptions){ + + if ($sub.State -ne "Enabled") {continue} + + try { + Set-AzContext -SubscriptionId $sub.Id + }catch { + write-host "Invalid subscription: " $sub.Id + {continue} + } + + $query = " + resources + | where type =~ 'microsoft.hybridcompute/machines/extensions' + | extend extensionPublisher = tostring(properties.publisher), extensionType = tostring(properties.type) + | where extensionPublisher =~ 'Microsoft.AzureData' + | parse id with * '/providers/Microsoft.HybridCompute/machines/' machineName '/extensions/' * + | project machineName, extensionName = name, resourceGroup, location, subscriptionId, extensionPublisher, extensionType, properties + " + + Search-AzGraph -Query $query | ForEach-Object { + + $setID = @{ + MachineName = $_.MachineName + Name = $_.extensionName + ResourceGroup = $_.resourceGroup + Location = $_.location + SubscriptionId = $_.subscriptionId + Publisher = $_.extensionPublisher + ExtensionType = $_.extensionType + } + $getID = @{ + Name = $_.extensionName + MachineName = $_.MachineName + ResourceGroup = $_.resourceGroup + SubscriptionId = $_.subscriptionId + } + $old = Get-AzConnectedMachineExtension @getID + $settings = @{} + foreach( $property in $_.properties.settings.psobject.properties.name ){ $settings[$property] = $_.properties.settings.$property } + if (-not $all) { + if (-not $settings.ContainsKey("LicenseType")) { $settings["LicenseType"] = $LicenseType } + } else { + $settings["LicenseType"] = $LicenseType + } + if ($_.properties.provisioningState -ne "Succeeded") { + Write-Warning "Skipping extension on server $($_.machineName) because it's state is $($_.properties.provisioningState)"; + } else { + Set-AzConnectedMachineExtension @setId -Settings $settings -NoWait + $new = Get-AzConnectedMachineExtension @getID + } + } +} + + \ No newline at end of file From e34a0b1a7d0e44058c6fff3d01cc62862048e6a6 Mon Sep 17 00:00:00 2001 From: "Alexander (Sasha) Nosov" Date: Fri, 10 Feb 2023 16:31:52 -0800 Subject: [PATCH 2/2] Final cleanup --- .../modify-license-type/README.md | 12 +- .../modify-license-type.ps1 | 120 ++++++++++++------ 2 files changed, 90 insertions(+), 42 deletions(-) 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 58252b43..fffb7fe3 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 @@ -11,15 +11,17 @@ ms.date: 2/09/2023 This script allows you to to set or change the license type on all Azure-connected SQL Servers -in a specific subscription, a list of subscriptions or the entire account. By default, it sets the specified license type value on the servers where it is undefined. But you can request to set it on all servers in scope. +on a specific resource, in a single resource group, a specific subscription, a list of subscriptions or the entire account. By default, it sets the specified license type value on the servers where it is undefined. But you can request to set it on all servers in the selected 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. +If not specified, all subscriptions your role has access to are scanned. + +If the license type is not specified, the value "Paid" is used. # Required permissions -You must be at least a *Contributor* of each subscription you modify. +You must have at least a *Contributor* role in each subscription you modify. # Launching the script @@ -28,6 +30,8 @@ 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| +|-ResourceGroup |resource_group_name|Optional: Limit the scope to a specific resource group| +|-MachineName |machine_name|Optional: Limit the scope to a specific machine| |-LicenceType | "Paid" (default), "PAYG" or "LicenseOnly"| Specifies the license type value | |-All|\$True or \$False (default)|Optional: Set the new license type value only if undefined| @@ -66,7 +70,7 @@ Use the following steps to run the script in Cloud Shell. 3. Run the script. ```console - .//modify-license-type.ps1 -LicenseType "Paid" + .//modify-license-type.ps1 -LicenseType "PAYG" ``` > [!NOTE] diff --git a/samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 b/samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 index 6772f4b8..4bc1ee77 100644 --- a/samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 +++ b/samples/manage/azure-arc-enabled-sql-server/modify-license-type/modify-license-type.ps1 @@ -8,19 +8,23 @@ # # The script accepts the following command line parameters: # -# -SubId [subscription_id] | [csv_file_name] (Accepts a .csv file with the list of subscriptions) -# -LicenceType [license_type_value] (Specific LT value) +# -SubId [subscription_id] | [csv_file_name] (Limit scope to specific subscriptions. Accepts a .csv file with the list of subscriptions) +# -ResourceGroup [resource_goup] (Limit scope to a specific resoure group) +# -MachineName [machine_name] (Limit scope to a specific machine) +# -LicenseType [license_type_value] (Specific LT value) # -All (Optional. Set the new license type value only if undefined) # param ( [Parameter (Mandatory= $false)] [string] $SubId, + [Parameter (Mandatory= $false)] + [string] $ResourceGroup, + [Parameter (Mandatory= $false)] + [string] $MachineName, [Parameter (Mandatory= $false)] [string] $LicenseType="Paid", [Parameter (Mandatory= $false)] - [string] $SkipServers, - [Parameter (Mandatory= $false)] [boolean] $All=$false ) @@ -51,6 +55,44 @@ function CheckModule ($m) { } } +function ObjectToHashtable { + [CmdletBinding()] + [OutputType('hashtable')] + param ( + [Parameter(ValueFromPipeline)] + $InputObject + ) + process { + ## Return null if the input is null. This can happen when calling the function + ## recursively and a property is null + if ($null -eq $InputObject) { + return $null + } + ## Check if the input is an array or collection. If so, we also need to convert + ## those types into hash tables as well. This function will convert all child + ## objects into hash tables (if applicable) + if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { + $collection = @( + foreach ($object in $InputObject) { + ObjectToHashtable -InputObject $object + } + ) + ## Return the array but don't enumerate it because the object may be pretty complex + Write-Output -NoEnumerate $collection + } elseif ($InputObject -is [psobject]) { + ## If the object has properties that need enumeration, cxonvert it to its own hash table and return it + $hash = @{} + foreach ($property in $InputObject.PSObject.Properties) { + $hash[$property.Name] = ObjectToHashtable -InputObject $property.Value + } + $hash + } else { + ## If the object isn't an array, collection, or other object, it's already a hash table + ## So just return it. + $InputObject + } + } +} # # Suppress warnings @@ -87,51 +129,53 @@ foreach ($sub in $subscriptions){ try { Set-AzContext -SubscriptionId $sub.Id }catch { - write-host "Invalid subscription: " $sub.Id + write-host "Invalid subscription: $($sub.Id)" {continue} } $query = " - resources - | where type =~ 'microsoft.hybridcompute/machines/extensions' - | extend extensionPublisher = tostring(properties.publisher), extensionType = tostring(properties.type) - | where extensionPublisher =~ 'Microsoft.AzureData' - | parse id with * '/providers/Microsoft.HybridCompute/machines/' machineName '/extensions/' * - | project machineName, extensionName = name, resourceGroup, location, subscriptionId, extensionPublisher, extensionType, properties + resources + | where type =~ 'microsoft.hybridcompute/machines/extensions' + | extend extensionPublisher = tostring(properties.publisher), extensionType = tostring(properties.type), provisioningState = tostring(properties.provisioningState) + | where extensionPublisher =~ 'Microsoft.AzureData' + | where provisioningState =~ 'Succeeded' + | parse id with * '/providers/Microsoft.HybridCompute/machines/' machineName '/extensions/' * + | project machineName, extensionName = name, resourceGroup, location, subscriptionId, extensionPublisher, extensionType, properties " - Search-AzGraph -Query $query | ForEach-Object { - + if ($MachineName) {$query += "| where machineName =~ '$($MachineName)'"} + if ($ResourceGroup) {$query += "| where resourceGroup =~ '$($ResourceGroup)'"} + + $resources = Search-AzGraph -Query "$($query) | where subscriptionId =~ '$($sub.Id)'" + foreach ($r in $resources) { + $setID = @{ - MachineName = $_.MachineName - Name = $_.extensionName - ResourceGroup = $_.resourceGroup - Location = $_.location - SubscriptionId = $_.subscriptionId - Publisher = $_.extensionPublisher - ExtensionType = $_.extensionType + MachineName = $r.MachineName + Name = $r.extensionName + ResourceGroup = $r.resourceGroup + Location = $r.location + SubscriptionId = $r.subscriptionId + Publisher = $r.extensionPublisher + ExtensionType = $r.extensionType } - $getID = @{ - Name = $_.extensionName - MachineName = $_.MachineName - ResourceGroup = $_.resourceGroup - SubscriptionId = $_.subscriptionId - } - $old = Get-AzConnectedMachineExtension @getID + $settings = @{} - foreach( $property in $_.properties.settings.psobject.properties.name ){ $settings[$property] = $_.properties.settings.$property } - if (-not $all) { - if (-not $settings.ContainsKey("LicenseType")) { $settings["LicenseType"] = $LicenseType } + $settings = $r.properties.settings | ConvertTo-Json | ConvertFrom-Json | ObjectToHashtable + + if ($settings.ContainsKey("LicenseType")) { + if ($All) { + if ($settings["LicenseType"] -ne $LicenseType ) { + $settings["LicenseType"] = $LicenseType + Write-Host "Resource group: [$($r.resourceGroup)] Connected machine: [$($r.MachineName)] : License type: [$($settings["LicenseType"])]" + Set-AzConnectedMachineExtension @setId -Settings $settings -NoWait + } + } } else { - $settings["LicenseType"] = $LicenseType + $settings["LicenseType"] = $LicenseType + Write-Host "Resource group: [$($r.resourceGroup)] Connected machine: [$($r.MachineName)] : License type: [$($settings["LicenseType"])]" + Set-AzConnectedMachineExtension @setId -Settings $settings } - if ($_.properties.provisioningState -ne "Succeeded") { - Write-Warning "Skipping extension on server $($_.machineName) because it's state is $($_.properties.provisioningState)"; - } else { - Set-AzConnectedMachineExtension @setId -Settings $settings -NoWait - $new = Get-AzConnectedMachineExtension @getID - } - } + } } \ No newline at end of file