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 license
don'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.