Test-CallRouting.ps1

Lync’s Roles-Based Access Control (RBAC) is great fun. It lets you fairly precisely define the rights of a user: you can easily grant or restrict access to specific commandlets, or apply scoping so that a commandlet can be run, but only against a subset of objects (like say a specific site).

I’m quite used to playing God in my deployments & never having to worry about such restrictions – until now.

I’ve recently found however that deploying a country into a global pool where my own access is subject to RBAC isn’t anywhere near as much fun. I’ve been running into all sorts of invisible walls as my usual open-slather access simply isn’t there. I understand and appreciate why this is necessary, and I don’t want you to think that I’m having a whinge – it’s presented as a bit of a backgrounder for you, and an intro for what’s to come.

One feature I *NORMALLY* enjoy access to is the ability to bung a user’s name and a phone number into the Lync Control Panel and have it tell me where the call’s going to be spat out – assuming it’s not barred of course. Alas, it transpires that when the Control Panel does this it creates a whole stack of new voice routing objects “-inmemory”, and here I run into one of those RBAC walls, as I’m not allowed to do that!

Not to be perturbed, I took to PowerShell and coded my way around it! Here we have “Test-CallRouting.ps1”.

Before & After

(Click on the two images for a larger view).

ControlPanel-TestCallWhen you’re blocked by RBAC you can click “Run” here all you like – NOTHING’S going to happen – not even a useful “Cannot run the commandlet because it is outside the user’s role-based access control (RBAC) scope”. Powershell-TestCall

 

Features

  • Accepts the username in all the same formats as “get-csuser” does – UPN, SIP URI, displayname – and you don’t need to specify “-identity” if it’s the first parameter
  • Outputs to the screen as an object, so you can pipe it to format-table and otherwise re-use the output downstream
  • ~Idiot-proofed (always a bold claim to make, I know!). Traps:
    • Bad / no user name
    • Bad / no DialedNumber
    • No Lync $PS session (more relevant if you’re doing Remote PowerShell)
    • Lync 2010 (no good as Get-CsEffectivePolicy was only introduced in Lync 2013)
    • If the user isn’t enabled for Enterprise Voice
    • If you’re blocked by RBAC (how cruel!)
  • Only uses “Get-” & two “Test-” commandlets, so hopefully it’s RBAC-friendly in your environment. If you hit that wall, membership of CsViewOnlyAdministrator and one of CsHelpDesk or CsVoiceAdministrator will get you through. (BTW, I sourced the details of these groups from Pat’s excellent XLS “All Lync 2013 Cmdlets and the Default RBAC Roles That Can Use Them“)
  • Tested against Windows 7, 8.1, Server 2008 & Server 2012
  • The download version of the script is signed
  • Localisation for variations on ‘English’ in the on-screen messages

Revision History

v1.0 – this is the initial release.

Download

The meat of the script is below, or you can Download the whole shebang (with a code-signing certificate) from here.

<#
.SYNOPSIS
This script replicates the "test voice routing / populate from user" in the Lync Control Panel, and is of
greatest benefit where your restrictions prevent you from using the Control Panel.

.DESCRIPTION
This script takes a username and dialled number, then retrieves the user's Dial Plan & Voice Policy.
It passes the dialled number to "Test-CsVoiceUser" and if the call would be routed, displays the Usage,
Route and Gateway that would route the call. If there are multiple gateways assigned to the route,
only the first is shown.

The script requires Lync 2013 and the user must have permission to execute "Get-CsEffectivePolicy" and
"Test-CsVoiceUser". Of the predefined RBAC roles, the minimum levels of membership are CsViewOnlyAdministrator
and one of CsHelpDesk or CsVoiceAdministrator.

.NOTES
Version : 1.0
Date : 3rd May 2014
Author : Greig Sheridan
Revision History :
v1.0 : 3rd May 2014

.LINK
https://greiginsydney.com/Test-CallRouting-ps1

.EXAMPLE
.\Test-CallRouting.ps1
Description
-----------
With no command-line parameters you will be prompted for a user name and a dialled number.

.EXAMPLE
.\Test-CallRouting.ps1 -Identity "greig@contoso.com" -DialledNumber "049912345678"
Description
-----------
Will determine the user's Dial Plan, Voice Policy, Normalise the dialled number and provide details of the
gateway that would route the call.

.PARAMETER Identity
A standard Identity name, as you'd provide to the "Get-CsUser" commandlet.

.PARAMETER DialedNumber
A telephone number in the format as dialled by the user.

#>

[CmdletBinding(SupportsShouldProcess = $True)]
param(
[parameter(Position=0,ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
[String]$Identity="",
[parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
[String]$DialedNumber=""
)

$Error.Clear()
$ErrorActionPreference = "stop"

#Setup some regionalisation for my American friends
# Let me know if any other countries (Locales) spell normalisation 'wrong' ;-)
$Locale = ""
try
{
#This is new in Win 8:
switch ((Get-WinSystemLocale).DisplayName)
{
{($_ -eq "English (Canada)") -or ($_ -eq "English (United States)")}
{
$Locale = "en-US"
}
default
{
$Locale = "TheRestOfUs"
}
}
}
catch {}

# This first command tests if you have a valid $PS session, and also tests the validity of the user account.
# (Get-csuser is one of the few commands available to EVERY standard RBAC group)
try
{
$getUser = invoke-expression "get-csuser -identity ""$Identity"" -ea silentlycontinue"
}
catch [System.Management.Automation.MethodInvocationException]
{
write-warning "The Lync PowerShell module is not loaded, or your session has expired"
#$_ | fl * -f
exit
}
catch [System.Management.Automation.CommandNotFoundException]
{
write-warning "The Lync PowerShell module is not loaded, or your session has expired"
#$_ | fl * -f
exit
}
catch
{
write-warning "Unhandled Error"
$_
#$_ | fl * -f
exit
}

$ServerVersion = ""
#Just in case your RBAC won't let you get to this one
try
{
$ServerVersion = get-csserverversion -ea silentlycontinue
}
catch {}
if ($ServerVersion -match "Server 2010")
{
write-warning "The detected Lync Server version is 2010. This script requires the Lync 2013 commandlet ""Get-CsEffectivePolicy"""
exit
}

if ($getUser -eq $null)
{
write-warning "User `"$($Identity)`" does not exist in AD or is not enabled for Lync"
exit
}

$sipaddress = [string]$getUser.SipAddress
if ($getUser.EnterpriseVoiceEnabled -ne $true)
{
write-warning "User `"$($Identity)`" is not enabled for Lync Enterprise Voice"
exit
}

try
{
$EffectiveUserPolicy = (Get-CsEffectivePolicy -Identity $Identity)
$vp = [string]($EffectiveUserPolicy.VoicePolicy)
$dp = [string]($EffectiveUserPolicy.LocationProfile)
}
catch [System.Management.Automation.CommandNotFoundException]
{
write-warning "You have no access to the required Lync 2013 'Get-CsEffectivePolicy' commandlet. (Try CsViewOnlyAdministrator)"
exit
}
catch
{
write-warning "Unhandled Error"
$_
#$_ | fl * -f
exit
}

$dialplan = [string](get-csdialplan -identity $dp).identity #This step ensures we get "Site:Lync2013RTM" and not "Site:4"
$voicepolicy = [string](get-csvoicepolicy -identity $vp).identity #This step ensures we get "Site:Lync2013RTM" and not "Site:4"

#Will RBAC let us do this?
try
{
$callpath = invoke-expression "Test-CsVoiceUser -DialedNumber ""$DialedNumber"" -SipUri ""$sipaddress"" | select MatchingUsage,TranslatedNumber,MatchingRule,FirstMatchingRoute"
}
catch [System.Management.Automation.CommandNotFoundException]
{
write-warning "You have no access to the required 'Test-CsVoiceUser' commandlet. (Try CsHelpDesk or CsVoiceAdministrator)"
exit
}

$translatedNumber = $callpath.TranslatedNumber

if ($callpath.MatchingRule -eq $null)
{
if ($Locale -eq "en-US")
{
write-warning "The dialed number was not normalized by the dial plan"
$translatedNumber = ""
}
else
{
write-warning "The dialled number was not normalised by the dial plan"
$translatedNumber = ""
}
}

$usage = $callpath.MatchingUsage
try
{
#This will error if we couldn't route the call
$firstRoute = $callPath | select -ExpandProperty FirstMatchingRoute
}
catch
{
$firstRoute = ""
}

switch (($firstRoute.pstngatewaylist).Count)
{
{($_ -eq "0") -or ($_ -eq $null)}
{
write-warning "The call cannot be routed"
}
1
{
$trunk = [string]($firstRoute.pstngatewaylist[0]) #Get the FIRST gateway in the list (just in case there are multiples)
$trunkname = get-cstrunk -identity $trunk
}
default
{
write-warning "More than one trunk was returned - only the first is shown"
$trunk = [string]($firstRoute.pstngatewaylist[0]) #Get the FIRST gateway in the list (just in case there are multiples)
$trunkname = get-cstrunk -identity $trunk
}
}

#Now create a custom object so we can output all this flexibly to screen or pipeline:
$Output = new-object PSObject
$Output | Add-Member NoteProperty -Name "SipAddress" -Value $sipaddress
$Output | Add-Member NoteProperty -Name "DialedNumber" -Value $dialedNumber
$Output | Add-Member NoteProperty -Name "DialPlan" -Value $dialplan
$Output | Add-Member NoteProperty -Name "VoicePolicy" -Value $voicepolicy
$Output | Add-Member NoteProperty -Name "Translated" -Value $translatedNumber
$Output | Add-Member NoteProperty -Name "Usage" -Value $usage
$Output | Add-Member NoteProperty -Name "SelectedRoute" -Value $firstRoute.Name
$Output | Add-Member NoteProperty -Name "FirstTrunk" -Value $trunkname.Identity
write-output $Output

 

Please let me know if you encounter any problems with it, or can suggest improvements or enhancements.
 
 

– G.

Leave a Reply

Your email address will not be published. Required fields are marked *

... and please just confirm for me that you're not a bot first: Time limit is exhausted. Please reload the CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.