License Sku Discovery

M365SLIC.License.POSH.Discovery.DS (DataSourceModuleType)

Will discover LicenseSku objects.

Knowledge Base article:

Summary

Will discover the M365 License objects.

Additional

None.

External

https://MonitoringGuys.com

Overridable Parameters

Name

Description

Default Value

IntervalSeconds

The frequncy of the workflow.

900

PoshLibraryPath

For customer support engineer use only.

ProbeActionTimeoutSeconds

If the workflow module does not exit gracefully by this time limit, the module will be forced to terminate.

ProbeActionTimeoutSecondsDEFAULT

SyncTime

This can be set to force a workflow to synchronize its Interval to a specific start time. If no SyncTime value is provided to the workflow, the workflow will be initiated at the agent's earliest opportunity after receiving a configuration change or restarting. Typically no SyncTime is preferred. Format is 00:00. Example for 5:36pm: 17:36. Example for 2:15am: 02:15.

WriteToEventLog

This will enable/disable script logging to the Operations Manager event log.

false

Element properties:

TypeDataSourceModuleType
IsolationAny
AccessibilityPublic
RunAsM365SL.RunAs.Profile
OutputTypeSystem.Discovery.Data

Member Modules:

ID Module Type TypeId RunAs 
DS DataSource Microsoft.Windows.TimedPowerShell.DiscoveryProvider Default

Overrideable Parameters:

IDParameterTypeSelector
IntervalSecondsint$Config/IntervalSeconds$
PoshLibraryPathstring$Config/PoshLibraryPath$
ProbeActionTimeoutSecondsint$Config/ProbeActionTimeoutSeconds$
SkuNamesFilePathstring$Config/SkuNamesFilePath$
SyncTimestring$Config/SyncTime$
WriteToEventLogbool$Config/WriteToEventLog$

Source Code:

<DataSourceModuleType ID="M365SLIC.License.POSH.Discovery.DS" Accessibility="Public" Batching="false" RunAs="M365SL!M365SL.RunAs.Profile">
<Configuration>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="ApiTokenScopeURL" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="ApiTokenURL" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="ApiURL" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="IntervalSeconds" minOccurs="1" type="xsd:integer"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="MgmtGroupRegKey" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="PoshLibraryPath" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="PrincipalName" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="ProbeActionTimeoutSeconds" minOccurs="1" type="xsd:integer"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="SkuNamesFilePath" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="SyncTime" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="TenantName" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="TLSVersion" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="WorkflowName" minOccurs="1" type="xsd:string"/>
<xsd:element xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="WriteToEventLog" minOccurs="1" type="xsd:boolean"/>
</Configuration>
<OverrideableParameters>
<OverrideableParameter ID="IntervalSeconds" Selector="$Config/IntervalSeconds$" ParameterType="int"/>
<OverrideableParameter ID="PoshLibraryPath" Selector="$Config/PoshLibraryPath$" ParameterType="string"/>
<OverrideableParameter ID="ProbeActionTimeoutSeconds" Selector="$Config/ProbeActionTimeoutSeconds$" ParameterType="int"/>
<OverrideableParameter ID="SkuNamesFilePath" Selector="$Config/SkuNamesFilePath$" ParameterType="string"/>
<OverrideableParameter ID="SyncTime" Selector="$Config/SyncTime$" ParameterType="string"/>
<OverrideableParameter ID="WriteToEventLog" Selector="$Config/WriteToEventLog$" ParameterType="bool"/>
</OverrideableParameters>
<ModuleImplementation Isolation="Any">
<Composite>
<MemberModules>
<DataSource ID="DS" TypeID="Windows!Microsoft.Windows.TimedPowerShell.DiscoveryProvider">
<IntervalSeconds>$Config/IntervalSeconds$</IntervalSeconds>
<SyncTime>$Config/SyncTime$</SyncTime>
<ScriptName>M365SLIC.LicenseDiscovery.ps1</ScriptName>
<ScriptBody><Script>&lt;#
Filename: M365SLIC.LicenseDiscovery.ps1
Author: Brian Zoucha, Stephen Mccommas, Taylour Blackwell, Tyson Paul
Description: Instance discovery for Microsoft 365 Service License Management and Monitoring.
Version History:
2020.11.17.2316 - Added API Url params
2020.10.13.1809 - Updated discovery for M365SLIC.License.Role to reflect service model change.
2020.09.30.1630 - Added Role class.
2020.09.29.1442 - Modified License service model.
2020.09.28.1442 - Added TLS param
2020.09.24.1554 - Updated Load-Library
2020.09.18.0000 - v1
#&gt;

Param (
[string]$ApiTokenScopeURL,
[string]$ApiTokenURL,
[string]$ApiURL,

[string]$ManagedEntityID = "No ManagedEntityID defined",
[string]$MgmtGroupRegKey,
[string]$PoshLibraryPath, #comma-separated list of .ps1 files to load
[string]$PrincipalName = "No PrincipalName defined!",
[string]$SkuNamesFilePath,
[string]$SourceID = "No SourceID defined",
[string]$TenantName,
[string]$TLSVersion,
[string]$WorkflowName = '&lt;No Name Provided&gt;',
[string]$WriteToEventLog="false"
)

[bool]$WriteToEventLog = [System.Convert]::ToBoolean($WriteToEventLog)
[string]$ScriptName = 'M365SLIC.LicenseDiscovery.ps1'
$NameSpace = 'License'
$Testing = $false
######################### FUNCTIONS ############################
################################################################
Function Load-Library {
Param (
[string]$PoshLibraryPath
)
$ErrorActionPreference = 'STOP'
If ($PoshLibraryPath ){
ForEach ($Path in $PoshLibraryPath.Split(',') ){
Try {
If (($Path.Length) -AND ($Path -notmatch '^-1$')) {
. $Path
}
} Catch {
Write-Host "Line [$($MyInvocation.ScriptLineNumber )]: Error loading PoshLibrary at path:[$($Path)]. This is likely to cause many other dependent functions to fail. `n`nError data: $($_)`n`n"
}
}
}
$ErrorActionPreference = 'CONTINUE'
}
################################################################

############## TESTING ##############
&lt;# #Run this as needed when testing

# powershell.exe -file C:\Test\M365SMP_Dev\SharePoint\M365SSPO.SharePointDiscovery.ps1 &gt; C:\Test\M365SMP_Dev\DiscoveryData.xml
#---------------------------------------
Function Testing {
$Testing = $true
$testParamsFile = (Join-path $TestFolder ("PARAMS_$($ScriptName)"))
#Write-Host "$(Test-Path $testParamsFile):$($testParamsFile)" -F Yellow -B Green
. $testParamsFile
. Load-Library -PoshLibraryPath $PoshLibraryPath

# Encode user data/passwords in current test user context
$M365_ClientSecret = Encode-UserData $M365_ClientSecret_PLAINTEXT
$M365_AccountPassword = Encode-UserData $M365_AccountPassword_PLAINTEXT
$error.Clear()
}
#---------------------------------------

$TestFolder = "C:\Test\M365SMP_Dev\$($NameSpace)\TestSetup"
If (Test-Path -Path $TestFolder) {
. Testing
}
#&gt;
############## TESTING ##############

. Load-Library -PoshLibraryPath $PoshLibraryPath
LogIt -EventID 9990 -Type $info -Msg "Begin script..." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
LogIt -EventID 9992 -Type $info -msg "Setting TLS for session, Version: [$($TLSVersion)]." -Proceed $WriteToEventLog -Line $(_LINE_); $Error.Clear();
#Set Default TLS
. Set-TLS -TLSVersion $TLSVersion

$TenantKey = Join-Path $MgmtGroupRegKey $TenantName
$ThisKey = Join-Path $TenantKey $NameSpace
$TenantKeyPath = $TenantKey.Replace("HKLM\",'HKLM:\')
$RegKeyPath = $ThisKey.Replace("HKLM\",'HKLM:\')
$TenantProperties = Get-ItemProperty -Path ($TenantKey.Replace("HKLM\",'HKLM:\') )
LogIt -EventID 9992 -Type $info -Msg "TenantKeyPath: [$($TenantKeyPath)], KeyPath: [$($RegKeyPath)]" -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()

$api = New-Object -comObject 'MOM.ScriptAPI'
$discoveryData = $api.CreateDiscoveryData(0, $SourceID, $ManagedEntityID)

Try {
# Verify if the Reg key exists
If (Test-Path $RegKeyPath ) {
$RegProperties = (Get-ItemProperty -Path $RegKeyPath -ErrorAction Stop)
# Verify if properties exists (Data/Values)
If (-NOT [bool]$RegProperties) {
$discoveryData
LogIt -EventID 9995 -Type $warn -Msg "RegKey does exist:[$($ThisKey)] but no properties found. This is unusual; if the key exists, properties/data should exist also. Exiting Discovery workflow: [$WorkflowName]. " -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
}
}
Else {throw}
} Catch {
# Return empty discovery data. This will undiscover entities if they were previously discovered.
$discoveryData
LogIt -EventID 9992 -Type $info -Msg "RegKey does not exist:[$($ThisKey)] or possible error accessing registry data value(s). Exiting Discovery workflow: [$WorkflowName]. " -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Exit
}

If ($Testing) {
$RegProperties.M365_AccountName = $M365_AccountName
$RegProperties.M365_ClientID = $M365_ClientId
$RegProperties.M365_AccountPassword = Encode-UserData -Data $M365_AccountPassword_PLAINTEXT
$RegProperties.M365_ClientSecret = Encode-UserData -Data $M365_ClientSecret_PLAINTEXT
}


# Decode Account Password
LogIt -EventID 9992 -Type $info -Msg "Decode password..." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Try {
$M365_AccountPassword_DECRYPTED = Decode-UserData -Data $RegProperties.M365_AccountPassword
} Catch {
LogIt -EventID 9995 -Type $warn -Msg "Unable to decode password. See error data. " -Proceed $true -LINE $(_LINE_); $Error.Clear()

}

# Decode Client Secret
LogIt -EventID 9992 -Type $info -Msg "Decode ClientSecret..." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Try {
$M365_ClientSecret_DECRYPTED = Decode-UserData -Data $RegProperties.M365_ClientSecret
} Catch {
LogIt -EventID 9995 -Type $warn -Msg "Unable to decode ClientSecret. See error data. " -Proceed $true -LINE $(_LINE_); $Error.Clear()

}

# Get Access Token
LogIt -EventID 9992 -Type $info -Msg "Get Access Token..." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Try {
$TokenResponse = Get-AccessToken -Delegated -User $RegProperties.M365_AccountName -Pass $M365_AccountPassword_DECRYPTED -ClientID $RegProperties.M365_ClientID -ClientSecret $M365_ClientSecret_DECRYPTED -TenantName $TenantName -ApiTokenUrl $ApiTokenURL -ApiTokenScopeURL $ApiTokenScopeURL
} Catch {
LogIt -EventID 9995 -Type $warn -Msg "Unable to retrieve Access Token. See error data. " -Proceed $true -LINE $(_LINE_); $Error.Clear()
}


LogIt -EventID 9992 -Type $info -Msg "Retrieve License object(s)" -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Try {
$LicenseUrl = "$($ApiURL)/v1.0/subscribedSkus"
$Licenses = Invoke-RestMethod -Headers @{Authorization = "Bearer $($TokenResponse.access_token)"} -Uri $LicenseUrl -Method Get -ContentType 'application/json' -ErrorAction Stop
LogIt -EventID 9992 -Type $info -Msg "Retrieved License object(s): [$($Licenses.Value.skuPartNumber | Out-String)]" -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
} Catch {
LogIt -EventID 9995 -Type $warn -Msg "Unable to get License object(s). See error data." -Proceed $true -LINE $(_LINE_); $Error.Clear()
}

If (-NOT ($Licenses.Value.Count)) {
LogIt -EventID 9992 -Type $info -Msg "No License objects exist. Will not UNdiscover any registered objects. Exiting Discovery workflow: [$WorkflowName]. " -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Exit
}

&lt;# Ability to UNdiscovery License objects is not used at this time
If ((-NOT ($Licenses.Value.Count)) -AND (-NOT $Error.Count)) {
# Return empty discovery data. This will undiscover entities if they were previously discovered.
$discoveryData
LogIt -EventID 9992 -Type $info -Msg "No License objects exist. Will undiscover any previously discovered objects. Exiting Discovery workflow: [$WorkflowName]. " -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
Exit
}
#&gt;


#region IfLicensesExist
Else {
#Discover M365SLIC.License.Role
Try {
LogIt -EventID 9992 -Type $info -Msg "Proceed to discovery type: [M365SLIC.License.Role] for tenant: [$($TenantName)]..." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
$instance = $discoveryData.CreateClassInstance('$MPElement[Name="M365SLIC.License.Role"]$')

# Add instance properties
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/IntervalSeconds$", [int]($RegProperties.IntervalSeconds ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/M365_AccountName$", [string]($RegProperties.M365_AccountName ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/M365_AccountPassword$", [string]($RegProperties.M365_AccountPassword ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/M365_ClientID$", [string]($RegProperties.M365_ClientID ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/M365_ClientSecret$", [string]($RegProperties.M365_ClientSecret ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/TenantName$", [string]($TenantName) )

# Host
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.WatcherNode']/TenantName$", [string]($TenantName) )

# Standard Props
$instance.AddProperty("$MPElement[Name='Windows!Microsoft.Windows.Computer']/PrincipalName$", $PrincipalName)
$instance.AddProperty("$MPElement[Name='System!System.Entity']/DisplayName$", $TenantName)

$discoveryData.AddInstance($instance)
LogIt -EventID 9992 -Type $info -Msg "Discovery object type: [M365SLIC.License.Role] for tenant: [$($TenantName)] added to instance data item!" -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
} Catch {
$msg = "There was a critical failure in discovery type: [M365SLIC.License.Role] for tenant at path: [$($TenantKey)]"
LogIt -EventID 9995 -Type $Critical -Msg $msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
}


#region Try to load friendly DisplayNames
$hashNames = @{}
Try {
$ObjNames = Import-Csv -Path $SkuNamesFilePath | Sort-Object -ErrorAction Stop
# Build hash table of ugly-to-friendly name associations.
ForEach ($item in $ObjNames) {
$hashNames[($item.skuPartNumber)] = $item.DisplayName
}
} Catch {
LogIt -EventID 9992 -Type $info -Msg "Problem trying to import Sku DisplayNames from path: [$($SkuNamesFilePath)]." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
# No friendly DisplayNames could be found. No choice but to build hash table of ugly-to-ugly name associations.
ForEach ($item in ($Licenses.Value.skuPartNumber)) {
$hashNames[($item)] = $item
}
}
#endregion

#region ForEachSku
ForEach ($objSku in $Licenses.Value) {
Try {
LogIt -EventID 9992 -Type $info -Msg "Properties exist at this Key: [$($ThisKey)]. Proceed to discover this class type: [M365SLIC.License.Sku] for TenantName: [$($TenantName)]." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
If (-NOT ($hashNames[($objSku.skuPartNumber)])) {
LogIt -EventID 9992 -Type $info -Msg "No friendly name exists for Sku: [$($objSku.skuPartNumber)]." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
$hashNames[($objSku.skuPartNumber)] = $objSku.skuPartNumber
}
# Declare instance
$instance = $discoveryData.CreateClassInstance('$MPElement[Name="M365SLIC.License.Sku"]$')

# Parent
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/IntervalSeconds$", [int]($RegProperties.IntervalSeconds ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/M365_AccountName$", [string]($RegProperties.M365_AccountName ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/M365_AccountPassword$", [string]($RegProperties.M365_AccountPassword ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/M365_ClientID$", [string]($RegProperties.M365_ClientID ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/M365_ClientSecret$", [string]($RegProperties.M365_ClientSecret ) )
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.M365ServiceComponent']/TenantName$", [string]($TenantName) )

# Add instance properties (M365 license Sku object)
# Key
$instance.AddProperty("$MPElement[Name='M365SLIC.License.Sku']/SkuPartNumber$", ($objSku.SkuPartNumber) )
$instance.AddProperty("$MPElement[Name='M365SLIC.License.Sku']/SkuID$", ($objSku.SkuID) )

# Host
$instance.AddProperty("$MPElement[Name='M365SL!M365SL.WatcherNode']/TenantName$", [string]($TenantName) )

# Standard Props
$instance.AddProperty("$MPElement[Name='Windows!Microsoft.Windows.Computer']/PrincipalName$", $PrincipalName)
$instance.AddProperty("$MPElement[Name='System!System.Entity']/DisplayName$", $hashNames[($objSku.SkuPartNumber)] )

$discoveryData.AddInstance($instance)
LogIt -EventID 9992 -Type $info -Msg "Added discovery instance data for M365SLIC.License.Sku SkuPartNumber: [$($objSku.SkuPartNumber)], tenant: [$($TenantName)]." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
}
Catch {
$msg = "There was a critical failure in discovery for M365SLIC.License.Sku SkuPartNumber: [$($objSku.SkuPartNumber)], tenant: [$($TenantName)]."
LogIt -EventID 9995 -Type $Critical -Msg $msg -Proceed $true -LINE $(_LINE_); $Error.Clear()
}
}
#endregion ForEachSku
} #endregion IfLicensesExist

LogIt -EventID 9992 -Type $info -Msg "Return License discovery data for tenant: [$($TenantName)]." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()
$discoveryData

If ($Testing) {
$api.Return($discoveryData)
}

LogIt -EventID 9991 -Type $info -Msg "End script." -Proceed $WriteToEventLog -LINE $(_LINE_); $Error.Clear()</Script></ScriptBody>
<Parameters>
<Parameter>
<Name>ApiTokenScopeURL</Name>
<Value>$Config/ApiTokenScopeURL$</Value>
</Parameter>
<Parameter>
<Name>ApiTokenURL</Name>
<Value>$Config/ApiTokenURL$</Value>
</Parameter>
<Parameter>
<Name>ApiURL</Name>
<Value>$Config/ApiURL$</Value>
</Parameter>
<Parameter>
<Name>ManagedEntityId</Name>
<Value>$Target/Id$</Value>
</Parameter>
<Parameter>
<Name>MgmtGroupRegKey</Name>
<Value>$Config/MgmtGroupRegKey$</Value>
</Parameter>
<Parameter>
<Name>PoshLibraryPath</Name>
<Value>$FileResource[Name='Res.M365SLIC.M365Library.ps1.Resource']/Path$,$Config/PoshLibraryPath$</Value>
</Parameter>
<Parameter>
<Name>PrincipalName</Name>
<Value>$Config/PrincipalName$</Value>
</Parameter>
<Parameter>
<Name>SkuNamesFilePath</Name>
<Value>$Config/SkuNamesFilePath$</Value>
</Parameter>
<Parameter>
<Name>SourceId</Name>
<Value>$MPElement$</Value>
</Parameter>
<Parameter>
<Name>TenantName</Name>
<Value>$Config/TenantName$</Value>
</Parameter>
<Parameter>
<Name>TLSVersion</Name>
<Value>$Config/TLSVersion$</Value>
</Parameter>
<Parameter>
<Name>WorkflowName</Name>
<Value>$Config/WorkflowName$</Value>
</Parameter>
<Parameter>
<Name>WriteToEventLog</Name>
<Value>$Config/WriteToEventLog$</Value>
</Parameter>
</Parameters>
<TimeoutSeconds>$Config/ProbeActionTimeoutSeconds$</TimeoutSeconds>
</DataSource>
</MemberModules>
<Composition>
<Node ID="DS"/>
</Composition>
</Composite>
</ModuleImplementation>
<OutputType>System!System.Discovery.Data</OutputType>
</DataSourceModuleType>