We recently started using Office 365. One of the advantages of this subscription based licensing is that you can change the number of licenses monthly, based on what you actually need. In the beginning all works fine and you regularly check if you don’t have (too much) unused licenses, but after a while other priorities arise and before you know it you have a bunch of licenses that you pay each month but are not being used.
So what better solution to come up with than try to monitor this with, of course, PRTG!
So I started digging around on how to get those license information with powershell. I quickly found out that you can assign a read-only admin role to users, so I started by creating a PRTG-user in our AD and sync it to AzureAD. Before assigning this role to my PRTG-user I first want to add some error handling to my script and wanted to know what happens when you try to get the license information with a default user without any roles. Seems that any valid user can read the license information and there’s no need to assign the read-only admin role to my PRTG user!
64-bit powershell environment needed
Once my script was finished and worked fine when I ran it on my PRTG Core server, it was time to add a sensor in PRTG for my new script. Well, that was not what I expected. The script just failed, saying connect-MSolService was not a recognized cmdlet. It quickly came to my mind that the MSOnline powershell module was 64-bit only and a PRTG Probe runs as a 32-bit process. I remembered there was an add-on in the PRTG Tools Family, that allowed you to start a 64-bit powershell environment and run the script there, called PSx64.exe. I’ve used this before, but remembered I sometimes had problems with this way of working when I needed to pass parameters to my script, so I searched the Internet for alternative solutions. I came across a solution that you can just build into your script that checks if the script is running in a 32-bit environment and if it is, restarts the script in a 64-bit powershell environment. So putting the next code on top of your script allows you to run it on any environment without the need to use external workarounds.
### Check PowerShell environment (32/64 bit) and restart in 64-bit mode if needed
if ($env:PROCESSOR_ARCHITEW6432 -eq "AMD64") {
if ($myInvocation.Line) {&"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile $myInvocation.Line}
else{&"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile -file "$($myInvocation.InvocationName)" $args}
exit $lastexitcode
}
Now we’ve tackled the 64-bit problem it is time to have a look at the actual script to get the licenses. This is done using the PowerShell module MSOnline (possible it will also work with the AzureAD module, but here I used the MSOnline module).
Using the Get-MsolAccountSku cmd-let we get a list of all services and their numbers:
AccountSkuId = product/service identifier
ActiveUnits = effective number of licenses you own for this product/service
ConsumedUnits = number of assigned licenses for this product/service
What I wanted to see in PRTG is the number of licenses we have for a product or service and the number assigned licenses. Of course it would be nice to also have a count showing the “unused” licenses and a usage counter showing the percentage of licenses in use. With these counters we should have enough information to create different types of warnings in PRTG like “if unused licenses > 5”, “if license usage < 90%” or to have a history of active and assigned licenses over a period of time.
I’ve added 3 parameters.
- User: the user to logon to MS Online services
- Pass: the password of course
- Sku: this allows you to create a single sensor with a single product/service or filter multiple products in a single sensor (e.g. all Teams licenses)
This resulted in the following script:
param (
[string]$User ="",
[string]$Pass = "",
[string]$Sku = ""
)
### Check PowerShell environment (32/64 bit) and restart in 64-bit mode if needed
if ($env:PROCESSOR_ARCHITEW6432 -eq "AMD64") {
if ($myInvocation.Line) {&"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile $myInvocation.Line}
else{&"$env:WINDIR\sysnative\windowspowershell\v1.0\powershell.exe" -NonInteractive -NoProfile -file "$($myInvocation.InvocationName)" $args}
exit $lastexitcode
}
Import-Module MSOnline
if ($User -eq "" -or $Pass -eq "") { $con = Connect-MsolService } #allowing interactive logon for troubleshooting purposes
else {
$secpasswd = ConvertTo-SecureString $Pass -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential ($User, $secpasswd)
$Pass = "" # clear the password, we have it stored securely
$con = Connect-MsolService -Credential $creds -ErrorAction SilentlyContinue
}
if ($?) { # check if the connect-msolservice worked or if there was an error
write-host "<prtg>"
$licenses = Get-MsolAccountSku | where-object {$_.AccountSkuId -like "*$Sku*"} # filter Sku
if ($licenses.count -ge 0)
{
foreach ($license in $licenses)
{
$service = $license.AccountSkuId.Substring($license.AccountName.Length+1) # get the name of the service without the prefix
# Usage percentage
write-host " <result>"
write-host " <channel>Usage ($service)</channel>"
if ($license.ActiveUnits -ne 0) {write-host " <value>$($license.ConsumedUnits/$license.ActiveUnits*100)</value>"}
else {write-host " <value>0</value>"}
write-host " <unit>percent</unit>"
write-host " <float>1</float>"
write-host " <DecimalMode>Auto</DecimalMode>"
write-host " </result>"
# Active licenses
write-host " <result>"
write-host " <channel>Active ($service)</channel>"
write-host " <value>$($license.ActiveUnits)</value>"
write-host " <unit>Count</unit>"
write-host " </result>"
# Used licenses
write-host " <result>"
write-host " <channel>Consumed ($service)</channel>"
write-host " <value>$($license.ConsumedUnits)</value>"
write-host " <unit>Count</unit>"
write-host " </result>"
# unused licenses
write-host " <result>"
write-host " <channel>Unused ($service)</channel>"
write-host " <value>$($license.ActiveUnits-$license.ConsumedUnits)</value>"
write-host " <unit>Count</unit>"
write-host " </result>"
}
}
write-host "</prtg>"
}
else
{ # connection failed
write-host "<prtg>"
write-host " <error>1</error>"
write-host " <text>could not connect to MSOnline service ($error)</text>"
write-host "</prtg>"
}
PRTG
In PRTG I created a new device, pointing to portal.office.com and set a Windows username & password to be used to connect to MSOnline. I also set the scanning interval on 1 hour (there’s no need to check licenses every minute). Then I created 2 sensors, one for Office 365 ProPlus licenses and one for Teams licenses, each one of the type EXE/Advanced, pointing to my powershell script GetMSOnlineLicenses.ps1.
As you can see, the only difference is the -sku parameter. If you just want a single sensor, showing all the products as different channels you can just leave out the -sku parameter.
This gives us the following information in PRTG:
As you can see, the Teams sensor has a lot more channels becuase there were multiple Teams services available. For clarity I advise you to limit each sensor to one product/service or one type of service (e.g. Teams). If you just put all products in a single sensor reports will be hard to read.
Now I can start adding notification triggers and reports about my Microsoft 365 licenses and be sure I don’t have (too much) unused licenses laying around and doing nothing.
Hi guys,
stumbled upon this one by accident.
Meanwhile they offer a native solution in PRTG, the Microsoft 365 sensors.
See here: https://kb.paessler.com/en/topic/88256-implemented-are-there-default-sensors-for-monitoring-office-365
Best,
Sascha
I’ve seen it but haven’t had the time to try the new sensor out yet.
Great to see it’s included now!
Emilie,
The SKU’s are set by Microsoft, so you can’t change them.
If you limit your sensor to only 1 SKU, you could adjust the script and use a fixed string as your channel name, or add some logic to the script to replace the SKU with your own custom name you want to see in PRTG.
Hello and thanks for this script.
I have a question.
Is it possible to rename the sku?
Ex:
O365_BUSINESS;Microsoft 365 Apps for Business
SMB_BUSINESS;Microsoft 365 Apps for Business
O365_BUSINESS_ESSENTIALS;Office 365 Business Basic
SMB_BUSINESS_ESSENTIALS;Office 365 Business Basic
O365_BUSINESS_PREMIUM;Office 365 Business Standard
Thanks again.