Get-CSOnlineUser

Das Commandlet "Get-CSOnlineUser" ist meiner Ansicht nach ein sehr wichtiges Werkzeug, um Berichte über Benutzer und deren Einstellungen zu erzeugen. Allerdings ist der Funktionsumfang nicht immer vorhersehbar. Daher schreibe ich diese Seite.

Verschwundene Properties

Kennen Sie noch die Anfangszeiten von Teams und Telefonie? Damals hat ADSync im Skype for Business Hybrid Mode die Rufnummer aus dem lokalen AD-Feld "msRTCSip-Line" ausgelesen und in das Feld "OnPremLineUri" geschrieben, damit die Skype for Business Online Anwender telefonieren konnten. Get-CSOnlineUser war damals auch noch das "Frontend" für Skype for Business Online. Das ist alles mittlerweile Geschichte und mit dem Wegfall von Skype for Business Online ist der Befehl "Get-CSOnlineUser" nun in die Teams PowerShell gewandert.

Auf der Seite MovetoTeams habe ich die Migration zu Teams beschrieben und besonderen Wert auf Voice/Telefonie gelegt. Eine Abfrage mit konnte dabei liefert:

PS C:\>Get-CsOnlineUser -Identity sip:frank.cariusxx@netatwork.de | fl name,OnPrem*

OnPremHideFromAddressLists           : False
OnPremHostingProvider                : sipfed.online.lync.com
OnPremOptionFlags                    : 385
OnPremEnterpriseVoiceEnabled         : True
OnPremSIPEnabled                     : True
OnPremSipAddress                     : sip:frank.cariusxx@netatwork.de
OnPremLineURI                        : tel:+495251304613
OnPremLineURIManuallySet             : true

Wer genau hinschaut, findet hier noch das Feld "OnPremLineURIManuallySet". Dieses Property ist mittlerweile verschwunden. Mittlerweile setzen Sie eine Cloud-Rufnummer nicht mehr per Set-CSUser sondern weisen Sie per "Set-CsPhoneNumberAssignment" zu. Anscheinend ist das Feld "OnPremLineUri" gar nicht mehr relevant und damit auch egal, ob Sie es per ADSync oder Manuell setzen.

Beim Wechsel der Teams PowerShell 2.x auf 3.x sind einige Felder verändert worden.

Laut dem Artikel sind auch viele andere Properties nicht mehr verfügbar, ohne dass darauf explizit hingewiesen wurde.

Bitte prüfen Sie ihre Skripte auf Abhängigkeiten

Auslöser

Der nächte "Aha"-Effekt erlebte ich bei einem größeren Kunden, bei die lokale Skype for Business-Umgebung zugunsten von Teams abgelöst werden musste und ich vorab einige "Health-Checks" gemacht habe. Dabei ist mir ein unangenehmes Verhalten von Get-CSOnlineUser aus der Teams PowerShell 4.4.2 (Stand Jun 2022)

Wenn Sie von Skype for Business On-Premises mit Telefonie zu Teams mit DirectRouting migrieren, dann müssen Sie irgendwann entscheiden, wie sie zukünftig die Rufnummern verwalten wollen. Sie können dies weiter On-Premises im Feld "msRTCSIP-Line" hinterlegen und ADSync damit das feld "OnPremLineUri" in der Cloud verwalten lassen. Sie können aber auch mit dem Wegfall von Skype for Business direkt die Rufnummern in der Cloud verwalten. Dazu sollten Sie aber nicht mehr "Set-CSOnlineUser -OverwriteOnPremLineURI:$true" nutzen, sondern "Set-CsPhoneNumberAssignment -Identity "<User name>" -PhoneNumber <phone number> -PhoneNumberType DirectRouting" aber wir kommen ja von einer Migration.

Daher habe ich mir erst einmal eine Liste der CSOnlineUser gezogen, um den aktuellen Stand zu analysieren. Genau genommen habe ich in der Teams Online PowerShell folgendes ausgeführt:

Get-CsOnlineUser `
   | select userprincipalname,InterpretedUserType,EnterpriseVoiceEnabled,`
            HostedVoicemail,HostedVoicemailpolicy,`
            LineUri, OnPremLineUri,OnPremSipEnabled,OnPremSipAddress, `
   | export-csv csonlineuser.csv -notypeinformation

Bei der Filterung auf diese Werte habe ich dann aber gesehen, dass die OnPremLineUri immer leer war und die OnPremSipAddress den Wert "Not Yet Supported" hat.

Nur gut, dass mir das aufgefallen ist, denn basierend auf der Auswertung hätte ich vielleicht eine falsche Entscheidung gefällt.

Detailansicht

Ich habe mi dann Get-CSOnlineuser mit einem Benutzer in meinem Lab genauer angeschaut. Ich habe einmal den Benutzre explizit adressiert und einmal nachträglich gefiltert.

Get-CSOnlineUser user1@uclabor.de
Get-CSOnlineUser | where {$_.sipaddress -eq "sip:user1@uclabor.de"}

Die beiden Ausgaben haben ich mal nebeneinander gestellt. Links ist die Filterung mit "Where" und rechts der direkte Aufruf einer einzelnen Person

Da sind schon einiger Felder bemerkenswert:

  • Interpreted Usertype
    Hier ist der Inhalt "unterschiedlich": DirSyncEnabledOnlineTeamsOnlyUser vs. HybridOnlineTeamsOnlyUser
  • LastSyncTimeStamp
    Das Infofeld um die letzte Änderung durch der internen Sync abzufragen gibt es auch nur beim direkten Abruf
  • OnPremHostingProvider, OnPremLineUri und OnPremSipAddress
    Bei der Listenabfrage erhalte ich hier immer nur ein "Not Yet Supported."
  • OptionFlags, WhenCreated LastProvisionTimeStamps LastPublishTimeStamps
    Diese Felder gibt es bei der Listenabfrage gar nicht.

Wenn ich also eine Auswertung über Felder machen möchte, die bei der allgemeinen Abfragen nicht vorhanden oder nicht detailliert genug gefüllt sind, dann müsste ich zuerst eine Liste der Benutzer generieren und über eine For-Schleife dann für jeden Benutzer individuell noch einmal "Get-CSOnlineUser" aufrufen. Ob das im Sinne des Erfinders ist?

Ich bin noch nicht sicher, ob das ein Bug ist, oder by Design. Ich finde es auf jeden Fall "bedenklich", dass ein Commandlet abhängig vom Aufruf andere Werte und Felder zurück liefert. Ich wollte über den Inhalt von "OnPremLineUri" z.B. ermitteln, welche Objekte noch im lokalen AD das Feld "msRTCSIP-Line" gefüllt und per ADSync repliziert bekommen. Das funktioniert aktuell nicht. Aber auch die Bewertung des Feldes "InterpretedUserType" irritiert, dass hier unterschiedliche Typen geliefert werden.

Loop als Lösung?

Mein Ziel war es, aus der Cloud eine Liste aller Benutzer samt ihrer Rufnummern in "LineURI" und "OnPremLineUri" und dem EnterpriseVoice-Status zu erhalten. Nun wissen wir, dass zumindest der Inhalt von "OnPremLineUri" nicht bei der normalen "Get-CSOnlineUser"-Rückgabe enthalten ist. Ich habe mir daher eine kleine Schleife gebaut:

Write-host "Collect CSOnlineUser"
$csonlineuserlist = get-csonlineuser 
Write-host " Total Users $($csonlineuserlist.count)"

Write-Host "Loading Details per User"
$count = 0 
foreach ($csonlineuser in $csonlineuserlist) {
    $count++
    Write-Host "$($count)/$($csonlineuserlist.count) $($csonlineuser.sipaddress)"
    get-csonlineuser $csonlineuser.sipaddress | Select-Object sipaddress,enterprisevoiceenabled,lineuri,onpremlineuri
}

Zuerst hole ich mir die Userliste um dann für jeden Benutzer die Daten abzufragen. Das Skript funktioniert im Grund aber sehr sehr langsam. Sie können damit wenige hundert Benutzer exportieren, da die Teams PowerShell anscheinend bei größeren Benutzeranzahlen in ein Throttling läuft. Zumindest habe ich sehr schnell immer 20er Gruppen bekommen, die aber letztlich mit einem Durchsatz von 2-5 Sek/Benutzer geliefert wurden.

Insofern würde ich eher einen Export als Liste vorziehen und die OnPremLineUri aus dem lokalen AD passend dazu ermitteln.

Get-CsOnlineUser `
| select SipAddress, UserPrincipalName, EnterpriseVoiceEnabled, LineUri,OnPremLineUri `
| export-csv csonlineuser.csv

Nun muss ich nur noch die On-Premises User mit "Get-CSUser" einsammeln und z.B. anhand der SIP-Adresse als primären Schlüssel zueinander matchen. Hier mal eine einfache nicht optimierte Verssion.

# Export-CSOnlineUser
#
# Loads all CSOnlineUser and collects details about SIP-Address  LineUri ,OnPremLineUri, EnterpriseVoiceEnabled

Write-Host "Collect CSOnlineUser"
$csonlineuserlist = Get-CsOnlineUser | select-object SipAddress, UserPrincipalName, EnterpriseVoiceEnabled, LineUri,OnPremLineUri
$csonlineuserlist | export-csv -path "csonlineuserlist.csv" -notypeinformation
Write-Host " Total OnlineUsers $($csonlineuserlist.count)"

Write-Host "Loading Details per OnPremUser"
$csuserlist  = get-csuser | select-object SipAddress,LineUri,HostingProvider
$csuserlist | export-csv -path "csuserlist.csv" -notypeinformation
Write-Host " Total OnPremUsers $($csuserlist.count)"

Write-host "Start Merging"
$count = 0 
foreach ($csuser in $csuserlist) {
    $count++
    Write-Host "$($count)/$($csuserlist.count) $($csuser.sipaddress)" -NoNewline
    $line = $csonlineuserlist | Where-Object {$_.sipaddress -eq $csuser.sipaddress}
    if ($Null -eq $line) {
        Write-Host "User not found in online" -ForegroundColor yellow
        $csonlineuserlist += [PSCustomObject]@{
            SipAddress = $csuser.sipaddress
            UserPrincipalName = "NOTFOUND"
            EnterpriseVoiceEnabled ="NOTFOUND"
            LineUri                = "NOTFOUND"
            OnPremLineUri          = $csuser.lineuri
        }
    }
    elseif ($line.count -gt 1) {
        Write-Host "User not Unique" -BackgroundColor yellow
        $line.OnPremLineUri = "DUPLICATE"
    }
    else {
        Write-host "Match - Update" -ForegroundColor green
        $line.OnPremLineUri = $csuser.lineuri
    }
}

Filter

Wenn sie eine sehr große Benutzeranzahl haben, dann werden Sie bemerken, dass Get-CSOnlineUser sehr langsam wird. Es kann natürlich sinnvoll sein, einmal alle User in eine Variable zu übertragen und dann mehrere Abfragen quasi "offline" auszuführen. Wenn Sie aber ehr nur wenige Antworten erwarten, dann ist es wichtig den richtigen Filter zu nutzen.

Ein gutes Beispiel ist z.B. der Check, ob alle Teams Anwender, die "EnterpriseVoiceEnabled" sind, auch eine LineURI haben. Ein Ansatz wäre:

Get-CSOnlineuser `
| where  {$_.enterprisevoiceenabled -eq $true -and $_.lineuri -notlike "tel:*"} `
| ft sipaddress,enterprisevoiceenabled,lineuri

Allerdings werden hier alle Objekte erst mit allen Properties übertragen und dann lokal gefiltert. Für eine Abfrage sicherlich suboptimal. Daher können Sie die Filterung auch dem Server übertragen, z.B.: mit:

Get-CSOnlineuser -Filter {(enterprisevoiceenabled -eq $true) -and (lineuri -eq "")} `
| ft sipaddress,enterprisevoiceenabled,lineuri

Interessanter ist aber der Zeitaufwand für die Aufgabe.

  • Laden mit "-Filter"
    3:12 Minuten für -Filter gegen 20.000 Objekte und 150 Ergebnisse
  • Laden und Where-Object
    4:30 Minuten für -Filter gegen 20.000 Objekte und 150 Ergebnisse

Wenn Sie nur wenige hundert Benutzer haben, dann dürfte nur nur Unterschied sondern die Gesamtlaufzeit geringer sein. Wer aber mehrere Abfragen gegen die gleiche Datenmenge startet oder ausführlichere Filter-Kriterien nutzen will, sollte Cachen.

Das die Teams PowerShell die Filter für die interne Abfrage umformatieren muss, sind nicht alle Vergleichsoperatoren verfügbar oder eingeschränkt nutzbar:


Quelle: https://docs.microsoft.com/en-us/powershell/module/skype/get-csonlineuser?view=skype-ps

Leider kann ich bei der Nutzung von "-Filter" nicht alle Anfragen verwenden, wie bei "Where-Object, auch wenn die Onlinehilfe das anders beschreibt.

Get-CSOnlineuser -Filter  {enterprisevoiceenabled -eq $true -and lineuri -notlike "tel:*"} 
get-csonlineuser : The method or operation is not implemented.

PS C:\disablecsuser> Get-CSOnlineuser -Filter  {enterprisevoiceenabled -eq $true -and lineuri.startswith("tel:")}
Get-CSOnlineuser : Query not supported for operator:  query: enterprisevoiceenabled -eq $true -and lineuri.startswith("tel:")

Wenn ich die bei Teams interesante "LineUri" mit "-notlike" oder mit "Startswith prüfen möchte, bekomme ich Fehler. Solche Auswertungen kann ich also nur nachträglich machen. Die OnlineHilfe zu Get-CSOnlineUser beschreibt auch, welche Felder Sie mit "-Filter" nutzen können. Allerdings ändert sich dies je nach Versionsstand der Teams PowerShell gerne mal. Auch ist nicht immer sichergestellt, dass die Abfragezeit vergleichbar ist. Es scheint Felder zu geben, die Microsoft mit einem Index versehen hat während andere Felder wohl eine ineffektive Suche nutzen.

-Filter {Userprincipalname -like "*@msxfaq.de"}  10 Sek
-Filter {Company -eq 'MSXFAQ'}                   90 Sek

Manchmal ändern sich auch Ausgabeformate:

In the Teams PowerShell Module version 3.0.0 or later, the output format of Policies has now changed from String to JSON type UserPolicyDefinition.
Quelle: https://docs.microsoft.com/en-us/powershell/module/skype/get-csonlineuser?view=skype-ps

Die folgenden Filter habe ich in verschiedenen Projekten und Skripten schon genutzt

# Liste alle Benutzer mit Enterprise Voice
Get-CsOnlineUser -Filter {EnterpriseVoiceEnabled -eq "true"}

#Suche alle User mit einer Rufnummer
Get-CsOnlineUser -Filter { LineUri -like "tel:*" }

# Suche Benutzer mit einer bestimmten UPN-Domain
Get-CsOnlineUser -Filter {Userprincipalname -like "*@msxfaq.de"

# Suche nach dem Firmen-Feld
Get-CsOnlineuser -Filter {Company -eq 'MSXFAQ'}

# Suche nach einer Rufnummer. Eine exakte Suche ist sehr schnell
Get-CsOnlineUser -Filter {LineURI -eq "tel:+1234"}
Get-CsOnlineUser -Filter {LineURI -eq "tel:+1234,ext:"}

# Sie können auch das TEL-Prefix weglassen und er findet den User
Get-CsOnlineUser -Filter {LineURI -eq "1234"}

# Eine Suche nach einem Teilstring dauert deutlich länger
Get-CsOnlineUser -Filter {LineURI -like "tel:1234*"}

Einschätzung

Manchmal sind schnelle Entwicklungszyklen mit vielen neuen Funktionen auch kontraproduktiv. Beim Umgang mit der Teams PowerShell werde ich zukünftig definitiv mehr "Debug-Ausgaben" und Validierungen der Inhalte vornehmen müssen, um keine Fehlentscheidungen zu treffen.

Weitere Links