#
.SYNOPSIS
Generates an Azure license report, including license usage data, and sends it via email.
.DESCRIPTION
This PowerShell script connects to the Microsoft Graph API, retrieves available licenses and licensed users, and calculates the assigned and available licenses.
It then generates an HTML report and sends it to the specified recipients. A summary report is also generated and saved to a file.
.PARAMETER None
This script does not require any parameters.
.EXAMPLE
.\Azure-License-Report.ps1
.NOTES
This script is intended for use in a test or production environment. Make sure to test the script in a non-production environment before running it in production.
Author: D.de Kooker - info@dcomputers.nl
Version: 1.0
DISCLAIMER: Use scripts at your own risk, if there is anything I can help you with I will try but I do not take responsibility for the way that anyone else uses my scripts.
Sharing is caring. Share your knowledge with the world so that everybody can learn from it.
.LINK
The latest version can Always be found on my GIT page on the link below:
https://git.dcomputers.nl/Dcomputers/PowershellScripts
#>
#region Global script settings and variables
#General
$Version = "v1.0"
$logfilelocation = "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\Logs"
$logfilename = "$(Get-Date -Format yyyyMMddHHmmss)-Azure-Licensing-Report.log"
$summaryfilename = "$(Get-Date -Format yyyyMMddHHmmss)-Azure-Licensing-Summary.txt"
#Azure Enterprise app configuration
$STR_TenantID = ""
$STR_AppID = ""
$STR_ClientSecret = ""
#Email report settings
$STR_SMTPServer = ""
$STR_SMTPServerPort = ""
$STR_SMTPUsername = ""
$STR_SMTPPassword = ""
$STR_EmailSubject= "Azure License report - $(Get-Date -Format "dd-MM-yyyy")"
$STR_SMTPFromaddress = "Servicedesk ICT "
$STR_Receivers = "servicedesk@contoso.com,systemengineer1@contoso.com" #List of commaseperated emailaddresses
#endregion
#region functions
function SendMailv2 ($To,$Subject,$Body){
$SMTPClient = New-Object Net.Mail.SmtpClient($STR_SMTPServer, $STR_SMTPServerPort)
# $SMTPClient.EnableSsl = $true
$SMTPClient.Credentials = New-Object System.Net.NetworkCredential($STR_SMTPUsername, $STR_SMTPPassword);
$SMTPMessage = New-Object System.Net.Mail.MailMessage($STR_SMTPFromaddress,$To,$Subject,$Body)
$SMTPMessage.IsBodyHTML = $true
$SMTPClient.Send($SMTPMessage)
}
function Initiate-Log {
# Get current user and session information
$username = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$computerName = $env:COMPUTERNAME
$sessionID = $pid
$date = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
# Write log header
$logHeader = "[$date] Log initiated by $username on $computerName (Session ID: $sessionID)"
Add-Content -Path $logfilelocation\$logfilename -Value "**********************"
Add-Content -Path $logfilelocation\$logfilename -Value "LogFile initiation"
Add-Content -Path $logfilelocation\$logfilename -Value "Start time: $date"
Add-Content -Path $logfilelocation\$logfilename -Value "Username: $username"
Add-Content -Path $logfilelocation\$logfilename -Value "Machine: $computerName"
Add-Content -Path $logfilelocation\$logfilename -Value "Process ID: $sessionID"
Add-Content -Path $logfilelocation\$logfilename -Value "Script Version: $Version"
Add-Content -Path $logfilelocation\$logfilename -Value "Script Source: https://git.dcomputers.nl/Dcomputers/PowershellScripts"
Add-Content -Path $logfilelocation\$logfilename -Value "**********************"
}
function Write-Log {
[CmdletBinding()]
Param (
[Parameter(Mandatory=$true)]
[string]$Message,
[Parameter(Mandatory=$false)]
[ValidateSet("INFO", "WARNING", "ERROR")]
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logmessage = "[$timestamp] [$Level] $Message"
Add-Content -Path $logfilelocation\$logfilename -Value $logmessage
}
function Write-Summary {
[CmdletBinding()]
Param (
[Parameter(Mandatory=$true)]
[string]$Message
)
Add-Content -Path $logfilelocation\$summaryfilename -Value $Message
}
#endregion
#region prerequisites check
#Create log directory if not present and initiate logfile
if (!(test-path $logfilelocation)) {mkdir $logfilelocation}
Initiate-Log
#Check if the required Powershell Modules are available
$modules = @("Microsoft.Graph")
foreach ($module in $modules) {
if (!(Get-Module -Name $module -ListAvailable)) {
Write-Host "The $module module is not installed. Please install it and try again."
Write-Log -Message "The $module module is not installed. Please install it and try again." -Level ERROR
exit 1
}
}
#Setup MSGraph connection
$ClientSecretPass = ConvertTo-SecureString -String $STR_ClientSecret -AsPlainText -Force
$ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $STR_AppID, $ClientSecretPass
Connect-MgGraph -TenantId $STR_TenantID -ClientSecretCredential $ClientSecretCredential
Write-Log -Message "Connected to MsGraph API" -Level INFO
#Download the latest product name translation file
if (Test-Path "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\ProductNames.csv") {
Remove-Item "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\ProductNames.csv" -Force
}
$uri = "https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv"
Invoke-WebRequest $uri -OutFile "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\ProductNames.csv"
$ReadableNames = Import-Csv "$($MyInvocation.MyCommand.Path | Split-Path -Parent)\ProductNames.csv"
#endregion
#region collect license usage and generate a readable report
Write-Log -Message "Collecting license usage data" -Level INFO
# Get all available licenses and licensed users
$availableLicenses = Get-MgSubscribedSku | Where-Object {$_.CapabilityStatus -eq "Enabled"}
Write-Log -Message "Retrieved available licenses" -Level INFO
$licensedUsers = Get-MgUser -Filter 'assignedLicenses/$count ne 0' -ConsistencyLevel eventual -CountVariable licensedUserCount -All
Write-Log -Message "Retrieved licensed users" -Level INFO
# Initialize an empty array to store the license usage data
$licenseUsageData = @()
$userLicenses = @()
# Loop through each available license
foreach ($licensedUser in $licensedUsers) {
$userLicenses += Get-MgUserLicenseDetail -UserId $licensedUser.Id
}
foreach ($availableLicense in $availableLicenses) {
# Get the license details
$licenseDetails = Get-MgSubscribedSku -SubscribedSkuId $availableLicense.Id
Write-Log -Message "Retrieved license details for $($availableLicense.SkuPartNumber)" -Level INFO
# Get the total licenses available
$totalLicenses = $licenseDetails.PrePaidUnits.Enabled
# Get the assigned licenses
$assignedLicenses = 0
foreach ($userLicense in $userLicenses) {
if ($userLicense.SkuId -eq $availableLicense.SkuId) {
$assignedLicenses++
}
}
# Calculate the available licenses
$availableLicensesCount = $totalLicenses - $assignedLicenses
# Calculate license status
if ($availableLicensesCount -le 1) {
$licenseStatus = "ERROR"
} elseif ($availableLicensesCount -le ($totalLicenses * 0.05)) {
$licenseStatus = "WARNING"
} else {
$licenseStatus = "OK"
}
# Create a custom object to store the license usage data
$licenseUsageObject = [PSCustomObject]@{
LicenseDisplayName = $($ReadableNames | Where-Object {$_.String_Id -eq $($availableLicense.SkuPartNumber)} | Select-Object -ExpandProperty Product_Display_Name)[0]
LicenseSKU = $availableLicense.SkuPartNumber
AssignedLicenses = $assignedLicenses
AvailableLicenses = $availableLicensesCount
TotalLicenses = $totalLicenses
LicenseStatus = $licenseStatus
}
# Add the custom object to the array
$licenseUsageData += $licenseUsageObject
Write-Log -Message "Added license usage data for $($availableLicense.SkuPartNumber)" -Level INFO
}
# Create an HTML report
$htmlReport = @"
Azure Product license report - $(Get-Date -Format "dd-MM-yyyy - HH:mm")
Script version: $Version
| License Name |
License SKU |
Assigned Licenses |
Available Licenses |
Total Licenses |
Status |
"@
foreach ($licenseUsageObject in $($licenseUsageData | Sort-Object -Property LicenseDisplayName)) {
$htmlReport += @"
| $($licenseUsageObject.LicenseDisplayName) |
$($licenseUsageObject.LicenseSKU) |
$($licenseUsageObject.AssignedLicenses) |
$($licenseUsageObject.AvailableLicenses) |
$($licenseUsageObject.TotalLicenses) |
$(switch ($licenseUsageObject.LicenseStatus) {
'ERROR' {"$($licenseUsageObject.LicenseStatus) | "}
'WARNING' {"$($licenseUsageObject.LicenseStatus) | "}
default {"$($licenseUsageObject.LicenseStatus) | "}
} )
"@
}
$htmlReport += @"
This is an automated report.
"@
#endregion
#region send reports and generate summary report
# Send the report via email
$licenseStatus = "OK"
switch ($licenseUsageData | Select-Object -ExpandProperty LicenseStatus) {
{ $_ -contains "ERROR" } { $licenseStatus = "ERROR"; break }
{ $_ -contains "WARNING" } { $licenseStatus = "WARNING"; break }
}
if ($licenseStatus -ne "OK") {
$subject = "$($LicenseStatus): $STR_EmailSubject"
}
else {
$subject = "$STR_EmailSubject"
}
$body = $htmlReport
SendMailv2 -To $STR_Receivers -Subject $subject -Body $body
# write summary
Write-Summary "Azure Product license report Summary:"
Write-Summary "---------------------------"
Write-Summary "Report date: $(Get-Date -Format "dd-MM-yyy HH:mm:ss")"
foreach ($license in $($licenseUsageData | Sort-Object -Property LicenseDisplayName)){
Write-Summary "******************"
Write-Summary "License Name: $($license.LicenseDisplayName)"
Write-Summary "License SKU: $($license.LicenseSKU)"
Write-Summary "Assigned Licenses: $($license.AssignedLicenses)"
Write-Summary "Available Licenses: $($license.AvailableLicenses)"
Write-Summary "Total Licenses: $($license.TotalLicenses)"
Write-Summary "Status: $($license.LicenseStatus)"
}
Write-Summary "---------------------------"