• Retrieve Lync Connection Information

    Submitted by Tracy A. Cerise, University of Kentucky

     

    This script pulls Microsoft Lync 2010 connection information, such as the number of connections per Front End Server and number of unique users, and either displays it to the console or writes it to a file, depending on the parameters you supply with the call.

     

    To use this script, copy the code shown below, paste it into a text editor (like Notepad) and then save the file with a .PS1 extension (for example, C:\Scripts\Connections.ps1). After that, you can run the script from within the Lync Server Management Shell simply by typing the full path to the .PS1 file and then pressing ENTER:

     

    C:\Scripts\Connections.ps1

     

    For a full description as well as requirements for connecting to the Front End Server database, see the introductory comments in the script below. For additional help on using this script, type the following at the Lync Server Management Shell prompt and press ENTER:

     

    C:\Scripts\Connections.ps1 -help

     

     

    #################################################################################################

    # Connections.ps1

    #

    # Program to pull Lync connection information. 

    # This program will pull complete information across all

    # frontend servers in a pool.  It can also be used to find

    # specific connection information on an individual user by

    # supplying the user's sip address.  The parameter for

    # the pool to access for connection information can be

    # pre-populated so it doesn't have to be passed on the command line.

    #

    # ACKNOWLEDGEMENT: This program's database connection information

    #                  was originally taken from the "List Connections

    #                  to Registrar Pools" submitted by Scott Stubberfield

    #                  and Nick Smith from Microsoft to the Lync 2010

    #                  PowerShell blog

    #                  (http://blogs.technet.com/b/csps/) on June 10, 2010.

    #

    # NOTE: In order to gain remote access to each frontend server's

    #       RTCLOCAL database where connection information is found,

    #       you need to open the local firewall for port 1434.

    #       Also, need to go into the SQL Server Configuration Manager

    #       and for RTCLOCAL, enable named pipes and restart the SQL

    #       service for the named pipes to take effect.

    #

    #       Port 1434 is required in order to make the connection to

    #       the named instance RTCLOCAL on the remote machines.

    #

    #

    # Written by: Tracy A. Cerise (tracy@uky.edu)

    #       Date: March 2011

    #

    # Modification History

    # --------------------

    #

    # Could add # users with enterprise voice enabled and other

    # individual stats to this page as well

    #

    #################################################################################################

     

    ##########################################################

    # Commandline Parameters to use for running this program #

    ##########################################################

    param($Pool = "your.pool.here", $SIPAddr, $FilePath, [switch] $Help)

     

     

     

    #################################################################################################

    #################################       Functions       #########################################

    #################################################################################################

     

     

    function GetData {

     

       param ($sipAddr = $null, $server)

     

       ##############################################################################################

       # Went to using a named parameter for this function due to the

       # way Powershell does its thing with parameter passing, which

       # is NOT GOOD!  At any rate, need to call this function

       # as you would from a command line: GetData -sipAddr "value"

       # -server "value"

       #

       # Also, assuming a value of NULL for the SIP address of an

       # individual user, mostly to use this for finding overall

       # values, only occasionally to seek specific users.

       ##############################################################################################

     

     

       if ($sipAddr) {

          $whereClause = "where R.UserAtHost = '$sipAddr' "

       }

       else {

          $whereClause = $null

       }

     

       #Define SQL Connection String

       $connstring = "server=$server\rtclocal;database=rtcdyn;trusted_connection=true;"

     

       #Define SQL Command

       $command = New-Object System.Data.SqlClient.SqlCommand

     

       $command.CommandText = "Select (cast (RE.ClientApp as varchar (100))) as ClientVersion, R.UserAtHost as UserName, Reg.Fqdn `

                               From rtcdyn.dbo.RegistrarEndpoint RE `

                               Inner Join `

                               rtc.dbo.Resource R on R.ResourceId = RE.OwnerId `

                               Inner Join `

                               rtcdyn.dbo.Registrar Reg on Reg.RegistrarId = RE.PrimaryRegistrarClusterId `

                               $whereClause `

                               Order By ClientVersion, UserName "

     

       $connection = New-Object System.Data.SqlClient.SqlConnection

       $connection.ConnectionString = $connstring

       $connection.Open()

     

       $command.Connection = $connection

     

       $sqladapter = New-Object System.Data.SqlClient.SqlDataAdapter

       $sqladapter.SelectCommand = $command

     

       $results = New-Object System.Data.Dataset

     

       $recordcount=$sqladapter.Fill($results)

     

       $connection.Close()

     

       return $Results.Tables[0]

    }

    #################################################################################################

     

     

    function Help {

     

    "

    NAME

        Connections.ps1

     

     

    SYNOPSIS

        Returns current connection count for all frontend servers in a given pool

        including a breakdown of connection by client, frontend server and users.

     

        It can also be used to return connection information on an individual user.

       

     

    SYNTAX

        Connections.ps1 [-Pool <PoolFQDN>] [-SIPAddr] [-FilePath] [-help]

     

     

    DESCRIPTION

        This program will return a connection count for a given pool.  The program

        can be edited to set a default pool.  You will also be able to get

        information on an individual user by providing the users SamAccountName.

     

     

    EXAMPLES

     

        -------------------------- EXAMPLE 1 --------------------------

     

        C:\PS>connections.ps1

     

        Description

        -----------

        Returns information on all connections on all frontend servers in the

        default pool that has been hard-coded into the program.

     

     

        -------------------------- EXAMPLE 2 --------------------------

     

        C:\PS>connections.ps1 -Pool alternate.pool.fqdn

     

        Description

        -----------

        Returns information on all connections on all frontend servers in the

        pool given with the pool parameter.

     

     

        -------------------------- EXAMPLE 3 --------------------------

     

        C:\PS>connections.ps1 -SIPAddr userid@sip.domain

     

        Description

        -----------

        Returns all connection information for the given user including which

        frontend server connected to, how many connections and which clients

        connected with.

     

        -------------------------- EXAMPLE 4 --------------------------

     

        C:\PS>connections.ps1 -filepath c:\path\to\file\filename.csv

     

        Description

        -----------

        Returns information on all connections on all frontend servers in the

        default pool that has been hard-coded into the program and in addition

        writes out all the raw connection information into the filename specified.

     

     

    "

       exit

     

    }

    #################################################################################################

     

     

    #################################################################################################

    #################################     Main Program      #########################################

    #################################################################################################

     

    #############################################

    ### Main program

    #############################################

     

    # If the help switch is toggled

    if ($help) {

       Help

    }

     

    #

    # Here is where we pull all the frontend server(s) from our topology for the designated

    # pool and iterate through them to get current connections from all the servers.

    #

    # There are three possibilities here:

    #    1. Have collection of frontend servers

    #    2. Have a single frontend server or

    #    3. Have no servers

    #

    # Only need to check for the first two cases, the third is implied otherwise...

     

    $feServers = Get-CsComputer -Pool $Pool

     

     

    if ($feServers.count) {

       # Frontend pool collection, iterate through them

       for ($i=0; $i -lt $feServers.count; $i++) {

     

          if ($SIPAddr) {

             $data = GetData  -sipAddr $SIPAddr -server $feServers[$i].identity

          }

          else {

             $data = GetData -server $feServers[$i].identity

          }

     

          # Since an individual user's connections are all homed on one server, won't have

          # data coming back from all frontend servers in the case of searching for a

          # single user

          if ($data) {

             $overallrecords = $overallrecords + $data

          }

       }

    }

    elseif ($feServers) {

       # Have a standalone server or a FE pool of only one server

       if ($SIPAddr) {

          $data = GetData  -sipAddr $SIPAddr -server $feServers.identity

       }

       else {

          $data = GetData -server $feServers.identity

       }

     

       # Make sure we have data to work with...

       if ($data) {

          $overallrecords = $data

       }

    }

     

    # Check to see if we have any data to act on

    if (! $overallrecords) {

       write-host -ForegroundColor Yellow "`r`nNothing returned from query!`r`n"

     

       # Nothing else to do

       exit

    }

    else {

       $count=0

     

       $userHash = @{}

       $clientHash = @{}

       $serverHash = @{}

     

       $overallrecords | foreach-object {

          # Each record has three components: Connected Client Version, User's SIP

          # address and the frontend server's FQDN.  Here, we'll build a hash

          # for each of these components for each record.

     

          # Build hash of users

          if (! $userHash.ContainsKey($_.UserName)) {

             $userHash.add($_.UserName, 1)

          }

          else {

             $userHash.set_item($_.UserName, ($userHash.get_item($_.UserName) + 1))

          }

     

          # Build hash of servers

          if (! $serverHash.ContainsKey($_.fqdn)) {

             $serverHash.add($_.fqdn, 1)

          }

          else {

             $serverHash.set_item($_.fqdn, ($serverHash.get_item($_.fqdn) + 1))

          }

     

          # Build hash of clients

          # Lets get rid of the extraneous verbage from the client version names, if applicable

          if ($_.ClientVersion.contains('(')) {

             # Get rid of extraneous verbage

             $clientName = $_.ClientVersion.substring(0, $_.ClientVersion.IndexOf('('))

          }

          else {

             # Have a client name with no extraneous verbage

             $clientName = $_.ClientVersion

          }

     

          if (! $clientHash.ContainsKey($clientName)) {

             $clientHash.add($clientName, 1)

          }

          else {

             $clientHash.set_item($ClientName, ($clientHash.get_item($ClientName) + 1))

          }

     

          $count++

       }

    }

     

    ################################

    ####  Output Query Results  ####

    ################################

     

    # If output to file is chosen, then write out the results and a note to that effect

    # then exit

     

    if ($FilePath) {

       $overallrecords | Export-Csv $FilePath

     

       write-host -foregroundcolor green "`r`nQuery Results written to $FilePath`r`n"

     

       exit

    }

     

     

    write-host -foregroundcolor cyan "`r`nClient Version/Agent                   Connections"

    write-host -foregroundcolor cyan "--------------------------------------------------"

     

    foreach ($key in $clientHash.keys) {

       # Break down client version into its two component parts and print

       # them out along with their respective counts in a nice format

       $index = $key.indexof(" ")

     

       if ($index -eq "-1") {

          # No second part

          $first = $key

          $second = " "

       }

       else {

          # Client version/agent has to main parts

          $first = $key.substring(0, $index)

          $second = $key.substring($index + 1)

       }

     

       $value = $clientHash.$key

     

       "{0, -20} {1, -20} {2, 5}" -f $first, $second, $value

    }

     

    write-host -foregroundcolor cyan "--------------------------------------------------"

     

    write-host -foregroundcolor cyan "`r`n`r`nFrontend Server     Connections"

    write-host -foregroundcolor cyan "-------------------------------"

     

    foreach ($key in $serverHash.keys) {

       $value = $serverHash.$key

     

       "{0, -22} {1, 5}" -f $key, $value

    }

     

    write-host -foregroundcolor cyan "-------------------------------"

     

     

    "{0, -22} {1, 5}" -f "Total connections...", $count

    "`r`n"

     

    write-host -foregroundcolor cyan "Total Unique Users/Clients"

    write-host -foregroundcolor cyan "-------------------------------"

    "{0, -22} {1, 5}" -f "Users...............", $userHash.count

    "{0, -22} {1, 5}" -f "Client Versions.....", $clientHash.count

    write-host -foregroundcolor cyan "-------------------------------"

    "`r`n"

     

    Write-Host -ForegroundColor Green "Query complete`r`n"

     

  • PowerShell Overview: Format-Table vs. Select-Object

    Can you use the Format-Table cmdlet instead of the Select-Object cmdlet?

     

    Depending on what you're doing, yes, you can. In the lab we included an exercise where we had people retrieve user accounts and then display just the values of the DisplayName and Enabled attributes. To do that, we had them pipe the data to the Select-Object cmdlet, like so:

     

    Get-CsAdUser | Select-Object DisplayName, Enabled

     

    That results in output similar to this:

     

    DisplayName                           Enabled

    -----------                           -------

    Ken Myer                              True

    Pilar Ackerman                        False

    David Jaffe                           True

    Aneta Olecka                          True

     

    Based on that. the question we were asked is this: instead of piping data to Select-Object, could we pipe it to Format-Table instead? You know, using a command like this one:

     

    Get-CsAdUser | Format-Table DisplayName, Enabled

     

    Well, let's see:

     

    DisplayName                           Enabled

    -----------                           -------

    Ken Myer                              True

    Pilar Ackerman                        False

    David Jaffe                           True

    Aneta Olecka                          True

     

    Looks like we can, doesn't it?

     

    In fact, by using the Format-Table cmdlet we can not only get the same output as we can by using Select-Object, but we can actually get even better (i.e., more aesthetically-pleasing) output. By default, any time you display two columns of data in Windows PowerShell the software tends to show one column at the far left of the screen and the other column at the far right, leaving a big giant hole in the middle. If you use Select-Object to determine the properties to be displayed onscreen there isn't much you can do about that; Select-Object doesn't offer many formatting options. With Format-Table, however, you can add the AutoSize parameter, which tells PowerShell to make each column just wide enough to display the data in that column. Here's the command:

     

    Get-CsAdUser | Format-Table DisplayName, Enabled -AutoSize

     

    And here's the output:

     

    DisplayName       Enabled

    -----------       -------

    Ken Myer             True

    Pilar Ackerman      False

    David Jaffe          True

    Aneta Olecka         True

     

    A little nicer looking, and a lot easier to read.

     

    By the way, you can also select property values, and display data onscreen, by using the Format-List cmdlet:

     

    Get-CsAdUser | Format-List DisplayName, Enabled

     

    That gives you output similar to this:

     

    DisplayName : Ken Myer

    Enabled     : True

     

    DisplayName : Pilar Ackerman

    Enabled     : False

     

    DisplayName : David Jaffe

    Enabled     : True

     

    DisplayName : Aneta Olecka

    Enabled     : True

     

    The moral of the story is this: if all you need to do is display data onscreen then, yes, you can use either Format-Table or Format-List instead of Select-Object. (And, as we've seen, using either of these cmdlets might give you nicer-looking output than Select-Object does.)

     

    However (and this is an important however), you can't always use Format-Table (or Format-List) instead of Select-Object. For example, if you need to pipe data to another cmdlet Format-Table won't give you the expected results. Consider this command, which retrieves information about all your Lync Server-enabled user accounts, pulls out just the DisplayName, Enabled, and SipAddress properties, and then writes that information to a comma-separated values file:

     

    Get-CsUser | Select-Object DisplayName, Enabled, SipAddress | Export-Csv –Path C:\Logs\Users.csv

     

    That's going to give us a CSV file with lines that look like this:

     

    #TYPE Selected.Microsoft.Rtc.Management.ADConnect.Schema.OCSADUser

    "DisplayName","Enabled","SipAddress"

    "Ken Myer",$True,"sip:kenmyer@litwareinc.com"

    "Pilar Ackerman",$False,"sip:pilar@litwareinc.com"

     

    Now, let's try the same thing using Format-Table:

     

    Get-CsUser | Format-Table DisplayName, Enabled, SipAddress | Export-Csv –Path C:\Logs\Users.csv

     

    Here's what that CSV file looks like:

     

    #TYPE Microsoft.PowerShell.Commands.Internal.Format.FormatStartData

    "ClassId2e4f51ef21dd47e99d3c952918aff9cd","pageHeaderEntry","pageFooterEntry","autosizeInfo","shapeInfo","groupingEntry"

    "033ecb2bc07a4d43b5ef94ed5a35d280",,,,"Microsoft.PowerShell.Commands.Internal.Format.TableHeaderInfo",

    "9e210fe47d09416682b841769c78b8a3",,,,,

    "27c87ef9bbda4f709f6b4002fa4af63c",,,,,

    "27c87ef9bbda4f709f6b4002fa4af63c",,,,,

     

    Yeesh; what's that all about? Well, the problem is that the Select-Object cmdlet passes user objects through the pipeline; we can determine that by looking at the TYPE header:

     

    #TYPE Selected.Microsoft.Rtc.Management.ADConnect.Schema.OCSADUser

     

    However, Format-Table doesn't send user objects through the pipeline. Instead, it passes a table object through the pipeline:

     

    #TYPE Microsoft.PowerShell.Commands.Internal.Format.FormatStartData

     

    That's really not what we want.

     

    So, yes, for displaying data onscreen, use Select-Object, Format-Table, or Format-List. For anything else, stick with Select-Object.

     

    Note. Anything else? In real life as well as in Lync Server PowerShell?

     

    Well, maybe not anything. But most things.

     

  • List the Users and Client Endpoint Versions Connected to a Registrar Pool: Direct Connection

    Submitted by Scott Stubberfield and Nick Smith, Microsoft

     

    Nick Smith and Scott Stubberfield have updated a previous script of theirs which lists all the users connected to a Registrar pool along with the client version of the endpoint they used to log on to the system. According to Nick, updates made since the time the previous script was published include the following:

     

    • The script outputs the results to the screen instead of a .CSV file.  This allows administrators to pipe the results to cmdlets like Where-Object.
    • The script prompts for the pool FQDN and automatically queries all servers that are members of the pool.
    • There are two versions of the script. 
      • The Get-PoolUserRegistrations.ps1 script (this one) connects directly to the SQL instance on the remote server.  This requires that the Windows firewall be disabled or configured to allow SQL connections.
      • The Get-PoolUserRegistrations_Remoting.ps1 script uses Windows PowerShell remoting to run the SQL query locally on each machine.  With this script, the Windows firewall does not need to be disabled; however, you must run the Enable-PSRemoting cmdlet on each server in order to allow remote connections.

     

    The net result is that anyone running the script will get back data similar to this:

     

    ClientVersion        UserName                  Fqdn

    -------------        --------                  ----

    UCCAPI/4.0.7629.0... kenmyer@litwareinc.com    atl-cs-001.litwareinc.com

    UCCAPI/4.0.7629.0... packerman@litwareinc.com  atl-cs-001.litwareinc.com

    UCCAPI/4.0.7629.0... pdoyle@litwareinc.com     atl-cs-001.litwareinc.com

     

    To make use of this script, copy the code shown below, paste it into a text editor (such as Notepad) and then save the script as a .PS1 file (for example, C:\Scripts\Get-PoolUserRegistrations.ps1). From the Lync Server Management Shell you can then run the script by referencing the script file path followed by the fully qualified domain name of the Registrar pool you want to return information from. For example:

     

    C:\Scripts\Get-PoolUserRegistrations.ps1 –PoolFqdn atl-cs-001.litwareinc.com

     

    If you do not include a Registrar pool the script will prompt you to enter a pool FQDN.

     

    Here's the code:

     

    Param (
        $PoolFQDN = (Read-Host -Prompt "Please enter the pool fqdn")
    )

    #Get all the CS pool information
    $CSPool = Get-CSPool $PoolFQDN

    #Create empty variable that will contain the user registration records
    $overallrecords = $null

    #Loop through a front end computers in the pool
    Foreach ($Computer in $CSPool.Computers){

        #Get computer name from fqdn
        $ComputerName = $Computer.Split(".")[0]

        #Defined Connection String
        $connstring = "server=$ComputerName\rtclocal;database=rtcdyn;`
            trusted_connection=true;"

        #Define SQL Command    
        $command = New-Object System.Data.SqlClient.SqlCommand
        $command.CommandText = "Select (cast (RE.ClientApp as `
            varchar (100))) as ClientVersion, `
            R.UserAtHost as UserName, `
            RE.EndpointId as EndpointId, `
            EP.ExpiresAt, `
            '$computer' as RegistrarFQDN `
            From `
           rtcdyn.dbo.RegistrarEndpoint RE `
           Inner Join `
            rtc.dbo.Resource R on R.ResourceId = RE.OwnerId `
           Inner Join `
            rtcdyn.dbo.Endpoint EP on EP.EndpointId = RE.EndpointId `
            Order By ClientVersion, UserName "

        #Make the connection to Server    
        $connection = New-Object System.Data.SqlClient.SqlConnection
        $connection.ConnectionString = $connstring
        $connection.Open()
        $command.Connection = $connection
        
        #Get the results
        $sqladapter = New-Object System.Data.SqlClient.SqlDataAdapter
        $sqladapter.SelectCommand = $command
        $results = New-Object System.Data.Dataset    
        $recordcount=$sqladapter.Fill($results)
        
        #Close the connection
        $connection.Close()
        
        #Append the results to the reuslts from the previous servers
        $overallrecords = $overallrecords + $Results.Tables[0]
    }

    $overallrecords

     

  • Set the Forward To Number in Lync Server

    How can I set the actual number that calls are forwarded to?

     

    Well, unfortunately, you can't. We wish you could, because we get asked this question all the time. But you can't.

     

    Sorry.

     

    If you're wondering what we're talking about (something that happens quite a bit, now that we think about it) Lync Server's voice policies let you determine whether or not your users are allowed to forward their work calls to an alternate phone (such as their home phone or cell phone). By default, users are allowed to forward calls. If you'd prefer that they not be allowed to do this then all you have to do is disable call forward in the appropriate voice policy. For example, this command disables call forwarding at the global scope:

     

    Set-CsVoicePolicy –Identity global –AllowCallForwarding $False

     

    That part's easy. However, what people really want to do is be able to specify the phone number that calls can be forwarded to. For example, suppose you want Ken Myer to have his calls forwarded to (425) 555-1219. Can you run some PowerShell command that will cause any calls to Ken's work number to automatically be forwarded to (425) 555-1219? As far as we know, no, you can't.


    Hey, you win a few, you lose a few, right?

  • Enabling User Accounts in Lync Server vs. Exchange Server

    Does the Active Directory object for a user have to exist before you can enable that user for Lync Server?

     

    Yes; if you're going to enable a user for Lync Server then that user must already have an Active Directory user account. For example, suppose you try to enable Ken Myer for Lync Server, but Ken doesn't have an Active Directory user account. In that case, you're going to get an error message similar to this:

     

    Enable-CsUser : Management object not found for identity "Ken Myer".

     

    Note. Ken's Active Directory account doesn’t have to be enabled; you can run Enable-CsUser against a disabled account and everything will work just fine. The account just has to exist.

     

    The reason this question came up is because the New-Mailbox cmdlet in Microsoft Exchange will create an Active Directory account for you. For example, this command will not only create a new mailbox for Ken Myer, but it will create a new Active Directory account for him as well:

     

    New-Mailbox -UserPrincipalName "kenmyer@litwareinc.com" -Alias kenmyer -Database "Mailbox Database 1" -Name kenmyer -OrganizationalUnit "Redmond" –Password "ken395HJ2" -FirstName "Ken" -LastName "Myer -DisplayName "Ken Myer" -ResetPasswordOnNextLogon $True

     

    The Lync Server cmdlets won't do this; there's no way to create an Active Directory user account using a Lync Server cmdlet. In fact, and for the most part, the Lync Server cmdlets work only on Lync Server-specific attributes. For example, suppose Ken Myer has gotten a promotion and needs to have his job title changed to Assistant to the Regional Manager. Can you use, say, the Set-CsUser cmdlet to change his job title? No. You're going to have to look elsewhere in order to change a non-Lync Server attribute.

     

    Note. For example, this command mixes a little Lync Server PowerShell (the Get-CsAdUser cmdlet) and some generic Windows PowerShell in order to change Ken Myer's job title:

     

    $userDN = (Get-CsAdUser "Ken Myer").DistinguishedName

    $userAccount = [ADSI] "LDAP://$userDN"

    $userAccount.Title = "Assistant to the Regional Manager"

    $userAccount.SetInfo()

     

    And yes, we know: you're a little disappointed, aren't you? But here's something that will brighten your day: when it comes to user accounts, Exchange PowerShell and Lync Server PowerShell speak the same language. Why do you care about that? Well, for one thing, that means you can use the New-Mailbox cmdlet to create both an Active Directory user account and a new Exchange mailbox and then – in the very same command – pipe that user object to Enable-CsUser and enable that user for Lync Server!

     

    Hey, would we kid you about a thing like that?

     

    New-Mailbox -UserPrincipalName "kenmyer@litwareinc.com" -Alias kenmyer -Database "Mailbox Database 1" -Name kenmyer -OrganizationalUnit "Redmond" –Password "ken395HJ2" -FirstName "Ken" -LastName "Myer -DisplayName "Ken Myer" -ResetPasswordOnNextLogon $True | Enable-CsUser  -RegistrarPool pool0.litwareinc.com  –SipAddressType SamAccountName
    –SipDomain litwareinc.com

     

    Give it a try and see what happens.

     

    Oh, and yes, this also works with the Enable-Mailbox cmdlet. This command enables a user's Exchange mailbox and enables that user for Lync Server, all in one fell swoop:

     

    Enable-Mailbox "Ken Myer" | Enable-CsUser -RegistrarPool pool0.litwareinc.com  –SipAddressType SamAccountName –SipDomain litwareinc.com

     

    Cool, huh?