Merge pull request #870 from anosov1960/master

Major rewrite of the script
This commit is contained in:
Alexander (Sasha) Nosov
2021-01-12 19:46:26 -08:00
committed by GitHub
3 changed files with 246 additions and 157 deletions
+67 -10
View File
@@ -3,12 +3,13 @@ services: Azure SQL
platforms: Azure
author: anosov1960
ms.author: sashan
ms.date: 12/17/2020
ms.date: 1/11/2021
---
# Overview
This script is provided to help you manage the SQL Server licenses that are consumed by the SQL Servers deployed to Azure. The script writes the results to a `sql-license-usage.csv` file. If the file with this name already exists, the new results will be appended to it. The report includes the following information for each scanned subscription as well as the totals for each category.
This script provides a simple solution to analyze and track the consolidated utilization of SQL Server licenses by all of the SQL resources in a specific subscription or the entire the account. By default, the script scans all subscriptions the user account has access. Alternatively, you can specify a single subscription or a .CSV file with a list of subscription. The usage report includes the following information for each scanned subscription.
| **Category** | **Description** |
|:--|:--|
|Date|Date of the scan|
@@ -24,10 +25,67 @@ This script is provided to help you manage the SQL Server licenses that are cons
|Developer vCores|Total vCores used by SQL Server Developer edition|
|Express vCores|Total vCores used by SQL Server Express edition|
The following resources are in scope for the license utilization analysis:
- Azure SQL databases (vCore-based purchasing model only<sup>1</sup>)
- Azure SQL elastic pools (vCore-based purchasing model only<sup>1</sup>)
- Azure SQL managed instances
- Azure SQL instance pools
- Azure Data Factory SSIS integration runtimes
- SQL Servers in Azure virtual machines
- SQL Servers in Azure virtual machines hosted in Azure dedicated host
<sup>1</sup>The DTU-based resources are not eligible for Azure Hybrid Benefit or HADR benefit.
# Launching the script
The script accepts the following command line parameters:
| **Parameter** | **Value** | **Description** |
|:--|:--|:--|
|-SubId|subscription_id *or* a file_name|Accepts a .csv file with the list of subscriptions<sup>2</sup>|
|-UseInRunbook||Must be specified when executed as a Runbook|
|-Server|[protocol:]server[instance_name][,port]|Required to save data to the database|
|-Database|database_name|Required to save data to the database|
|-Username|user_name|Required to save data to the database|
|-Password|password|Required to save data to the database, must be passed as secure string|
|-FilePath|csv_file_name|Required to save data in a .csv format. Ignored if database parameters are specified|
<sup>2</sup>You 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
```
If both database parameters and *FilePath* are omitted, the script will write the results to a `.\sql-license-usage.csv` file. The file is created automatically. If the file already exists, the consecutive scans will append the results results to it. If the database parameters are specified, the data will be saved
>[!NOTE]
> - The usage data is a snapshot at the time of the script execution based on the size of the deployed SQL resources in vCores.
> - For IaaS workloads, such as SQL Server in Virtual Machines or SSIS integration runtimes, each vCPU is counted as one vCore.
> - For PaaS workloads, each vCore of Business Critical service tier is counted as one Enterprise vCore and each vCore of General Purpose service tier is counted as one Standard vCore.
> - The values AHB ECs and PAYG ECs are reserved for the future use and should be ignored
## Example 1
The following command will scan all the subscriptions in the account and save the results in `.\sql-license-usage.csv`
```PowerShell
.\sql-license-usage.ps1
```
## Example 2
The following command will scan the subscription `<sub_id>` and save the results in `<my_csv_file>` file.
```PowerShell
.\sql-license-usage.ps1 -SubId <sub_id> -FilePath .\<my_csv_file>.csv
```
## Example 3
The following command will scan all the subscriptions in the account and save the results in a SQL database `<db_name>` on a SQL Server instance `<sql_server_name>.database.windows.net`.
```PowerShell
.\sql-license-usage.ps1 -ServerInstance <server_name>.database.windows.net -Database <db_name> -Username <user_name> -Password $pwd
```
# Running the script using Cloud Shell
@@ -41,21 +99,20 @@ Use the following steps to calculate the SQL Server license usage:
curl https://raw.githubusercontent.com/microsoft/sql-server-samples/master/samples/manage/azure-hybrid-benefit/sql-license-usage.ps1 -o sql-license-usage.ps1
```
3. Run the script with a specific subscriptions ID or the file name as the parameter. The file should be used if you need to scan a subset of the subscriptions. If the parameter is not specified, the script will scan all the subscriptions in your account.
3. Run the script with a set of parameters that reflect your desired configuration.
```console
./sql-license-usage.ps1 <subscription ID> or <filename>.csv
./sql-license-usage.ps1 <parameters>
```
If the a file is specified, it must be a `.csv` file with the list of subscriptions. To create a file containing all subscriptions in your account, use the following command. You can then edit the file to remove the subscriptions you don't want to scan.
```console
Get-AzSubscription | Export-Csv .\mysubscriptions.csv -NoTypeInformation
```
> [!NOTE]
> - To paste the commands into the shell, use `Ctrl-Shift-V` on Windows or `Cmd-v` on MacOS.
> - The `curl` command will copy the script directly to the home folder associated with your Cloud Shell session.
# Tracking SQL license usage over time
You can track your license utilization over time by periodically running this script. Each new scan will add the results to `sql-license-usage.csv`, which you can use for reporting the license usage over time in Excel or other tools. To run this script on schedule using Azure automation, read [Create a PowerShell runbook tutorial](https://docs.microsoft.com/azure/automation/learn/automation-tutorial-runbook-textual-powershell).
You can track your license utilization over time by periodically running this script. To schedule automatic execution of the script, create a PowerShell runbook using an Azure Automation account. See the [Runbook tutorial](https://docs.microsoft.com/en-us/azure/automation/learn/automation-tutorial-runbook-textual-powershell) for the details of how to create a PowerShell runbook. Because the script accesses the resources across multiple subscriptions, the runbook must be able to authenticate using the Run As account that was automatically created when you created your Automation account. The logic required for the Runbooks is part of the script.
>[!IMPORTANT]
> - When running the script as a runbook, use a database to ensure that the results can be analyzed outside of the runbook.
> - You must specify a *-UseInRunbook* switch to ensure that the runbook is authenticated using the Run As account.
@@ -1,112 +0,0 @@
# ----------------------------------------------------------------------------------
#
# Copyright Microsoft Corporation
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ---------------------------------------------------------------------------------
#
# This script calculates the usage of different types of SQL Server licenses by the SQL resources
# in the specific subscriptions.
#
# NOTE:
# 1. The script requires a .csv file with the list of subscriptions. Use Export-Csv comlet to create the file.
# 2. SQL Database resources that use the DTU based business model are excluded.
#
# Set the subscription Id
$SubcriptionId = read-host -Prompt "Enter Subscription ID"
Set-AzContext -SubscriptionId $SubcriptionId
# Variables to keep track of SQL VMs and VCPUs count
$total_std_vcores = 0
$total_ent_vcores = 0
#Get all SQL databases in the subscription
$databases = Get-AzSqlServer | Get-AzSqlDatabase
# Get the databases with License Included and add to VCore count
foreach ($db in $databases){
if (($db.SkuName -ne "ElasticPool") -and ($db.LicenseType -eq "LicenseIncluded")) {
if ($db.Edition -eq "BusinessCritical") {
$total_ent_vcores += $db.Capacity
} elseif ($db.Edition -eq "GeneralPurpose") {
$total_std_vcores += $db.Capacity
}
}
}
#Get all SQL elastic pools in the subscription
$pools = Get-AzSqlServer | Get-AzSqlElasticPool
# Get the elastic pools with License Included and and add to VCore count
foreach ($pool in $pools){
if ($pool.LicenseType -eq "LicenseIncluded") {
if ($pool.Edition -eq "BusinessCritical") {
$total_ent_vcores += $pool.Capacity
} elseif ($pool.Edition -eq "GeneralPurpose") {
$total_std_vcores += $pool.Capacity
}
}
}
#Get all SQL managed instances in the subscription
$instances = Get-AzSqlInstance
# Get the SQL managed instances with License Included and add to VCore count
foreach ($ins in $instances){
if (($ins.InstancePoolName -eq $null) -and ($ins.LicenseType -eq "LicenseIncluded")) {
if ($ins.Sku.Tier -eq "BusinessCritical") {
$total_ent_vcores += $ins.VCores
} elseif ($ins.Sku.Tier -eq "GeneralPurpose") {
$total_std_vcores += $ins.VCores
}
}
}
#Get all instance pools in the subscription
$ipools = Get-AzSqlInstancePool
# Get the instance pools with License Included and add to VCore count
foreach ($ip in $ipools){
if ($ip.LicenseType -eq "LicenseIncluded") {
if ($ip.Edition -eq "BusinessCritical") {
$total_ent_vcores += $ip.VCores
} elseif ($ip.Edition -eq "GeneralPurpose") {
$total_std_vcores += $ip.VCores
}
}
}
#Get All Sql VMs with AHB license configured
$sql_vms= Get-AzSqlVM | where {$_.LicenseType.Contains("AHUB")}
# Get the VM size, match it with the corresponding VCPU count and add to VCore count
foreach ($sql_vm in $sql_vms){
$vm = Get-AzVm -Name $sql_vm.Name -ResourceGroupName $sql_vm.ResourceGroupName
$vm_size = $vm.HardwareProfile.VmSize
# Select first size and get the VCPus available
$size_info = Get-AzComputeResourceSku | where {$_.ResourceType.Contains('virtualMachines') -and $_.Name -like $vm_size} | Select-Object -First 1
# Save the VCPU count
$vcpu= $size_info.Capabilities | Where-Object {$_.name -eq "vCPUsAvailable"}
if ($vcpu){
$data = [pscustomobject]@{vm_resource_uri=$vm.Id;sku=$sql_vm.Sku;size=$vm_size;vcpus=$vcpu.value}
$array += $data
if ($data.sku -like "Enterprise"){
$total_ent_vcores += $data.vcpus
}elseif ($data.sku -like "Standard"){
$total_std_vcores += $data.vcpus
}
}
}
Write-Host "Total number of VCores for SQL Enterprise: " $total_ent_vcores
Write-Host "Total number of VCores for SQL Standard: " $total_std_vcores
@@ -1,4 +1,4 @@
# ----------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------
#
# Copyright Microsoft Corporation
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,37 +12,147 @@
# limitations under the License.
# ---------------------------------------------------------------------------------
#
# Sample script to calculate the consolidated SQL Server license usage by all of the SQL resources in a specific subscription or the entire the account.
#
# This script accepts a .csv file or a subscription ID as a parameter. The file must include a list of subscriptions to be scanned for the license usage. You can create
# such a file by the following command and then edit to remove the subscriptions you don't want to scan:
# > Get-AzSubscription | Export-Csv .\mysubscriptions.csv -NoTypeInformation
#
# If no parameter is provided, the script will prompt for a file or subscriptiobn ID. If no value is provided, the scipt will scan all the subscriptions you account
# has access to.
# This script provided a simple solution to analyze and track the consolidated utilization of SQL Server licenses
# by all of the SQL resources in a specific subscription or the entire the account. By default, the script scans
# all subscriptions the user account has access. Alternatively, you can specify a single subscription or a .CSV file
# with a list of subscription. The usage report includes the following information for each scanned subscription.
#
# The following resources are in scope for the license utilization analysis:
# - Azure SQL databases (vCore-based purchasing model only)
# - Azure SQL elastic pools (vCore-based purchasing model only)
# - Azure SQL managed instances
# - Azure SQL instance pools
# - Azure Data Factory SSIS integration runtimes
# - SQL Servers in Azure virtual machines
# - SQL Servers in Azure virtual machines hosted in Azure dedicated host
#
# NOTE: The script does not calculate usage for Azure SQL resources that use the DTU-based purchasing model
#
# The script accepts the following command line parameters:
#
# -SubId [subscription_id] | [csv_file_name] (Accepts a .csv file with the list of subscriptions)
# -UseInRunbook (Required when executed as a Runbook)
# -Server [protocol:]server[instance_name][,port] (Required to save data to the database)
# -Database [database_name] (Required to save data to the database)
# -Username [user_name] (Required to save data to the database)
# -Password [password] (Required to save data to the database, must be passed as secure string)
# -FilePath [csv_file_name] (Required to save data in a .csv format. Ignored if database parameters are specified)
#
param (
[string] $SubId,
[string] $Server,
[string] $Username,
[SecureString] $Password,
[string] $Database,
[string] $FilePath,
[switch] $UseInRunbook,
[switch] $IncludeEC
)
#The following block is required for runbooks only
if ($UseInRunbook){
# Ensures you do not inherit an AzContext in your runbook
Disable-AzContextAutosave Scope Process
$connection = Get-AutomationConnection -Name AzureRunAsConnection
# Wrap authentication in retry logic for transient network failures
$logonAttempt = 0
while(!($connectionResult) -and ($logonAttempt -le 10))
{
$LogonAttempt++
# Logging in to Azure...
$connectionResult = Connect-AzAccount `
-ServicePrincipal `
-Tenant $connection.TenantID `
-ApplicationId $connection.ApplicationID `
-CertificateThumbprint $connection.CertificateThumbprint
Start-Sleep -Seconds 5
}
}
# Subscriptions to scan
if ($args[0] -like "*.csv") {
$subscriptions = Import-Csv $args[0]
}elseif($args[0] -ne $null){
$subscriptions = [PSCustomObject]@{SubscriptionId = $args[0]} | Get-AzSubscription
if ($SubId -like "*.csv") {
$subscriptions = Import-Csv $SubId
}elseif($SubId -ne $null){
$subscriptions = [PSCustomObject]@{SubscriptionId = $SubId} | Get-AzSubscription
}else{
$subscriptions = Get-AzSubscription
}
[Boolean] $useDatabase = $PSBoundParameters.ContainsKey("Server") -and $PSBoundParameters.ContainsKey("Username") -and $PSBoundParameters.ContainsKey("Password") -and $PSBoundParameters.ContainsKey("Database")
#Initialize tables and arrays
[System.Collections.ArrayList]$usage = @()
if ($includeEC -eq $null){
$usage += ,(@("Date", "Time", "Subscription Name", "Subscription ID", "AHB Std vCores", "AHB Ent vCores", "PAYG Std vCores", "PAYG Ent vCores", "HADR Std vCores", "HADR Ent vCores", "Developer vCores", "Express vCores"))
if ($useDatabase){
#Database setup
$cred = New-Object System.Management.Automation.PSCredential($Username,$Password)
[String] $tableName = "Usage-per-subscription"
[String] $testSQL = "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'dbo'
AND TABLE_NAME = '$tableName'"
[String] $createSQL = "CREATE TABLE [dbo].[$tableName](
[Date] [date] NOT NULL,
[Time] [time](7) NOT NULL,
[SubscriptionName] [nvarchar](50) NOT NULL,
[SubscriptionID] [nvarchar](50) NOT NULL,
[AHB_EC] [int] NULL,
[PAYG_EC] [int] NULL,
[AHB_STD_vCores] [int] NULL,
[AHB_ENT_vCores] [int] NULL,
[PAYG_STD_vCores] [int] NULL,
[PAYG_ENT_vCores] [int] NULL,
[HADR_STD_vCores] [int] NULL,
[HADR_ENT_vCores] [int] NULL,
[Developer_vCores] [int] NULL,
[Express_vCores] [int] NULL)"
[String] $insertSQL = "INSERT INTO [dbo].[$tableName](
[Date],
[Time],
[SubscriptionName],
[SubscriptionID],
[AHB_EC],
[PAYG_EC],
[AHB_STD_vCores],
[AHB_ENT_vCores],
[PAYG_STD_vCores],
[PAYG_ENT_vCores],
[HADR_STD_vCores],
[HADR_ENT_vCores],
[Developer_vCores],
[Express_vCores])
VALUES
('{0}','{1}','{2}','{3}','{4}','{5}','{6}','{7}','{8}','{9}','{10}','{11}','{12}','{13}')"
$propertiesToSplat = @{
Database = $Database
ServerInstance = $Server
User = $cred.Username
Password = $cred.GetNetworkCredential().Password
Query = $testSQL
}
# Create table if does not exist
if ((Invoke-SQLCmd @propertiesToSplat).Column1 -eq 0) {
$propertiesToSplat.Query = $createSQL
Invoke-SQLCmd @propertiesToSplat
}
}else{
$usage += ,(@("Date", "Time", "Subscription Name", "Subscription ID", "AHB ECs", "PAYG ECs", "AHB Std vCores", "AHB Ent vCores", "PAYG Std vCores", "PAYG Ent vCores", "HADR Std vCores", "HADR Ent vCores", "Developer vCores", "Express vCores"))
#File setup
if (!$PSBoundParameters.ContainsKey("FilePath")) {
$FilePath = '.\sql-license-usage.csv'
}
[System.Collections.ArrayList]$usageTable = @()
$usageTable += ,(@("Date", "Time", "Subscription Name", "Subscription ID", "AHB ECs", "PAYG ECs", "AHB Std vCores", "AHB Ent vCores", "PAYG Std vCores", "PAYG Ent vCores", "HADR Std vCores", "HADR Ent vCores", "Developer vCores", "Express vCores"))
}
$subtotal = [pscustomobject]@{ahb_std=0; ahb_ent=0; payg_std=0; payg_ent=0; hadr_std=0; hadr_ent=0; developer=0; express=0}
@@ -233,31 +343,65 @@ foreach ($sub in $subscriptions){
}
}
# Get All VMs hosts in the subscription
$host_groups = Get-AzHostGroup
# Get the dedicated host size, match it with the corresponding VCPU count and add to VCore count
foreach ($host_group in $host_groups){
$vm_hosts = $host_group | Select-Object -Property @{Name = 'HostGroupName'; Expression = {$_.Name}},@{Name = 'ResourceGroupName'; Expression = {$_.ResourceGroupName}} | Get-AzHost
foreach ($vm_host in $vm_hosts){
$token = (Get-AzAccessToken).Token
$params = @{
Uri = "https://management.azure.com/subscriptions/" + $sub +
"/resourceGroups/" + $vm_host.ResourceGroupName.ToLower() +
"/providers/Microsoft.Compute/hostGroups/" + $host_group.Name +
"/hosts/" + $vm_host.Name +
"/providers/Microsoft.SoftwarePlan/hybridUseBenefits/SQL_" + $host_group.Name + "_" + $vm_host.Name + "?api-version=2019-06-01-preview"
Headers = @{ 'Authorization' = "Bearer $token" }
Method = 'GET'
ContentType = 'application/json'
}
$softwarePlan = Invoke-RestMethod @params
if ($softwarePlan.Sku.Name -like "SQL*"){
$size_info = $VM_SKUs | where {$_.ResourceType.Contains('hostGroups/hosts') -and $_.Name.Contains($vm_host.Sku.Name)} | Select-Object -First 1
$cores= $size_info.Capabilities | Where-Object {$_.name -eq "Cores"}
$subtotal.ahb_ent += $cores.Value
}
}
}
# Increment the totals and add subtotals to the usage array
$subtotal.psobject.properties.name | %{$total.$_ += $subtotal.$_}
if ($includeEC -eq $null){
$usage += ,(@((Get-Date -Format d), (Get-Date -Format t), $sub.Name, $sub.Id, $subtotal.ahb_std, $subtotal.ahb_ent, $subtotal.payg_std, $subtotal.payg_ent, $subtotal.hadr_std, $subtotal.hadr_ent, $subtotal.developer, $subtotal.express))
$Date = Get-Date -Format "yyy-MM-dd"
$Time = Get-Date -Format "HH:mm:ss"
if ($IncludeEC -eq $null){
$ahb_ec = 0
$payg_ec = 0
}else{
$ahb_ec = ($subtotal.ahb_std + $subtotal.ahb_ent*4)
$payg_ec = ($subtotal.payg_std + $subtotal.payg_ent*4)
}
if ($useDatabase){
$propertiesToSplat.Query = $insertSQL -f $Date, $Time, $sub.Name, $sub.Id, $ahb_ec, $payg_ec, $subtotal.ahb_std, $subtotal.ahb_ent, $subtotal.payg_std, $subtotal.payg_ent, $subtotal.hadr_std, $subtotal.hadr_ent, $subtotal.developer, $subtotal.express
Invoke-SQLCmd @propertiesToSplat
}else{
$usage += ,(@((Get-Date -Format d), (Get-Date -Format t), $sub.Name, $sub.Id, ($subtotal.ahb_std + $subtotal.ahb_ent*4), ($subtotal.payg_std + $subtotal.payg_ent*4), $subtotal.ahb_std, $subtotal.ahb_ent, $subtotal.payg_std, $subtotal.payg_ent, $subtotal.hadr_std, $subtotal.hadr_ent, $subtotal.developer, $subtotal.express))
$usageTable += ,(@( $Date, $Time, $sub.Name, $sub.Id, $ahb_ec, $payg_ec, $subtotal.ahb_std, $subtotal.ahb_ent, $subtotal.payg_std, $subtotal.payg_ent, $subtotal.hadr_std, $subtotal.hadr_ent, $subtotal.developer, $subtotal.express))
}
}
# Add the total numbers to the usage array
if ($includeEC -eq $null){
$usage += ,(@((Get-Date -Format d), (Get-Date -Format t), "Total", $null, $total.ahb_std, $total.ahb_ent, $total.payg_std, $total.payg_ent, $total.hadr_std, $total.hadr_ent, $total.developer, $total.express))
if ($useDatabase){
Write-Host ([Environment]::NewLine + "-- Added the usage data to $tableName table --")
}else{
$usage += ,(@((Get-Date -Format d), (Get-Date -Format t), "Total", $null, ($total.ahb_std + $total.ahb_ent*4), ($total.payg_std + $total.payg_ent*4), $total.ahb_std, $total.ahb_ent, $total.payg_std, $total.payg_ent, $total.hadr_std, $total.hadr_ent, $total.developer, $total.express))
}
# Write usage data to the .csv file
# Append to '.\sql-license-usage.csv'
$table = ConvertFrom-Csv ($usage | %{ $_ -join ','} )
$table | Format-table
#$fileName = '.\sql-license-usage_' + (Get-Date -f yyyy-MM-dd_HH-mm-ss) + '.csv'
$fileName = '.\sql-license-usage.csv'
$table | Export-Csv $fileName -NoTypeInformation -Append
Write-Host ([Environment]::NewLine + "-- The usage data is saved to " + $fileName + "--")
(ConvertFrom-Csv ($usageTable | %{$_ -join ','})) | Export-Csv $FilePath -Append -NoType
Write-Host ([Environment]::NewLine + "-- Added the usage data to $FilePath --")
}