Recently, I needed to generate a consolidated report of Remote Desktop Services Client Access License (RDS-CAL) usage across multiple licensing servers in our domain. What seemed like a straightforward task turned into a deep dive into the quirks of Windows Server licensing management. Here's my journey and the solution that finally worked.
The final results is a HTML website that shows all the servers defined with the RDS-CAL license usages:
The Initial Approach: PowerShell RDS Module
My first instinct was to use the Remote Desktop Services PowerShell module, which seemed purpose-built for this task:
Import-Module RemoteDesktopServices
Get-RDLicenseKeyPack -ConnectionBroker $serverName
This immediately failed with "The term 'Get-RDLicenseKeyPack' is not recognized." Apparently, this cmdlet isn't available or installed on our servers so that is not a reliable command to use.
Second Attempt: WMI Queries
Next, I tried the traditional WMI approach:
Get-WmiObject -Class Win32_TSLicenseKeyPack
-ComputerName $serverName -Namespace "root\cimv2\TerminalServices"
This failed with "Invalid class Win32_TSLicenseKeyPack". It turns out the namespace might be incorrect or the WMI class isn't available remotely it needs to be run locally on the server.
Detective Work : Checking the Registry
At this point, I started looking at alternative methods, including checking the registry for license information:
Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\
TermServLicensing\Parameters'
While this showed the service was configured, it didn't provide actual license counts or usage data.
Using pre-existing tools for discovery
Windows Server includes several built-in tools for RDS licensing:
licmgr.exe- The GUI Remote Desktop Licensing Managerlsdiag.msc- RD Licensing Diagnoserlicensingdiag.exe- Command-line diagnostic tool
Running licensingdiag.exe showed promise but required very specific parameters:
licensingdiag.exe -report "C:\Temp\diag.txt" -log "C:\Temp\diag.cab"
However, when run locally on the servers, the report was empty or didn't contain the actual license information I needed.
Locating the correct WMI Namespace
After much trial and error, I discovered that the correct WMI query was:
Get-WmiObject -Namespace "root\cimv2" -Class Win32_TSLicenseKeyPack
This worked locally on each server but not remotely. The key insight was that the WMI namespace for RDS licensing is actually under root\cimv2, not root\cimv2\TerminalServices as many online resources suggest - I do not like the rabbit holes when you fall down them!
The Final Solution: Remote WMI Queries with PowerShell
Here's the complete solution that generates both CSV and HTML reports:
# Remote RDS License Query Script
$licenseServers = @(
"server1.domain.com",
"server2.domain.com",
"server3.domain.com"
)
$combinedReport = @()
$timestamp = Get-Date -Format "yyyyMMdd_HHmm"
foreach ($server in $licenseServers) {
Write-Host "Querying $server..." -ForegroundColor Yellow
try {
# Use Invoke-Command to run the WMI query remotely
$licenses = Invoke-Command -ComputerName $server -ScriptBlock {
Get-WmiObject -Namespace "root\cimv2" -Class Win32_TSLicenseKeyPack
} -ErrorAction Stop
foreach ($license in $licenses) {
$reportEntry = [PSCustomObject]@{
ServerName = $server
ProductVersion = $license.ProductVersion
ProductVersionID = $license.ProductVersionID
KeyPackType = switch ($license.KeyPackType) {
0 { "Unknown" }
1 { "Retail Purchase" }
2 { "Volume Purchase" }
3 { "Concurrent License" }
4 { "Temporary" }
5 { "Open License" }
6 { "Built-in" }
default { "Other" }
}
TotalLicenses = $license.TotalLicenses
IssuedLicenses = $license.IssuedLicenses
AvailableLicenses = $license.AvailableLicenses
UsagePercentage = if ($license.TotalLicenses -gt 0) {
[math]::Round(($license.IssuedLicenses / $license.TotalLicenses)
* 100, 2)
} else { 0 }
ExpirationDate = if ($license.ExpirationDate) {
[Management.ManagementDateTimeConverter]
::ToDateTime($license.ExpirationDate)
} else {
"Never"
}
}
$combinedReport += $reportEntry
}
}
catch {
Write-Host "Failed to query $server : $_" -ForegroundColor Red
}
}
# Export to CSV
$combinedReport | Export-Csv -Path
"RDS_License_Report_${timestamp}.csv" -NoTypeInformation
# Generate HTML Report with visual formatting
$serverGroups = $combinedReport | Group-Object ServerName
Lessons Learned
- WMI namespaces matter: The correct namespace for RDS licensing is
root\cimv2, not the seemingly obviousroot\cimv2\TerminalServices. - Not all PowerShell modules are created equal: The Remote Desktop Services module isn't always available or complete, even on RDS servers.
- Built-in tools can be misleading: Commands like
netsh rds show licensedon't exist, despite some documentation suggesting otherwise. - Local vs. remote execution: Some WMI queries work locally but fail remotely, making PowerShell remoting essential.
- Error handling is crucial: With multiple servers and potential connectivity issues, robust error handling prevents the script from failing completely.
Detective Work
To make the data more presentable, I added HTML report generation. Here's how I enhanced the script to create a color-coded report that highlights usage levels:
# Create HTML report
$htmlReport = @"
<!DOCTYPE html>
<html>
<head>
<title>RDS License Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4CAF50; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.high-usage { background-color: #ffcccc; }
.medium-usage { background-color: #ffffcc; }
.low-usage { background-color: #ccffcc; }
.server-section { margin-top: 30px; }
</style>
</head>
<body>
<h1>RDS License Report</h1>
<p>Generated: $(Get-Date)</p>
"@
foreach ($group in $serverGroups) {
$htmlReport += @"
<div class="server-section">
<h2>$($group.Name)</h2>
<table>
<tr>
<th>Product</th>
<th>Type</th>
<th>Total</th>
<th>Issued</th>
<th>Available</th>
<th>Usage %</th>
<th>Expiration</th>
</tr>
"@
foreach ($license in $group.Group) {
$rowClass = ""
if ($license.UsagePercentage -ge 90) { $rowClass = "high-usage" }
elseif ($license.UsagePercentage -ge 75) { $rowClass = "medium-usage" }
else { $rowClass = "low-usage" }
$htmlReport += @"
<tr class="$rowClass">
<td>$($license.ProductVersionID)</td>
<td>$($license.KeyPackType)</td>
<td>$($license.TotalLicenses)</td>
<td>$($license.IssuedLicenses)</td>
<td>$($license.AvailableLicenses)</td>
<td>$($license.UsagePercentage)%</td>
<td>$($license.ExpirationDate)</td>
</tr>
"@
}
$htmlReport += "</table></div>"
}
$htmlReport += @"
</body>
</html>
"@
$htmlReport | Out-File -FilePath "RDS_License_Report.html" -Encoding UTF8
The HTML report features:
- Color-coded rows based on usage percentage (red for >90%, yellow for >75%, green for <75%)
- Clear table formatting with alternating row colors
- Server-by-server breakdown of license usage
- Expiration date tracking
This visual representation makes it much easier to spot servers approaching their license limits at a glance.