<?xml version="1.0"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
    <title>Paul's UC and Dev/Ops Blog</title>
    <link>http://paul.vaillant.ca</link>
    <atom:link href="http://paul.vaillant.ca/rss.xml" rel="self" type="application/rss+xml" />
    <description></description>
    <language>en-us</language>
    <pubDate>Thu, 17 Nov 2016 18:23:59 +0000</pubDate>
    <lastBuildDate>Thu, 17 Nov 2016 18:23:59 UTC</lastBuildDate>

    
    <item>
      <title>Document Your ThinkTel DIDs</title>
      <link>http://paul.vaillant.ca/2015/06/05/document-your-thinktel-dids.html</link>
      <pubDate>Fri, 05 Jun 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/06/05/document-your-thinktel-dids.html</guid>
      <description>&lt;p&gt;Documentation is important. It’s key to being able to work together in teams and not have to rely on information based informally from person to person. I’ve talked before about how to &lt;a href=&quot;/2015/05/01/document-your-powershell-scripts.html&quot;&gt;document your PowerShell scripts&lt;/a&gt; but did you know you can also label each DID on your ThinkTel SIP trunk? This lets you record who or what the DID is for and can be very useful since it is shown in CDR reports, letting you know who originated or received individual calls. This can also be a big step towards not needing to document your phone number assignments in a separate Excel spreadsheet.&lt;/p&gt;

&lt;p&gt;While the script below is specifically for Microsoft Lync or Skype for Business, it could easily be adapted for any system where the information on the allocated DID and the assigned user was available using PowerShell. This could be a backend database, a directory of some kind or web service API.&lt;/p&gt;

&lt;h2 id=&quot;get-ucontrol-labels&quot;&gt;Get uControl Labels&lt;/h2&gt;

&lt;p&gt;Using previously shown PowerShell (see &lt;a href=&quot;/2015/04/10/keeping-lync-unassigned-numbers-updated.html&quot;&gt;Keeping Lync Unassigned Numbers Updated&lt;/a&gt;), we start by downloading all the DIDs and their labels from uControl.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$sipTrunkDidsUrl = &quot;https://api.thinktel.ca/REST.svc/SipTrunks/{0}/Dids?PageFrom=0&amp;amp;PageSize=100000&quot; -f $SipPilotNumber
$didsList = Invoke-RestMethod -URI $sipTrunkDidsUrl -Credential $Credential -Method GET -ContentType &quot;text/xml&quot;
if(-not $didsList -or -not $didsList.ArrayOfTerseNumber) {
  Write-Error &quot;Failed to load or parse DIDs on SIP Pilot $SipPilotNumber&quot;
  exit
}

$existingLabels = @{}
$didsList.ArrayOfTerseNumber.TerseNumber | foreach {
  $existingLabels.Add($_.Number, $_.Label)
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One note, and you’ll see this below too, I’m using Invoke-RestMethod here so this will need to be run on a machine with PowerShell 3.0+ since that’s when this cmdlet was introduced. This was released with Windows 2012 R1 so hopefully you have at least that. If you did have Windows 2008 R2 for some reason, my sympathies, but you could use System.Net.WebClient instead. If anyone has this situation, and is interested in a version of this script for PowerShell 2.0, let me know.&lt;/p&gt;

&lt;h2 id=&quot;get-labels-from-skype-for-business&quot;&gt;Get Labels From Skype for Business&lt;/h2&gt;

&lt;p&gt;Next step, again using previously shown PowerShell (see &lt;a href=&quot;/2015/03/18/managing-your-lync-phone-numbers.html&quot;&gt;Managing Your Lync Phone Numbers&lt;/a&gt;), we get a list of all numbers assigned and a label to represent to what they are assigned to.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$currentLabels = @()
function AddCurrentLabel($telUri, $label) {
  if($telUri -notmatch '^tel:+1') {
    Write-Warning &quot;Skipping $telUri ($label) because it doesn't start with tel:+1&quot;
  } elseif($telUri -match ';ext=') {
    Write-Warning &quot;Skipping $telUri ($label) because it contains an extension&quot;
  } else {
    # $telUri will start with tel:+1 so let's skip that
    $number = $telUri.Substring(6)
    $currentLabels.Add($number, $label)
  }
}
Get-CsUser -Filter {LineURI -ne $Null} | foreach {
  AddCurrentLabel($_.LineURI, &quot;User &quot; + $_.SipAddress)
}
Get-CsUser -Filter {PrivateLine -ne $Null} | foreach {
  AddCurrentLabel($_.PrivateLine, &quot;User &quot; + $_.SipAddress + &quot; (private line)&quot;)
}
Get-CsAnalogDevice -Filter {LineURI -ne $Null} | foreach {
  AddCurrentLabel($_.LineURI, &quot;Analog Device &quot; + $_.DisplayName)
}
Get-CsCommonAreaPhone -Filter {LineURI -ne $Null} | foreach {
  AddCurrentLabel($_.LineURI, &quot;Common Area Phone &quot; + $_.DisplayName)
}
Get-CsRgsWorkflow | ?{ $_.LineURI } | foreach {
  AddCurrentLabel($_.LineURI, &quot;RGS Workflow &quot; + $_.PrimaryAddress)
}
Get-CsDialInConferencingAccessNumber -Filter {LineURI -ne $Null} | foreach {
  AddCurrentLabel($_.LineURI, &quot;Dial In Conf Number &quot; + $_.DisplayName)
}
Get-CsExUmContact -Filter {LineURI -ne $Null} | foreach {
  AddCurrentLabel($_.LineURI, &quot;ExUmContact &quot; + $_.DisplayName)
}
Get-CsTrustedApplicationEndpoint -Filter {LineURI -ne $Null} | foreach {
  AddCurrentLabel($_.LineUri, &quot;Trusted App Endpoint &quot; + $_.SipAddress)
}
Get-CsMeetingRoom -Filter {LineURI -ne $Null} | foreach {
  AddCurrentLabel($_.LineURI, &quot;Meeting Room &quot; + $_.SipAddress)
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For users, the choice of label is obvious (name and SIP address), and for other objects there is generally a description or display name available. In a worse case scenario, I find it sufficient to just note the kind of usage.&lt;/p&gt;

&lt;h2 id=&quot;find-updates&quot;&gt;Find Updates&lt;/h2&gt;

&lt;p&gt;Then we compare the two hashes:
 * We check for any labels that are only in the current set; those are ones that don’t exist on the SIP trunk since at worse an empty label would be returned for each DID,
 * We check for any labels that are only in the existing set; those are old ones that don’t exist in Skype for Business,
 * We check all the labels in both to see if they match, other wise we update it&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$newLabels = @{}
foreach($num in $currentLabels.Keys) {
  if($existingLabels.ContainsKey($num)) {
    # this is a valid number on the trunk
    if($currentLabels[$num] -ne $existingLabels[$num]) {
      # the label needs to be updated
      $newLabels.Add($num, $currentLabels[$num])
    }
  } else {
    # this is not a number on the trunk
    Write-Warning &quot;Skipping $num since it isn't on the trunk&quot;
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;put-data-back-to-ucontrol&quot;&gt;PUT data back to uControl&lt;/h2&gt;

&lt;p&gt;Last, but not least, we then send this data back to uControl.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
foreach($num in $newLabels.Keys) {
  $label = $newLabels[$num]
  Write-Verbose &quot;Updating $num ($label)&quot;

  # we actually need to start by getting the current number information
  # this ensures we don't clear out an existing number translation
  $sipTrunkDidUrl = &quot;https://api.thinktel.ca/REST.svc/SipTrunks/{0}/Dids/{1}&quot; -f $SipPilotNumber,$num
  $d = Invoke-RestMethod -URI $sipTrunkDidUrl -Credential $Credential -Method GET -ContentType &quot;application/json&quot;
  if(-not $d -or $d.Number -ne $num) {
    Write-Error &quot;Failed to load information for DID $num&quot;
    continue
  }

  # update the DID label
  $d.Label = $label

  # now POST this back to $sipTrunkDidUrl
  if($PSCmdlet.ShouldProcess($num,&quot;Update DID label&quot;)) {
    Invoke-RestMethod -URI $sipTrunkDidUrl -Credential $Credential -Method &quot;POST&quot; -ContentType &quot;application/json&quot; -Body $d
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You might wonder why I’m downloading each DID instead of just building it the response in memory. The initial GET query we did for all the DIDs doesn’t have all the information for the DID. So features like number translation would be over-written if we didn’t include it in the PUT body. You can see the difference since the initial GET query doesn’t return an ArrayOfDid, instead it’s an ArrayOfTerseNumber, with TerseNumber only having Number and Label instead of all the DID information.&lt;/p&gt;

&lt;p&gt;Note too the &lt;a href=&quot;https://msdn.microsoft.com/en-us/library/ms568271%28v=vs.85%29.aspx&quot;&gt;$PSCmdlet.ShouldProcess&lt;/a&gt; used above. That, plus &lt;a href=&quot;https://msdn.microsoft.com/en-us/library/system.management.automation.cmdletbindingattribute%28v=vs.85%29.aspx&quot;&gt;[CmdletBinding(SupportShouldProcess=$true)]&lt;/a&gt; above param() is what makes -WhatIf work so that you can see what changes would be made before actually committing to making the changes.&lt;/p&gt;

&lt;p&gt;And that’s all there is to it. Now, if you set this up as a scheduled task, your current number assignments will be reflected automatically into the uControl web portal.&lt;/p&gt;

&lt;h2 id=&quot;download-the-script&quot;&gt;Download the Script&lt;/h2&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Update-ThinkTelDidLabels.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Update-ThinkTelDidLabels.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Create a PDF from Images with PowerShell</title>
      <link>http://paul.vaillant.ca/2015/05/28/create-a-pdf-from-images-with-powershell.html</link>
      <pubDate>Thu, 28 May 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/05/28/create-a-pdf-from-images-with-powershell.html</guid>
      <description>&lt;p&gt;I don’t know how your organization is but my experience is that all organizations have processes and invariably one, or more, of those processes can be &lt;a href=&quot;http://www.urbandictionary.com/define.php?term=BYZANTINE&quot;&gt;byzantine&lt;/a&gt;. I experienced this first hand recently when I was asked to provide PDF versions of expense receipts.&lt;/p&gt;

&lt;h1 id=&quot;my-workflow&quot;&gt;My Workflow&lt;/h1&gt;

&lt;p&gt;So what I’ve been doing is taking a picture of receipts using my Windows Phone, which is set to upload a copy of my pictures to my OneDrive and then I was sending those pictures in. This was very easy and very convenient for me since I travel with my phone. I didn’t need to wait to get back into the office and I didn’t need yet another device in my bag or need to stop and setup something connected to my laptop. How to provide PDFs without completely doing away with my convinience?&lt;/p&gt;

&lt;p&gt;NOTE: for my purposes these are receipts, but the same could apply for anything that you’re taking a picture of but want to convert into Word or PDF format, possibly with additional content.&lt;/p&gt;

&lt;h1 id=&quot;wordapplication&quot;&gt;Word.Application&lt;/h1&gt;

&lt;p&gt;This is the &lt;a href=&quot;http://en.wikipedia.org/wiki/Apollo_13_%28film%29&quot;&gt;Apollo 13&lt;/a&gt; moment where they put one of everything that they have in the capsule on the table in front of the engineers and ask them to come up with a way to put a square filter in a round hole.&lt;/p&gt;

&lt;p&gt;I have Office, and Word can insert images into a document and save it as a PDF, so now I just have to automate the process.&lt;/p&gt;

&lt;p&gt;So first thing is that I want some flexibility in how I call this, specifically so I can use &lt;em&gt;&lt;a href=&quot;https://technet.microsoft.com/library/hh847897%28v=wps.630%29.aspx&quot;&gt;ls&lt;/a&gt;&lt;/em&gt; and &lt;em&gt;&lt;a href=&quot;https://technet.microsoft.com/library/6a70160b-cf62-48df-ae5b-0a9b173013b4%28v=wps.630%29.aspx&quot;&gt;where&lt;/a&gt;&lt;/em&gt; to select the files I want, so I need to be able to accept images from the pipeline. Sometimes I also want to create a single document instead of more than one, and sometimes I want to remove the images once they have been converted, so I have a parameters for those options.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
[CmdletBinding()]
param(
	[Parameter(ValueFromPipeline=$true)][string[]]$Images,
	[Parameter()][string]$CombinedFilename
)

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next I do the initial setup of creating a new instance of Word. I also setup some variables for use in the combined file case.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
BEGIN {
	$word = New-Object -ComObject Word.Application

	$doc = $null
	$first = $true
	if($CombinedFilename) {
		Write-Verbose &quot;Creating combined document&quot;
		$doc = $word.Documents.Add()
	}
}

&lt;/code&gt;&lt;/pre&gt;

&lt;h1 id=&quot;creating-documents-and-saving-them&quot;&gt;Creating Documents and Saving Them&lt;/h1&gt;

&lt;p&gt;Now we need to actually create the documents. For each images, we create a new document as needed, use &lt;a href=&quot;https://msdn.microsoft.com/en-us/library/microsoft.office.interop.word.inlineshapes.addpicture%28v=office.14%29.aspx&quot;&gt;AddPicture&lt;/a&gt; to add it to the current document and either save/close the document (if they are separate documents) or insert a new page for combined documents.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
PROCESS {
	foreach($img in $Images) {
		if(!$doc) {
			$doc = $word.Documents.Add()
		}

		#6 is [Microsoft.Office.Interop.Word.wdunits]::wdstory
		$word.Selection.EndKey(6) | Out-Null
		$word.Selection.InlineShapes.AddPicture($img) | Out-Null
		if($CombinedFilename) {
			if($first) {
				$first = $false
			} else {
				$word.Selection.InsertNewPage()
			}
		}

		if(!$CombinedFilename) {
			$pdf = $img.Substring(0, $img.LastIndexOf('.')) + &quot;.pdf&quot;
			#17 is [microsoft.office.interop.word.WdSaveFormat]::wdFormatPDF
			$doc.SaveAs([ref]$pdf, [ref]17)
			#0 is [microsoft.office.interop.word.wdsaveoptions]::wdDoNotSaveChanges
			$doc.Close([ref]0)
			$doc = $null
		}
	}
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One thing to note; I’m using the numeric value for &lt;a href=&quot;https://msdn.microsoft.com/en-us/library/microsoft.office.interop.word.wdsaveformat(v=office.14).aspx&quot;&gt;WdSaveFormat&lt;/a&gt; and &lt;a href=&quot;https://msdn.microsoft.com/en-us/library/microsoft.office.interop.word.wdsaveoptions%28v=office.14%29.aspx&quot;&gt;WdSaveOptions&lt;/a&gt; for testability. Specifically, I want to be able to test my scripts without having Word installed or the COM interop classes loaded.&lt;/p&gt;

&lt;h1 id=&quot;wrapping-it-up&quot;&gt;Wrapping It Up&lt;/h1&gt;

&lt;p&gt;With all the hard work done, now it’s just about saving the combined file, if that’s what we’re creating, and quitting Word.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
END {
	if($CombinedFilename) {
		$doc.SaveAs([ref]$CombinedFilename, [ref]17)
		#0 is [microsoft.office.interop.word.wdsaveoptions]::wdDoNotSaveChanges
		$doc.Close([ref]0)
	}

	#0 is [microsoft.office.interop.word.wdsaveoptions]::wdDoNotSaveChanges
	$word.Quit([ref]0)
	$word = $null
}

&lt;/code&gt;&lt;/pre&gt;

&lt;h1 id=&quot;lets-not-forget-testing&quot;&gt;Let’s Not Forget Testing&lt;/h1&gt;

&lt;p&gt;I’ve talked about testing before so let’s not forget this important part. The testing in this case is a little different than the last time. This time we need to create &lt;a href=&quot;http://en.wikipedia.org/wiki/Mock_object&quot;&gt;Mocks&lt;/a&gt;. No problem, &lt;a href=&quot;https://github.com/pester/Pester/wiki/Mocking-with-Pester&quot;&gt;Pester supports mocking&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first step is to setup functions to mimic the Word APIs we’re going to call. One stub function for each thing we’re going to test happened.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
function QuitWord { param($opts) }
function AddDocument { }
function AddPicture { param($path) }
function InsertNewPage { }
function SaveDocument { param($path, $opts) }
function CloseDocument { param($opts) }

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next is some helper functions to make the mock statements cleaner. Note that some of the Word methods use reference parameters so in PowerShell we use [ref] but that results in a &lt;a href=&quot;https://msdn.microsoft.com/en-us/library/system.management.automation.psreference%28v=vs.85%29.aspx&quot;&gt;PSReference&lt;/a&gt; type object. Pester would have a hard time matching those with the ParameterFilter so before the call to the stub function, I unwrap the value from the PSReference object.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
function NewDocument {
  $doc = New-Object psobject
  $doc | Add-Member -MemberType ScriptMethod -Name &quot;SaveAs&quot; -Value {
    param([ref]$path,[ref]$opts)
    SaveDocument $path.Value $opts.Value
  }
  $doc | Add-Member -MemberType ScriptMethod -Name &quot;Close&quot; -Value {
    param([ref]$opts)
    CloseDocument $opts.Value
  }
  return $doc
}

function NewWordApp {
  $docs = New-Object psobject
  $docs | Add-Member -MemberType ScriptMethod -Name &quot;Add&quot; -Value { AddDocument }

  $shapes = new-object psobject
  $shapes | Add-Member -MemberType ScriptMethod -Name &quot;AddPicture&quot; -Value {
    param($path)
    AddPicture $path
  }

  $sel = New-Object psobject @{InlineShapes = $shapes}
  $sel | Add-Member -MemberType ScriptMethod -Name &quot;EndKey&quot; -Value {
    param($opts)
    # ... no-op
  }
  $sel | Add-Member -MemberType ScriptMethod -Name &quot;InsertNewPage&quot; -Value {
    InsertNewPage
  }

  $word = New-Object psobject @{Visible = $false; Documents = $docs; Selection = $sel}
  $word | Add-Member -MemberType ScriptMethod -Name &quot;Quit&quot; -Value {
    param([ref]$opts)
    QuitWord $opts.Value
  }

  return $word
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally the actual tests. You can see one mock statement for each of our stub functions.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
Describe &quot;Convert-JpgToPdf.ps1&quot; {
  Mock New-Object { NewWordApp } -Verifiable -ParameterFilter { $ComObject -eq 'Word.Application' }

  Mock QuitWord { param($opts) }
  Mock AddDocument { NewDocument }
  Mock AddPicture { param($path) }
  Mock InsertNewPage { }
  Mock SaveDocument { param($path, $opts) }
  Mock CloseDocument { param($opts) }

  Context &quot;Images on the command line as separate documents&quot; {
    $images = @(&quot;fake-image1.jpg&quot;, &quot;fake-image2.jpg&quot;)
    &amp;amp; $cmd -Images $images

    It &quot;creates a word instance&quot; {
      Assert-VerifiableMocks
    }

    It &quot;creates 2 documents each with 1 image&quot; {
      Assert-MockCalled AddDocument -Exactly -Times 2
      foreach($img in $images) {
        Assert-MockCalled AddPicture -ParameterFilter { $path -eq $img }
      }
      Assert-MockCalled InsertNewPage -Exactly -Times 0
      Assert-MockCalled SaveDocument -Exactly -Times 2
      foreach($img in $images) {
        $pdf = $img -replace '\.jpg$','.pdf'
        Assert-MockCalled SaveDocument -ParameterFilter { $path -eq $pdf -and $opts -eq 17 }
      }
      Assert-MockCalled CloseDocument -Exactly -Times 2 -ParameterFilter { $opts -eq 0 }
    }

    It &quot;closes word nicely&quot; {
      Assert-MockCalled QuitWord -Exactly -Times 1 -ParameterFilter { $opts -eq 0 }
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There’s also a context for images on the command line as a combined document and the same (separate and combined document) contexts for images from the pipeline. See &lt;a href=&quot;https://github.com/pvaillant/pvaillant.github.io/blob/master/content/Convert-JpgToPdf.Tests.ps1&quot;&gt;Convert-JpgToPdf.Tests.ps1&lt;/a&gt; for all the tests.&lt;/p&gt;

&lt;h1 id=&quot;mocks-mocks-mocks-baked-beans-and-mocks&quot;&gt;Mocks, mocks, mocks, baked beans and mocks&lt;/h1&gt;

&lt;p&gt;There are other ways to achieve similar results. When I first wrote these tests I didn’t have the 6 stub functions. Instead I added the fake $doc to a script: scoped array each time I created it and in the &lt;em&gt;It&lt;/em&gt;, I looped over it to make sure they were as expected. Sadly that didn’t work. I had a number of Write-Verbose statements, and I could see that they were being created and added but the array was empty when I checked it after.&lt;/p&gt;

&lt;p&gt;A change in Pester 3.0 turned out the be the cause. From &lt;a href=&quot;https://github.com/pester/Pester/wiki/What%27s-New-in-Pester-3.0%3F&quot;&gt;What’s New in Pester 3.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;“Tests.ps1 scripts are now executed in a separate scope than Pester’s internal code, preventing some types of bugs that would occur when a test script happened to define a function or variable name that matched something Pester uses internally (or mock calls to a function that Pester needs internally.)””&lt;/p&gt;

&lt;p&gt;So the script: scope of my array was the issue. Not wanting to pollute my global scope restructured my tests to use all mocks instead.&lt;/p&gt;

&lt;h1 id=&quot;download-the-script&quot;&gt;Download the Script&lt;/h1&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Convert-JpgToPdf.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Convert-JpgToPdf.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1 id=&quot;download-the-tests&quot;&gt;Download the Tests&lt;/h1&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Convert-JpgToPdf.Tests.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Convert-JpgToPdf.Tests.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Testing Nirvana - 100% Code Coverage</title>
      <link>http://paul.vaillant.ca/2015/05/22/testing-nirvana-100-precent-code-coverage.html</link>
      <pubDate>Fri, 22 May 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/05/22/testing-nirvana-100-precent-code-coverage.html</guid>
      <description>&lt;p&gt;In my last post I introduced testing a PowerShell scripts, now lets take that a little deeper and improve on the testing.&lt;/p&gt;

&lt;p&gt;“Improve on the testing” you say, “how is that possible?”&lt;/p&gt;

&lt;p&gt;When you are testing a script, or function or any piece of code, you want to be sure not only to test the &lt;em&gt;main path&lt;/em&gt;, the normal case, but all the different execution paths. Every &lt;em&gt;if&lt;/em&gt; statement takes the execution through a different sequence and if there are some without test cases then you might have issues (that’s fancy for bugs) that are going to bite you later on.&lt;/p&gt;

&lt;p&gt;There are a couple of different ways to go about doing this.&lt;/p&gt;

&lt;h2 id=&quot;test-driven-development&quot;&gt;Test Driven Development&lt;/h2&gt;

&lt;p&gt;One option is to write your tests before you write your code (something called Test Driven Development or &lt;a href=&quot;http://en.wikipedia.org/wiki/TDD&quot;&gt;TDD&lt;/a&gt;). If you’re following this style of development then you’re creating a test before you even add a parameter or think about processing one kind of data differently. This option is also very good for the testability (that’s fancy for how easy it is to test) of your implementation because it forces you to find a way to test before you even start writing, rather than struggling to find a way to test something you’ve already written, and possibly having to go back and rewrite it to make it testable.&lt;/p&gt;

&lt;h2 id=&quot;code-coverage&quot;&gt;Code Coverage&lt;/h2&gt;

&lt;p&gt;Another option, which is particularly useful if you are writing your tests after the fact, is to use the Code Coverage feature of Pester. What is &lt;a href=&quot;http://en.wikipedia.org/wiki/Code_coverage&quot;&gt;Code Coverage&lt;/a&gt;? Code coverage means what percentage of your code is covered by tests. A particular line of code is considered covered if it is executed by one or more test cases. Having 100% code coverage should be your ideal goal although sometimes it’s not possible. If you really aren’t able to achieve 100% code coverage you should be sure it’s for the right reasons and seriously consider if it may be because the implementation isn’t testable rather than the test case not being testable.&lt;/p&gt;

&lt;h2 id=&quot;dead-code&quot;&gt;Dead Code&lt;/h2&gt;

&lt;p&gt;Code coverage, when you are using it after the fact, can also help discover dead code. I once read an article that &lt;em&gt;software developer&lt;/em&gt; was a terrible term because it made it sound like there was a definate end. Instead, the article proposed the term &lt;em&gt;software gardener&lt;/em&gt; because software can be very much like a living thing that grows but also needs pruning. When you have parts of your application that are no longer used, the &lt;em&gt;right thing &amp;amp;trad;&lt;/em&gt; to do isn’t to leave them there but to remove them. Besides, if you ever needed them again, you could just checkout an older copy of your code from your SCM (you are using some kind of &lt;a href=&quot;http://en.wikipedia.org/wiki/Revision_control&quot;&gt;Revision Control&lt;/a&gt; tool like &lt;a href=&quot;http://git-scm.com&quot;&gt;Git&lt;/a&gt; aren’t you?).&lt;/p&gt;

&lt;h2 id=&quot;so-how-did-i-do&quot;&gt;So How Did I Do?&lt;/h2&gt;

&lt;p&gt;A quick check of the code coverage of the tests from my last example shows less than 100%. Let’s dig into why.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
PS&amp;gt; Invoke-Pester .\Get-PhoneNumberClass.Tests.ps1 -CodeCoverage .\Get-PhoneNumberClass.ps1

Code coverage report:
Covered 67.97 % of 128 analyzed commands in 1 file.

Missed commands:

File                     Function     Line Command
----                     --------     ---- -------
Get-PhoneNumberClass.ps1 ClassifySlow  156 Write-Verbose &quot;Classifying $number (slow)&quot;
Get-PhoneNumberClass.ps1 ClassifySlow  157 $class = &quot;Ordinary&quot;
Get-PhoneNumberClass.ps1 ClassifySlow  158 $reason = &quot;&quot;
Get-PhoneNumberClass.ps1 ClassifySlow  159 @(&quot;Gold&quot;,&quot;Silver&quot;,&quot;Bronze&quot;)
Get-PhoneNumberClass.ps1 ClassifySlow  159 &quot;Gold&quot;,&quot;Silver&quot;,&quot;Bronze&quot;
Get-PhoneNumberClass.ps1 ClassifySlow  160 $CLASSES[$c].GetEnumerator()
Get-PhoneNumberClass.ps1 ClassifySlow  160 foreach {...
Get-PhoneNumberClass.ps1 ClassifySlow  161 if($number -match $_.Value) {...
Get-PhoneNumberClass.ps1 ClassifySlow  162 $class = $c
Get-PhoneNumberClass.ps1 ClassifySlow  163 $reason = $_.Key
Get-PhoneNumberClass.ps1 ClassifySlow  168 @{Number = $number; Class = $class; Reason = $reason}
Get-PhoneNumberClass.ps1 ClassifySlow  168 Number = $number
Get-PhoneNumberClass.ps1 ClassifySlow  168 Class = $class
Get-PhoneNumberClass.ps1 ClassifySlow  168 Reason = $reason
Get-PhoneNumberClass.ps1               196 $numbers = 0..$RunSize | %{ Get-Random -Minimum 1991000000 -Maximum 99999...
Get-PhoneNumberClass.ps1               196 $numbers = 0..$RunSize | %{ Get-Random -Minimum 1991000000 -Maximum 99999...
Get-PhoneNumberClass.ps1               196 Get-Random -Minimum 1991000000 -Maximum 9999999999
Get-PhoneNumberClass.ps1               198 Measure-Command { foreach($n in $numbers) { ClassifyFast $n | out-null } }
Get-PhoneNumberClass.ps1               198 $numbers
Get-PhoneNumberClass.ps1               198 ClassifyFast $n
Get-PhoneNumberClass.ps1               198 out-null
Get-PhoneNumberClass.ps1               199 select @{n='Name';e={'Fast'}},TotalMilliseconds
Get-PhoneNumberClass.ps1               199 n = 'Name'
Get-PhoneNumberClass.ps1               199 e = {'Fast'}
Get-PhoneNumberClass.ps1               199 'Fast'
Get-PhoneNumberClass.ps1               201 Measure-Command { foreach($n in $numbers) { ClassifySlow $n | out-null } }
Get-PhoneNumberClass.ps1               201 $numbers
Get-PhoneNumberClass.ps1               201 ClassifySlow $n
Get-PhoneNumberClass.ps1               201 out-null
Get-PhoneNumberClass.ps1               202 select @{n='Name';e={'Slow'}},TotalMilliseconds
Get-PhoneNumberClass.ps1               202 n = 'Name'
Get-PhoneNumberClass.ps1               202 e = {'Slow'}
Get-PhoneNumberClass.ps1               202 'Slow'
Get-PhoneNumberClass.ps1               204 Measure-Command { foreach($n in $numbers) { ClassifySlowOptimized $n | ou...
Get-PhoneNumberClass.ps1               204 $numbers
Get-PhoneNumberClass.ps1               204 ClassifySlowOptimized $n
Get-PhoneNumberClass.ps1               204 out-null
Get-PhoneNumberClass.ps1               205 select @{n='Name';e={'Slow Optimized'}},TotalMilliseconds
Get-PhoneNumberClass.ps1               205 n = 'Name'
Get-PhoneNumberClass.ps1               205 e = {'Slow Optimized'}
Get-PhoneNumberClass.ps1               205 'Slow Optimized'

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Right away I can see that this points to two sections in my script; section 1 is lines 156 - 168 (the function ClassifySlow) and section 2 is lines 196-205 (this is the part of PROCESS dealing with the -Test flag).&lt;/p&gt;

&lt;p&gt;How to remedy this? I’ve got a couple options here. I could remove both sections, actually I could go further and remove the -Slow option as well. Since this would introduce an incompatible backwards change (you couldn’t call the script with -Test and get the behavior you currently expected) that would mean bumping this to version 2.0.0. If you’re unfamilar with versioning, I strongly recommend reading &lt;a href=&quot;http://semver.org/&quot;&gt;Semantic Versioning&lt;/a&gt; and following it as a standard. If you are familar with SemVer then you’ll have recognized this already. Another option would be to add test cases that covered both sections. And a third option would be a little of both. That’s actually the option I’m going to go for.&lt;/p&gt;

&lt;p&gt;For section 1 I’m just going to remove it since that version of the Classify function was one I did purely to compare the performance of different approaches and the only place it’s used currently is by the -Test section. For section 2, I’ll add a test case to cover this function:&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
Context &quot;When a performance test is specified&quot; {
    $ExpectedAlgorithms = @(&quot;Fast&quot;, &quot;Slow&quot;)
    It &quot;returns a list of algorithms and their total milliseconds&quot; {
        [array]$results = &amp;amp; $cmd -Test
        $results.Length | Should Be $ExpectedAlgorithms.Length
        foreach($algo in $ExpectedAlgorithms) {
            $r = $results | where Name -eq $algo
            # in the latest Pester this is the same as
            # $r | Should Exist
            $r | Should Not Be $null
            $r.TotalMilliseconds | Should Match &quot;^\d+\.\d+$&quot;
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;From a semantic versioning perspective, given that the output of -Test is an array of hashes with the algorithm name and a time taken, I interpret this to be a patch level change; a backwards compatible bug fix. If the output of -Test had been an object with each algorithm as a property like this:&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
PS&amp;gt; [pscustomobject]@{Fast = 0.234; Slow = 0.345; SlowOptimized = 0.456}

 Fast  Slow SlowOptimized
 ----  ---- -------------
0.234 0.345         0.456

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then that would have been a backwards incompatible change.&lt;/p&gt;

&lt;h2 id=&quot;whats-the-coverage-now&quot;&gt;What’s The Coverage Now?&lt;/h2&gt;

&lt;p&gt;So with those changes, what’s the coverage look like?&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
PS&amp;gt; Invoke-Pester .\Get-PhoneNumberClass.Tests.ps1 -CodeCoverage .\Get-PhoneNumberClass.ps1

Code coverage report:
Covered 100.00 % of 106 analyzed commands in 1 file.

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now that’s what I want to see!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Testing Your PowerShell Scripts</title>
      <link>http://paul.vaillant.ca/2015/05/18/testing-your-powershell-scripts.html</link>
      <pubDate>Mon, 18 May 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/05/18/testing-your-powershell-scripts.html</guid>
      <description>&lt;p&gt;Continuing from the exciting week last week at &lt;a href=&quot;http://ignite.microsoft.com&quot;&gt;Microsoft Ignite 2015&lt;/a&gt; in Chicago, I have to say that one of the things I was most amazed with was &lt;a href=&quot;https://twitter.com/jsnover&quot;&gt;Jeffery Snover&lt;/a&gt;’s announcement that the next version of Windows would ship with open-source software! I think secretly he was very thrilled by this as well because every session of his that I was in, I noted that he mentioned this over and over again. Maybe it’s not the open-source aspect that he was most interested in, rather what this software is.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/pester/Pester&quot;&gt;Pester&lt;/a&gt;, the software included now with Windows 10, is a testing framework for PowerShell. Testing is a key part of software development, as any dev will tell you, and it make sense given Microsoft’s increased focus on making PowerShell dev friendly (see the class keyword as another great example of this). And making it dev friendly is a key part of the new push for &lt;a href=&quot;http://en.wikipedia.com/wiki/DevOps&quot;&gt;DevOps&lt;/a&gt; in Windows.&lt;/p&gt;

&lt;h2 id=&quot;a-pester-overview&quot;&gt;A Pester Overview&lt;/h2&gt;

&lt;p&gt;So I thought I’d talk about how to test a PowerShell script using the example from my latest post &lt;a href=&quot;/2015/05/11/classifying-phone-numbers.html&quot;&gt;Classifying Phone Numbers&lt;/a&gt;. The first thing you need to do is create a tests file. The convention here is to have the same file name but with a suffix of .tests.ps1 instead of .ps1. For example, for a script file called Get-PhoneNumberClass.ps1, the tests file would be called Get-PhoneNumberClass.Tests.ps1.&lt;/p&gt;

&lt;p&gt;Next, let’s look inside this tests file:&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(&quot;.Tests.&quot;, &quot;.&quot;)
$cmd = &quot;$here\$sut&quot;

Describe &quot;My-Script&quot; {
  Context &quot;...the name of my context...&quot; {
    It &quot;...does something...&quot; {
      $result = &amp;amp; $cmd
      $result.Value | Should Be &quot;my expected value&quot;
    }
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This style of testing is referred to as &lt;a href=&quot;http://en.wikipedia.org/wiki/Behavior-driven_development&quot;&gt;Behavior driven development&lt;/a&gt; and abbreviated as BDD.&lt;/p&gt;

&lt;p&gt;The idea is to look at it logically,
 1. Describe: this is the high level thing (like a script or function) that’s being &lt;em&gt;described&lt;/em&gt; (tested)
 2. Context: a specific context, or scenario, within the larger system
 3. It: a specific aspect or behavior within the context
 4. Should: an expected result of the behavior&lt;/p&gt;

&lt;h2 id=&quot;get-phonenumberclasstestsps1&quot;&gt;Get-PhoneNumberClass.Tests.ps1&lt;/h2&gt;

&lt;p&gt;Let’s take a look at some real-world tests. In my last post I created a script that would use regular expressions to classify phone numbers into Gold, Silver, Bronze and Ordinary classes based on how &lt;em&gt;nice&lt;/em&gt; the number looks.&lt;/p&gt;

&lt;p&gt;The first thing are the various test cases; you need to have some inputs and expected outputs that will help test the various paths within your script.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$TestCases = @(
    @{Number = 7000011222; Class = &quot;Gold&quot;; Reason = &quot;doubleTriple&quot;},
    @{Number = 7000033440; Class = &quot;Gold&quot;; Reason = &quot;doubleDouble0&quot;},
    @{Number = 7000005555; Class = &quot;Gold&quot;; Reason = &quot;same4&quot;},
    @{Number = 7000006660; Class = &quot;Gold&quot;; Reason = &quot;triple0&quot;},
    @{Number = 7000007890; Class = &quot;Gold&quot;; Reason = &quot;sequential4&quot;},
    @{Number = 7005242110; Class = &quot;Silver&quot;; Reason = &quot;double0&quot;},
    @{Number = 7005242007; Class = &quot;Silver&quot;; Reason = &quot;bond&quot;},
    @{Number = 7005223434; Class = &quot;Silver&quot;; Reason = &quot;twoDigitPattern&quot;},
    @{Number = 7005224511; Class = &quot;Bronze&quot;; Reason = &quot;double&quot;},
    @{Number = 7005242390; Class = &quot;Bronze&quot;; Reason = &quot;endsIn0&quot;},
    @{Number = 7002398201; Class = &quot;Ordinary&quot;; Reason = &quot;&quot;}
)

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In this case there are a number of different kinds of number patterns that will trigger the different classes so I have included an example of each.&lt;/p&gt;

&lt;p&gt;Next I want to make sure that if I pass a number on the command line, it returns the correct class.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
Context &quot;When numbers are passed by the command line&quot; {
    It &quot;identifies &amp;lt;Number&amp;gt; as &amp;lt;Class&amp;gt;&quot; -TestCases $TestCases {
        param($Number, $Class, $Reason)

        $c = &amp;amp; $cmd -Number $Number
        $c | Should Be $Class
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Since there’s also a flag that will cause the script to return extra details, I want to make sure I test that as well.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
Context &quot;When numbers are passed by the command line with Details&quot; {
    It &quot;identifies &amp;lt;Number&amp;gt; as &amp;lt;Class&amp;gt; because of &amp;lt;Reason&amp;gt;&quot; -TestCases $TestCases {
        param($Number, $Class, $Reason)

        $c = &amp;amp; $cmd -Number $Number -Details
        $c.Number | Should Be $Number
        $c.Class | Should Be $Class
        $c.Reason | Should Be $Reason
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Lastly, since my script can also takes input from the pipeline instead of the command line, I want to test that scenario too.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
Context &quot;When numbers are passed by the pipeline&quot; {
    It &quot;identifies &amp;lt;Number&amp;gt; as &amp;lt;Class&amp;gt; because of &amp;lt;Reason&amp;gt;&quot; -TestCases $TestCases {
        param($Number, $Class, $Reason)

        $c = $Number | &amp;amp; $cmd
        $c.Number | Should Be $Number
        $c.Class | Should Be $Class
        $c.Reason | Should Be $Reason
    }

    It &quot;identifies all numbers in the pipeline&quot; {
        $results = $TestCases | %{ $_.Number } | &amp;amp; $cmd
        foreach($t in $TestCases) {
            $r = $results | ? Number -eq $t.Number
            #$r | Should Exist
            $r.Number | Should Be $t.Number
            $r.Class  | Should Be $t.Class
            $r.Reason | Should Be $t.Reason
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;running-your-tests&quot;&gt;Running Your Tests&lt;/h2&gt;

&lt;p&gt;Now that we have a test file created we need to run it. If you’re on Windows 10 then you can just run &lt;em&gt;Invoke-Pester&lt;/em&gt; from the folder that has your tests file in it. If you’re not on Windows 10 then you’ll need to install Pester. Pester is a PowerShell modules so the easiest way is to download the latest release (&lt;a href=&quot;https://github.com/pester/Pester/archive/3.3.8.zip&quot;&gt;3.3.8&lt;/a&gt;), unzip it and run:&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;Import-Module c:\path\to\pester-3.3.8\pester.psd1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or you can also easily install it using &lt;a href=&quot;https://chocolatey.org/&quot;&gt;Chocolatey&lt;/a&gt; since there is a &lt;a href=&quot;https://chocolatey.org/packages/pester&quot;&gt;Pester package&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once that’s done you’ll be able to call Invoke-Pester. When you do, you should see something like the following:&lt;/p&gt;

&lt;pre&gt;
PS C:\path\to\tests&amp;gt; Invoke-Pester
Describing Get-PhoneNumberClass
   Context When numbers are passed by the command line
    [+] identifies 7000011222 as Gold 82ms
    [+] identifies 7000033440 as Gold 22ms
    [+] identifies 7000005555 as Gold 20ms
    [+] identifies 7000006660 as Gold 19ms
    [+] identifies 7000007890 as Gold 19ms
    [+] identifies 7005242110 as Silver 21ms
    [+] identifies 7005242007 as Silver 24ms
    [+] identifies 7005223434 as Silver 21ms
    [+] identifies 7005224511 as Bronze 22ms
    [+] identifies 7005242390 as Bronze 23ms
    [+] identifies 7002398201 as Ordinary 24ms
   Context When numbers are passed by the command line with Details
    [+] identifies 7000011222 as Gold because of doubleTriple 63ms
    [+] identifies 7000033440 as Gold because of doubleDouble0 22ms
    [+] identifies 7000005555 as Gold because of same4 22ms
    [+] identifies 7000006660 as Gold because of triple0 23ms
    [+] identifies 7000007890 as Gold because of sequential4 23ms
    [+] identifies 7005242110 as Silver because of double0 22ms
    [+] identifies 7005242007 as Silver because of bond 21ms
    [+] identifies 7005223434 as Silver because of twoDigitPattern 22ms
    [+] identifies 7005224511 as Bronze because of double 23ms
    [+] identifies 7005242390 as Bronze because of endsIn0 22ms
    [+] identifies 7002398201 as Ordinary because of  22ms
   Context When numbers are passed by the pipeline
    [+] identifies 7000011222 as Gold because of doubleTriple 65ms
    [+] identifies 7000033440 as Gold because of doubleDouble0 271ms
    [+] identifies 7000005555 as Gold because of same4 26ms
    [+] identifies 7000006660 as Gold because of triple0 22ms
    [+] identifies 7000007890 as Gold because of sequential4 22ms
    [+] identifies 7005242110 as Silver because of double0 22ms
    [+] identifies 7005242007 as Silver because of bond 26ms
    [+] identifies 7005223434 as Silver because of twoDigitPattern 25ms
    [+] identifies 7005224511 as Bronze because of double 27ms
    [+] identifies 7005242390 as Bronze because of endsIn0 26ms
    [+] identifies 7002398201 as Ordinary because of  29ms
    [+] identifies all numbers in the pipeline 125ms
Tests completed in 2.42s
Passed: 34 Failed: 0 Skipped: 0 Pending: 0
&lt;/pre&gt;

&lt;p&gt;Now, when you make changes to your scripts in the future, you’ll have confidence that you haven’t broken something that was working.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Classifying Phone Numbers</title>
      <link>http://paul.vaillant.ca/2015/05/11/classifying-phone-numbers.html</link>
      <pubDate>Mon, 11 May 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/05/11/classifying-phone-numbers.html</guid>
      <description>&lt;p&gt;What an exiting week last week. I was in Chicago for &lt;a href=&quot;http://ignite.microsoft.com&quot;&gt;Microsoft Ignite 2015&lt;/a&gt;. I had the opportunity to attend several very interesting sessions as well as walk the expo floor and meet with other great people in the PowerShell and Skype for Business community.&lt;/p&gt;

&lt;p&gt;You might also notice from the tags above that I’m going to start using the &lt;a href=&quot;/tag/SkypeForBusiness.html&quot;&gt;SkypeForBusiness&lt;/a&gt; instead of the &lt;a href=&quot;/tag/Lync.html&quot;&gt;Lync&lt;/a&gt; tag. Both tag pages cross link to each other so in case you end up in one, you’ll have a friendly reminder to check the other as well.&lt;/p&gt;

&lt;h2 id=&quot;the-inspiration&quot;&gt;The Inspiration&lt;/h2&gt;

&lt;p&gt;One of the sessions I attended was called &lt;a href=&quot;https://channel9.msdn.com/Events/Ignite/2015/BRK4112&quot;&gt;“Save Time! Automate Phone Number Management in Skype for Business”&lt;/a&gt; by &lt;a href=&quot;https://twitter.com/StaleHansen&quot;&gt;@StaleHansen&lt;/a&gt;. It was a great talk and I encourage you to check it as since it talks about similar topics that I’ve spoken about and it mentions both my &lt;a href=&quot;/2015/03/18/managing-your-lync-phone-numbers.html&quot;&gt;Managing Your Lync Phone Numbers&lt;/a&gt; and &lt;a href=&quot;/2015/04/10/keeping-lync-unassigned-numbers-updated.html&quot;&gt;Keeping Lync Unassigned Numbers Updated&lt;/a&gt; posts.&lt;/p&gt;

&lt;p&gt;In this talk, he spoke about classifying numbers to keep &lt;em&gt;nice&lt;/em&gt; numbers for certain use cases (IVRs, main numbers, senior executives, etc). There was an excellent slide that had rules he proposed:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/phone-number-classes.jpg&quot; alt=&quot;Phone Number Classes&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In speak with him afterwards he mentioned that he hadn’t had time yet to implement the classification using regular expressions and I offered to help.&lt;/p&gt;

&lt;h2 id=&quot;the-regular-expressions&quot;&gt;The Regular Expressions&lt;/h2&gt;

&lt;p&gt;I’ve created the following script to take a number and return a classification of Gold, Silver, Bronze or Ordinary based on his rules plus one more of my own (Silver numbers can have repeated patterns, like ending in 1212). It uses the following regular expressions to classify the numbers&lt;/p&gt;

&lt;table class=&quot;table table-hover table-stripped table-condensed&quot;&gt;
  &lt;thead&gt;&lt;tr&gt;&lt;th&gt;Class&lt;/th&gt;&lt;th&gt;Reason&lt;/th&gt;&lt;th&gt;RegEx&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;Gold&lt;/td&gt;&lt;td&gt;Double Triple&lt;/td&gt;&lt;td&gt;(\d)\1(\d)\2{2}$&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Gold&lt;/td&gt;&lt;td&gt;Double Double 0&lt;/td&gt;&lt;td&gt;(\d)\1(\d)\2{1}0$&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Gold&lt;/td&gt;&lt;td&gt;Triple 0&lt;/td&gt;&lt;td&gt;(\d)\1{2}0$&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Gold&lt;/td&gt;&lt;td&gt;Same 4&lt;/td&gt;&lt;td&gt;(\d)\1{3}$&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Gold&lt;/td&gt;&lt;td&gt;Sequential 4&lt;/td&gt;&lt;td&gt;(?:0(?=1)|1(?=2)|2(?=3)|3(?=4)|4(?=5)|5(?=6)|6(?=7)|7(?=8)|8(?=9)|9(?=0)){3}\d$&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Silver&lt;/td&gt;&lt;td&gt;Double 0&lt;/td&gt;&lt;td&gt;(\d)\1{1}0$&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Silver&lt;/td&gt;&lt;td&gt;James Bond&lt;/td&gt;&lt;td&gt;007$&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Silver&lt;/td&gt;&lt;td&gt;Two Digit Pattern&lt;/td&gt;&lt;td&gt;(\d{2})\1$&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Bronze&lt;/td&gt;&lt;td&gt;Double&lt;/td&gt;&lt;td&gt;(\d)\1$&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Bronze&lt;/td&gt;&lt;td&gt;Ends in 0&lt;/td&gt;&lt;td&gt;0$&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Anything else is classified as &lt;em&gt;Ordinary&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A couple quick notes on the regular expressions:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;$ matches the end of the line&lt;/li&gt;
  &lt;li&gt;\d matches any number&lt;/li&gt;
  &lt;li&gt;* means match the preceding item 0 or more times&lt;/li&gt;
  &lt;li&gt;? means match the preceding item 0 or 1 times&lt;/li&gt;
  &lt;li&gt;
    &lt;ul&gt;
      &lt;li&gt;means match the preceding item 1 or more times&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;{#} means match the preceding item exactly # times&lt;/li&gt;
  &lt;li&gt;any statement within () creates a capture group with some exceptions
    &lt;ul&gt;
      &lt;li&gt;if the capture group starts with ?: that means don’t capture&lt;/li&gt;
      &lt;li&gt;(?x) isn’t a capture group but says the regular expression should ignore comments and unescaped whitespace; this makes large complex expressions more readable&lt;/li&gt;
      &lt;li&gt;(?=…) means match as long as the next item matches the group …, but it doesn’t capture this next item&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;table&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;groups can have multiple options separated by&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
  &lt;li&gt;\1 (or any \#, etc) matches the 1st (or #th) capture group&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may have noticed that I used {1} in some of the regular expressions. You don’t normally need to do this (the default is to match one), but I do this when I want to match 0 afterwards since I need to break up the numbers, otherwise if I wrote (\d)\10 (for example) it would want to match a digit followed by the value of the 10th capture group. This could be written other ways, like (\d)\1[0] or (\d)\1(?:0) or even (\d)\1 0 (if you’re using (?x)), all of which are identical.&lt;/p&gt;

&lt;h2 id=&quot;the-complicated-one&quot;&gt;The Complicated One&lt;/h2&gt;

&lt;p&gt;The Sequential 4 is probably the most complex so I’ll take a moment to describe it.&lt;/p&gt;

&lt;pre&gt;(?:
    0(?=1)| # match a 0 as long as a 1 comes after
    1(?=2)| # match a 1 as long as a 2 comes after
    2(?=3)| # match a 2 as long as a 3 comes after
    3(?=4)| # match a 3 as long as a 4 comes after
    4(?=5)| # match a 4 as long as a 5 comes after
    5(?=6)| # match a 5 as long as a 6 comes after
    6(?=7)| # match a 6 as long as a 7 comes after
    7(?=8)| # match a 7 as long as a 8 comes after
    8(?=9)| # match a 8 as long as a 9 comes after
    9(?=0)  # match a 9 as long as a 0 comes after
){3} # do that 3 times
\d   # and also capture the last digit&lt;/pre&gt;

&lt;p&gt;So the only way for it to be able to capture a 2 (for example) is if a 3 comes next. If that’s the case then it would also match the 3, but only if a 4 came next, and so on. It has to do this 3 times, which means there have to be 4 sequential numbers. Because the &lt;em&gt;look forward&lt;/em&gt; ?= doesn’t capture that digit it ensured was there, I’ve included a \d at the end to capture it.&lt;/p&gt;

&lt;h2 id=&quot;slow-and-fast&quot;&gt;Slow and Fast&lt;/h2&gt;

&lt;p&gt;Some of these regular expressions are a little complex so I’ve implemented it two ways so that people can see different ways it could be done.&lt;/p&gt;

&lt;p&gt;The first way is using an array simple regular expressions that are tested one after another.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$CLASSES = @{
    Gold = @{
        doubleTriple = &quot;(\d)\1(\d)\2{2}$&quot;;
        doubleDouble0 = &quot;(\d)\1(\d)\2{1}0$&quot;;
        triple0 = &quot;(\d)\1{2}0$&quot;;
        same4 = &quot;(\d)\1{3}$&quot;;
        sequential4 = &quot;(?:0(?=1)|1(?=2)|2(?=3)|3(?=4)|4(?=5)|5(?=6)|6(?=7)|7(?=8)|8(?=9)|9(?=0)){3}\d$&quot;
    };
    Silver = @{
        double0 = &quot;(\d)\1{1}0$&quot;;
        bond = &quot;007$&quot;;
        twoDigitPattern = &quot;(\d{2})\1$&quot;
    };
    Bronze = @{
        double = &quot;(\d)\1$&quot;;
        endsIn0 = &quot;0$&quot;
    }
}

function ClassifySlow($number) {
    Write-Verbose &quot;Classifying $number (slow)&quot;
    $class = &quot;Ordinary&quot;
    $reason = &quot;&quot;
    foreach($c in @(&quot;Gold&quot;,&quot;Silver&quot;,&quot;Bronze&quot;)) {
        $CLASSES[$c].GetEnumerator() | foreach {
            if($number -match $_.Value) {
                $class = $c
                $reason = $_.Key
                break
            }
        }
    }
    @{Number = $number; Class = $class; Reason = $reason}
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The second is by combining all of the regular expressions together and allowing the regular expression engine to optimize the parsing.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$CLASS_RE = &quot;(?x)
(?:
    (?&amp;lt;Gold_doubleTriple&amp;gt;(\d)\1(\d)\2{2})
    |
    (?&amp;lt;Gold_doubleDouble0&amp;gt;(\d)\3(\d)\4 0)
    |
    (?&amp;lt;Gold_triple0&amp;gt;(\d)\5{2}0)
    |
    (?&amp;lt;Gold_same4&amp;gt;(\d)\6{3})
    |
    (?&amp;lt;Gold_sequential4&amp;gt;(?:0(?=1)|1(?=2)|2(?=3)|3(?=4)|4(?=5)|5(?=6)|6(?=7)|7(?=8)|8(?=9)|9(?=0)){3}\d)
    |
    (?&amp;lt;Silver_double0&amp;gt;(\d)\7 0)
    |
    (?&amp;lt;Silver_bond&amp;gt;007)
    |
    (?&amp;lt;Silver_twoDigitPattern&amp;gt;(\d{2})\8)
    |
    (?&amp;lt;Bronze_double&amp;gt;(\d)\9)
    |
    (?&amp;lt;Bronze_endsIn0&amp;gt;0)
)$&quot;

function ClassifyFast($number) {
    Write-Verbose &quot;Classifying $number (fast)&quot;
    $class = &quot;Ordinary&quot;
    $reason = &quot;&quot;
    if($number -match $CLASS_RE) {
        $class,$reason = $($matches.Keys | ? { $_ -notmatch &quot;^[0-9]+$&quot; }) -split '_'
    }
    @{Number = $number; Class = $class; Reason = $reason}
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Since these two alternate methods are driven by performance, I’ve included a -Test flag you can use to experience the differences between the two. In test runs on my computer, the normal &lt;em&gt;fast&lt;/em&gt; option using the combined all-in-one regex runs about 3x faster than the &lt;em&gt;slow&lt;/em&gt; option. There are some additional optimizations that make it possible for the &lt;em&gt;slow&lt;/em&gt; option to run similar to the &lt;em&gt;fast&lt;/em&gt; option but the it requires unwinding the nicely maintainable hash into arrays and using 53 lines instead of 10. Actually the code difference is event larger when you take into consideration that the all-in-one regular expression really is just 2 core lines of if(-match) and finding and splitting out the class and reason from the group capture name.&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Get-PhoneNumberClass.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Get-PhoneNumberClass.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Coming up next is testing scripts with &lt;a href=&quot;https://github.com/pester/Pester&quot;&gt;Pester&lt;/a&gt;. If you want a sneak peak, checkout the test script for this script here: &lt;a href=&quot;/content/Get-PhoneNumberClass.Tests.ps1&quot;&gt;Get-PhoneNumberClass.Tests.ps1&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Document Your PowerShell Scripts</title>
      <link>http://paul.vaillant.ca/2015/05/01/document-your-powershell-scripts.html</link>
      <pubDate>Fri, 01 May 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/05/01/document-your-powershell-scripts.html</guid>
      <description>&lt;p&gt;I was inspired by old war posters with this title. Can’t you picture the classic Uncle Sam with his finger out and a caption that reads “I want YOU to document your scripts!”? Or maybe you could twist it a different way and it would be motivational poster with the kitten running thought the fields captioned with “every time you release a script without documentation, a kitten dies” (it’s a thing they tell me).&lt;/p&gt;

&lt;p&gt;Today we’re talking about documenting PowerShell scripts. I love that you can embed the documentation directly into the script and that Get-Help formats it all for you and makes it nice.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Quick tip #1: you can access the help by adding -? to the end of any cmdlet. EG Get-Command -?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;Quick tip #2: if you’re coming from the Unix world, fear not. Microsoft has created an alias ‘man’ for Get-Help so everything is as it should be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The inline documentation takes the form of a comment block (I like to use &amp;lt;# and #&amp;gt;, but you can just comment every line starting with # if you want) at the start of the file. Then within that block you can specify different pieces of information including:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;.SYNOPSIS&lt;/strong&gt; – a brief explanation of what the script or function does&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;.DESCRIPTION&lt;/strong&gt; – a more detailed explanation of what the script or function does&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;.PARAMETER &amp;lt;name&amp;gt;&lt;/strong&gt; – an explanation of a specific parameter. You should have have one of these sections for each parameter the script or function uses.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;.EXAMPLE&lt;/strong&gt; – an example of how to use the script or function. You can have multiple .EXAMPLE sections if you want to provide more than one example.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;.INPUTS&lt;/strong&gt; - Description and types of objects that can be piped to the function or script&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;.OUTPUTS&lt;/strong&gt; - Description and type of the objects that the cmdlet returns&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;.NOTES&lt;/strong&gt; – any miscellaneous notes on using the script&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;.LINK&lt;/strong&gt; – If you include a URL beginning with http:// or https://, it will be opened automatically when Get-Help is called with –online&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can get more details in &lt;a href=&quot;https://technet.microsoft.com/en-us/library/hh847834.aspx&quot;&gt;about Comment Based Help&lt;/a&gt; (or by running &lt;em&gt;Get-Help about_Comment_Based_Help&lt;/em&gt;). There’s also another good reference on MSDN &lt;a href=&quot;https://msdn.microsoft.com/en-us/library/aa965353%28VS.85%29.aspx&quot;&gt;How to Write Cmdlet Help&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So this is to say that I’m doing my part to save the kittens. All the scripts on my site now include documentation. You can check it out in my new &lt;a href=&quot;/help/index.html&quot;&gt;Get-Help&lt;/a&gt; section. I also created a tool, well a PowerShell script, called &lt;a href=&quot;/help/ConvertTo-HelpMarkdown.html&quot;&gt;ConvertTo-HelpMarkdown&lt;/a&gt; that will read out the inline documentation and create a markdown page that, as in my case, can be converted to HTML. You can download it from my &lt;a href=&quot;/scripts.html&quot;&gt;scripts&lt;/a&gt; page if you’d like.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Installing LRS Admin The Easy Way</title>
      <link>http://paul.vaillant.ca/2015/04/24/installing-lrs-admin-the-easy-way.html</link>
      <pubDate>Fri, 24 Apr 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/04/24/installing-lrs-admin-the-easy-way.html</guid>
      <description>&lt;p&gt;Maybe that should be &lt;em&gt;The Easy Way ™&lt;/em&gt;? Oh, it’s probably too generic to trademark anyways. Besides, by now I’m sure you know what my easy way is don’t you? If you guessed anything other than PowerShell then I’m sad and you should spend more time reading my other blog posts.&lt;/p&gt;

&lt;p&gt;I really love &lt;a href=&quot;http://catalog.lync.com/en-us/hardware/lync-room-systems/index.aspx#/locale=en-us&amp;amp;categoryid=3&amp;amp;sortby=3&amp;amp;subcategoryid=&amp;amp;filter=&amp;amp;manufacture=&amp;amp;version=&amp;amp;isQualified=&amp;amp;region=&amp;amp;language=&amp;amp;page=1&amp;amp;apptype=&amp;amp;tags=&quot;&gt;Lync Room Systems&lt;/a&gt;. Not the blind-for-no-reason kind of love, the customers-see-and-experience-ROI-so-easily kind of love which is also the first-hand-makes-my-life-easier-and-better kind of love if you have ever used an LRS. “Start meetings faster” seems like such a simple value proposition but it’s so powerful. Anyway, love of LRS aside, if you have any LRS then you should also be using the &lt;a href=&quot;http://www.microsoft.com/en-us/download/details.aspx?id=40329&quot;&gt;LRS admin tool&lt;/a&gt;. Unfortunately this isn’t built into Lync but there are some fairly details &lt;a href=&quot;https://technet.microsoft.com/en-us/library/dn436324.aspx&quot;&gt;deployment instructions&lt;/a&gt; if you’re into that whole &lt;a href=&quot;http://en.wikipedia.org/wiki/RTFM&quot;&gt;RTFM&lt;/a&gt;. Puff… why read a manual on how to deploy something when someone else has written a script to do all that! Well that’s what I’ve done for you.&lt;/p&gt;

&lt;p&gt;I’ll post the link for this script right here in case people don’t want to go any further.&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Install-LrsAdmin.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Install-LrsAdmin.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For those of you who are interested, thanks for continuing to read &lt;i class=&quot;fa fa-smile-o&quot;&gt;&lt;/i&gt;.&lt;/p&gt;

&lt;p&gt;There are a number of parameters that you can specify but they all have defaults.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
param(
	[Parameter()][string]$LrsAppUser = &quot;LRSApp&quot;,
	[Parameter()][string]$LrsAppUserOU = &quot;CN=Users&quot;,
	[Parameter()][string]$LrsSupportAdminGroup = &quot;LRSSupportAdminGroup&quot;,
	[Parameter()][string]$LrsSupportAdminGroupOU = &quot;CN=Users&quot;,
	[Parameter()][string]$LrsFullAccessAdminGroup = &quot;LRSFullAccessAdminGroup&quot;,
	[Parameter()][string]$LrsFullAccessAdminGroupOU = &quot;CN=Users&quot;,
	[Parameter()][string]$PoolName,
	[Parameter()][string]$SipDomain
)

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For &lt;em&gt;$PoolName&lt;/em&gt; and &lt;em&gt;$SipDomain&lt;/em&gt;, if they aren’t specified the script tries to auto-detect a value.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
if(!$PoolName) {
	Write-Verbose &quot;Auto-detecting Lync Pool...&quot;
	[array]$registrars = Get-CsService -Registrar -Verbose:$false
	if($registrars.count -eq 1) {
		$PoolName = $registrars[0].PoolFQDN
		Write-Verbose &quot;    found $PoolName&quot;
	} else {
		$pools = $($registrars | %{ $_.PoolFQDN }) -join &quot; &quot;
		Write-Error &quot;Failed to auto-detect registrar pool; please specify LyncRegistrarPool and try again [$pools]&quot;
		exit
	}
}

if(!$SipDomain) {
	Write-Verbose &quot;Auto-detecting SIP Domain...&quot;
	$sipDomains = Get-CsSipDomain -Verbose:$false
	if(!$sipDomains -or $sipDomains -is [array]) {
		Write-Error &quot;Failed to auto-detect proper SIP domain&quot;
		exit
	} else {
		$SipDomain = $sipDomains.Name
		Write-Verbose &quot;    found $SipDomain&quot;
	}
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we check that the script is being run on a Lync Front End with at least CU2.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$fe = Get-WmiObject -query 'select * from win32_product' | where {$_.name -like &quot;Microsoft Lync Server 2013, Front End Server&quot;}
if(!$fe) {
	Write-Error &quot;This machine is not a Lync Front End&quot;
	exit
}
$feVer = $fe.Version
[int]$feVerBuild = $feVer -split '\.' | select -last 1
if(!$feVer.StartsWith(&quot;5.0.8308.&quot;) -or $feVerBuild -lt 557) {
	Write-Error &quot;Lync Front End is not minimum Lync 2013 CU2 (July/2013)&quot;
	exit
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first step is to create a SIP enabled user for the app.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$appUser = Get-ADUser -filter {sAMAccountName -eq $LrsAppUser} -Verbose:$false # work around not supporting -ErrorAction SilentlyContinue
if($appUser) {
	Write-Warning &quot;LRS App User $LrsAppUser already exists&quot;
} else {
	$appUserOu = ToAbsoluteLdap $LrsAppUserOU
	$appUser = New-AdUser $LrsAppUser -GivenName LRS -Surname User -DisplayName $LrsAppUser -SamAccountName $LrsAppUser -Path $appUserOu -Server $dc -PassThru 
}
if(!$appUser) {
	Write-Error &quot;Failed to create AD user $LrsAppUser&quot;
	exit
}
$csUser = Get-CsUser $appUser.DistinguishedName -Verbose:$false -ErrorAction SilentlyContinue
if($csUser) {
	if($csUser.Enabled) {
		Write-Warning &quot;LRS App User $LrsAppUser already enabled for Lync&quot;
	} else {
		Write-Warning &quot;LRS App User $LrsAppUser exists but is disabled for Lync&quot;
	}
} else {
	$csUser = Enable-CsUser $appUser.DistinguishedName -RegistrarPool $PoolName -SipAddress &quot;sip:$LrsAppUser@$SipDomain&quot; -DomainController $dc -PassThru
}
if(!$csUser) {
	Write-Error &quot;Failed to SIP enable LRS App User $LrsAppUser&quot;
	exit
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The second step is to create the global security groups in AD.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$supportAdminsGrp = Get-ADGroup -filter {sAMAccountName -eq $LrsSupportAdminGroup} -Verbose:$false # because no -ErrorAction SilentlyContinue
if($supportAdminsGrp) {
	Write-Warning &quot;LRS Support Admin Group $LrsSupportAdminGroup already exists&quot;
} else {
	$supportAdminsGrpOu = ToAbsoluteLdap $LrsSupportAdminGroupOU
	$supportAdminsGrp = New-ADGroup $LrsSupportAdminGroup -GroupScope Global -GroupCategory Security -SamAccountName $LrsSupportAdminGroup -Path $supportAdminsGrpOu -Server $dc -PassThru
}
if(!$supportAdminsGrp) {
	Write-Error &quot;Failed to create AD group $LrsSupportAdminGroup&quot;
	exit
}

$fullAdminsGrp = Get-ADGroup -filter {sAMAccountName -eq $LrsFullAccessAdminGroup} -Verbose:$false # because no -ErrorAction SilentlyContinue
if($fullAdminsGrp) {
	Write-Warning &quot;LRS Full Access Admin Group $LrsFullAccessAdminGroup already exists&quot;
} else {
	$fullAdminsGrpOu = ToAbsoluteLdap $LrsFullAccessAdminGroupOU
	$fullAdminsGrp = New-ADGroup $LrsFullAccessAdminGroup -GroupScope Global -GroupCategory Security -SamAccountName $LrsFullAccessAdminGroup -Path $fullAdminsGrpOu -Server $dc -PassThru
}
if(!$fullAdminsGrp) {
	Write-Error &quot;Failed to create AD group $LrsFullAccessAdminGroup&quot;
	exit
}

$supportAdminsGrpMbrs = $supportAdminsGrp | Get-AdGroupMember | %{ $_.distinguishedName }
if(-not $($supportAdminsGrpMbrs -contains $fullAdminsGrp.distinguishedName)) {
	Write-Verbose &quot;Adding LRS Full Access Admin Group as member of LRS Support Admin Group&quot;
	Add-ADGroupMember $supportAdminsGrp.distinguishedName $fullAdminsGrp.distinguishedName -Server $dc
} else {
	Write-Warning &quot;LRS Full Access Admin Group is already a member of LRS Support Admin Group&quot;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The third step is to install ASP.NET MVC 4.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$aspMvc4 = Get-WmiObject -Class Win32_Product | where {$_.Name -eq 'Microsoft ASP.NET MVC 4 Runtime'}
if($aspMvc4) {
	Write-Warning &quot;ASP.NET MVC 4 Runtime is already installed&quot;
} else {
	$url = &quot;http://download.microsoft.com/download/2/F/6/2F63CCD8-9288-4CC8-B58C-81D109F8F5A3/AspNetMVC4Setup.exe&quot;
	DownloadAndInstall $url
	$aspMvc4 = Get-WmiObject -Class Win32_Product | where {$_.Name -eq 'Microsoft ASP.NET MVC 4 Runtime'}
}
if(!$aspMvc4) {
	Write-Error &quot;Failed to install ASP.NET MVC 4 Runtime&quot;
	exit
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;NOTE&lt;/em&gt;: the snippet above refers to a function &lt;em&gt;DownloadAndInstall&lt;/em&gt; that is defined in the script file you can download but that I haven’t shown in the post. Download the script and use that if you want to copy and paste step by step.&lt;/p&gt;

&lt;p&gt;The fourth step is to set the application ports.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
Set-CsWebServer -Identity $PoolName `
		-MeetingRoomAdminPortalInternalListeningPort 4456 `
		-MeetingRoomAdminPortalExternalListeningPort 4457

$webSrv = Get-CsService -WebServer -PoolFqdn $PoolName
if($webSrv.MeetingRoomAdminPortalInternalListeningPort -ne 4456) {
	Write-Error &quot;Failed to set MeetingRoomAdminPortalInternalListeningPort&quot;
	exit
}
if($webSrv.MeetingRoomAdminPortalExternalListeningPort -ne 4457) {
	Write-Error &quot;Failed to set MeetingRoomAdminPortalExternalListeningPort&quot;
	exit
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The fifth step is to actually download and install the LRS Admin app.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$url = &quot;http://download.microsoft.com/download/8/7/8/878DA290-F608-4297-B1C7-4A5FC8245EA3/LyncRoomAdminPortal.exe&quot;
DownloadAndInstall $url
if(-not $(Test-Path $installPath)) {
	Write-Error &quot;Failed to install Lync Room Admin Portal&quot;
	exit
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then finally the last step is to write the web.config file for the app.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$webConfig = &quot;$installPath\web.config&quot;
$cfgOrig = '&lt;add key=&quot;PortalUserName&quot; value=&quot;sip:LRSApp@microsoft.com&quot; /&gt;'
$cfgNew = '&amp;lt;add key=&quot;PortalUserName&quot; value=&quot;sip:' + $LrsAppUser + &quot;@&quot; + $SipDomain + '&quot; /&amp;gt;' + &quot;`n&quot; +
	'    &lt;add key=&quot;PortalUserRegistrarFQDN&quot; value=&quot;' + $PoolName + '&quot; /&gt;' + &quot;`n&quot; +
	'    &lt;add key=&quot;PortalUserRegistrarPort&quot; value=&quot;5061&quot; /&gt;'
$(gc $webConfig) -replace $cfgOrig,$cfgNew | Out-File $webConfig -Encoding ASCII

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now wasn’t that easier then doing it all by hand?&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Managing extensions in Lync dial plans</title>
      <link>http://paul.vaillant.ca/2015/04/17/managing-extensions-in-lync-dial-plans.html</link>
      <pubDate>Fri, 17 Apr 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/04/17/managing-extensions-in-lync-dial-plans.html</guid>
      <description>&lt;p&gt;The title of this article should probably include the works &lt;em&gt;“without having to tear your hair out”&lt;/em&gt;. Extensions are one of those things that are synonymous with PBX for a lot of people. And extensions are something that also can be a point of frustration with Lync, not only because of how they are managed, but also with the duplication that can occur given that extensions have to be separately entered into Lync and Exchange. Well here’s some PowerShell to make your day better if you deal with extensions.&lt;/p&gt;

&lt;p&gt;First some context: this script was developed while working with a customer who wanted to have both DIDs as well as maintain legacy extensions. It was developed to make the extensions more manageable since the same effort could have been achieved manually at the cost of less hair at the end of the day or whenever a change was required (add extension, change extension, etc).&lt;/p&gt;

&lt;p&gt;Second, a little segway first into &lt;a href=&quot;http://www.itu.int/rec/T-REC-E.164-201011-I/en&quot;&gt;E.164&lt;/a&gt; and &lt;a href=&quot;https://tools.ietf.org/html/rfc3966&quot;&gt;RFC3966&lt;/a&gt;. This are the relevant standards for the tel: URI used in Lync for the LineURI value assigned to a user. I like standards and for DIDs that means tel:+17005551212 all the time. In this case what we’re adding is the extension for each user to the LineURI itself in the form of tel:+17005551212;ext=1234. If you’re interested in E.164 and Lync I’d suggest also reading a nice post by Ken Lasko on the subject of how to format service numbers (&lt;a href=&quot;http://ucken.blogspot.ca/2015/01/service-number-formatting-in-lync.html&quot;&gt;Service Number Formatting in Lync&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Now to the script! First we grab all the users.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;$users = Get-CsUser -Filter {EnterpriseVoiceEnabled -eq $true}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we make sure that the same extension hasn’t been assigned more than once (it happens, easily, hence the check).&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$assignedDups = new-object 'system.collections.generic.dictionary[int,system.collections.generic.list[string]]'
$assignedExts = new-object 'system.collections.generic.dictionary[int,string]'
# for every user who has an extension assigned
$users | where LineURI -match ';ext=' | foreach {
	$sip = $_.SipAddress
	$line = $_.LineURI.Substring(4) # remove tel:
	[int]$ext = $line -split ';ext=' | select -last 1
	# check if we've seen this extension already
	if($assignedDups.ContainsKey($ext)) {
		# seen this before multiple times
		$assignedDups[$ext].Add($line)
	}
	else if($assignedExts.ContainsKey($ext)) {
		# this is the second time we've seen this extension
		# create a list of duplicates; the first one and this one
		$dups = new-object 'system.collections.generic.list[string]' @($assignedExts[$ext],$line)
		# save the duplicates
		$assignedDups.Add($ext, $dups)
		# and remove this extension from the unique list
		$assignedExts.Remove($ext)
	}
	else {
		# this is the first time we've seen this extension
		$assignedExts.Add($ext, $line)
	}
}

# if we've seen any duplicates
if($assignedDups.Count -gt 0) {
	# for each duplicate
	$assignedDups.GetEnumerator() | foreach {
		# print a warning that shows all the phone numbers assigned this duplicate
		$dups = $_.Value -join ', '
		Write-Warning &quot;Skipping $($_.Key) which has duplicates ($dups)&quot; 
	}
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once we have a list of extensions that aren’t duplicates it’s just a matter of getting all the dial plan normalization rules and checking for any rules that are missing.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$rules = Get-CsVoiceNormalizationRule -Identity $DialPlan | where Name -match $NormalizationRulePrefix
$currentExts = new-object 'system.collections.generic.dictionary[int,string[]]'
$rules | foreach {
	[int]$ext = $_.Pattern.Substring(1, $_.Pattern.Length - 2) # remove leading ^ and trailing $
	$line = $_.Translation
	$identity = $_.Identity
	$currentExts.Add($ext, [string[]]@($line,$identity))
}

$assignedKeys = $assignedExts.Keys
$currentKeys = $currentExts.Keys

# adds = assigned keys - current keys
$newExts = $assignedKeys | where { $currentKeys -notcontains $_ }
Write-Verbose &quot;$($newExts.count) new extensions&quot;

# deletes = current keys - assigned keys
$oldExts = $currentKeys | where { $assignedKeys -notcontains $_ }
Write-Verbose &quot;$($oldExts.count) old extensions&quot;

# updates = existing current keys that don't match assigned keys
$setExts = $assignedKeys | where { $currentKeys -contains $_ -and $assignedExts[$_] -ne $currentKeys[$_][0] }
Write-Verbose &quot;$($setExts.count) updated extensions&quot;

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I do this because I expect this script to be run over and over and it wouldn’t make sense to recreate the dial plan from scratch each time when we can just make the small number of changes that will happen, if any, between runs. The most likely scenario is that we there’s only a small number of adds/changes each time.&lt;/p&gt;

&lt;p&gt;Now that we know the changes we need to make, make them!&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$batch = Get-Date -Format &quot;yyyy-MM-dd HH:mm&quot;

# new-csvoicenormalizationrule w/ date as description
$newExts | foreach {
	New-CsVoiceNormalizationRule -Identity &quot;$DialPlan/$NormalizationRulePrefix $_&quot; -Pattern $('^' + $_ + '$') -Translation $assignedExts[$_] -Description &quot;Created $batch&quot; -Confirm:$false
}

# set-csvoicenormalizationrule w/ date as description
$setExts | foreach {
	$rule = $currentKeys[$_][1]
	Set-CsVoiceNormalizationRule -Identity $rule -Translation $assignedExts[$_] -Description &quot;Updated $batch&quot; -Confirm:$false
}

# remove-csvoicenormalizationrule
$oldExts | foreach {
	$rule = $currentKeys[$_][1]
	Remove-CsVoiceNormalizationRule -Identity $rule -Confirm:$false
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you have the need to manage extensions, download this script today!&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Update-LyncExtensionDialing.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Update-LyncExtensionDialing.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Keeping Lync Unassigned Numbers Updated</title>
      <link>http://paul.vaillant.ca/2015/04/10/keeping-lync-unassigned-numbers-updated.html</link>
      <pubDate>Fri, 10 Apr 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/04/10/keeping-lync-unassigned-numbers-updated.html</guid>
      <description>&lt;p&gt;Here’s a perfect example of something a human should never half to do: maintain a list of unassigned phone numbers. The only problem is where to get an authoritative source of the numbers that are in Lync? Hmmm… we could use an Excel spreadsheet but that just moves the burden of suffering to maintaining the spreadsheet instead of removing it. Instead of doing that by hand, let’s use the uControl API I talked about earlier (see &lt;a href=&quot;/2015/03/18/managing-your-lync-phone-numbers.html&quot;&gt;Managing Your Lync Phone Numbers&lt;/a&gt;) and some PowerShell.&lt;/p&gt;

&lt;p&gt;The first thing we need to do is get a list of all the phone numbers on the SIP trunk. Using the uControl RESTful API:&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$wc = New-Object System.Net.WebClient
$wc.Credentials = $Credential.GetNetworkCredential()
$sipTrunkDidsUrl = &quot;https://api.thinktel.ca/REST.svc/SipTrunks/{0}/Dids?PageFrom=0&amp;amp;PageSize=100000&quot; -f $SipPilotNumber
$didsXml = $wc.DownloadString($sipTrunkDidsUrl)
if(-not $didsXml) {
	Write-Error &quot;Failed to list DIDs on SIP Pilot $SipPilotNumber&quot;
	exit
}
$didsList = [xml]$didsXml
if(-not $didsList -or -not $didsList.ArrayOfTerseNumber) {
	Write-Error &quot;Failed to load or parse DIDs on SIP Pilot $SipPilotNumber&quot;
	exit
}
$didsList.ArrayOfTerseNumber.TerseNumber | %{ $_.Number }

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With those in hand, let’s get all the current unassigned numbers:&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
Get-CsUnassignedNumber | %{
  [long]$start = $_.NumberRangeStart.Substring(2)
  [long]$end = $_.NumberRangeEnd.Substring(2)
  $start..$end
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Lastly, let’d assume that there is already an announcement available for the unassigned numbers, so you can get it with &lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg398937.aspx&quot;&gt;Get-CsAnnouncement&lt;/a&gt;. Or, if you didn’t have an announcement, you could create one as follows:&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
New-CsAnnouncement -identity $(Get-CsService -ApplicationServer | select -first 1).identity `
  -Name &quot;Unassigned&quot; -Language en-us `
  -TextToSpeechPrompt &quot;The number you're trying to reach is unassigned. Please try again&quot;

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With all that information, it’s just a little &lt;a href=&quot;http://en.wikipedia.org/wiki/Set_\(mathematics\)&quot;&gt;set math&lt;/a&gt; to find the entries we need to add and remove.&lt;/p&gt;

&lt;p&gt;Like always, you can download this script&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Update-LyncUnassignedNumbers.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Update-LyncUnassignedNumbers.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep in mind that because unassigned numbers are processed last in the Lync routing, we don’t have to worry about removing an unassigned number if you assign it to a user or contact in Lync. If you really want to, say because you really want to keep your unassigned numbers purely for unassigned numbers, take solace: you are not alone. There is an option in the script above (-Force) that will do just that.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Alternative to Export-CsArchivingData</title>
      <link>http://paul.vaillant.ca/2015/04/02/alternative-to-export-csarchivingdata.html</link>
      <pubDate>Thu, 02 Apr 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/04/02/alternative-to-export-csarchivingdata.html</guid>
      <description>&lt;p&gt;Have you heard about the Lync Archiving role? If not, it’s the compliance and &lt;a href=&quot;http://en.wikipedia.org/wiki/Electronic_discovery&quot;&gt;e-discovery&lt;/a&gt; component of Lync. It’s similar to the &lt;a href=&quot;https://technet.microsoft.com/en-us/library/ff637980.aspx&quot;&gt;litigation hold&lt;/a&gt; functionality available in Exchange. You configure archiving with a number of policy options like whether to notify federated partners of the archiving, retention period, archiving requirement (eg block msgs if they can’t be archived), etc. Refer to the documentation for &lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg413030.aspx&quot;&gt;Set-CsArchivingConfiguration&lt;/a&gt; for a more complete list of the settings.&lt;/p&gt;

&lt;p&gt;It’s also important to understand what archiving stores and what it doesn’t. It stores instant messages, either P2P or in conferences but nothing else. No files transferred, no audio, no video nor screensharing.&lt;/p&gt;

&lt;p&gt;Archiving should also not be confused with conversation history. Conversation history is stored in Exchange and is what users access as their record of the conversation. It can also be deleted by the end user themselves which is not desirable for archiving.&lt;/p&gt;

&lt;p&gt;Once you’ve got it configured and running it just works away in the background, but when you need to access it, then what? This is where &lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg398452.aspx&quot;&gt;Export-CsArchivingData&lt;/a&gt; comes in. You specify a database &amp;amp; output folder plus optionally a start/end date &amp;amp; user URI, and it creates multiple Outlook Express Electronic Mail (EML) file (.EML file extension) in the output folder. That’s ok, but what if you want something different? SQL to the rescue!&lt;/p&gt;

&lt;p&gt;Archiving is stored in the LcsLog database. Sadly there’s no schema documentation available on TechNet but you can use the following query to get messages that a person either sent or got. It works in both Lync 2010 and Lync 2013.&lt;/p&gt;

&lt;pre class=&quot;hljs sql&quot;&gt;&lt;code&gt;
use LcsLog
declare @start datetime = '2015-01-01 00:00:00'
declare @end datetime = '2015-01-31 00:00:00'
declare @useruri nvarchar(max)

select
	m.MessageIdTime Time, f.UserUri FromUser, t.UserUri ToUser, 'p2p' Type, ct.ContentType, m.Body
from Messages m
	join Users f on m.FromId = f.UserId
	join Users t on m.ToId = t.UserId
	join ContentTypes ct on m.ContentTypeId = ct.ContentTypeId
where
	m.MessageIdTime &amp;gt;= @start and m.MessageIdTime &amp;lt; @end
	and (@useruri is null or (f.UserUri = @useruri or t.UserUri = @useruri))
UNION ALL
select
	m.Date Time, f.UserUri FromUser, t.UserUri ToUser, 'conference' Type, ct.ContentType, m.Body
from ConferenceMessages m
	join Users f on m.FromId = f.UserId
	join ConferenceMessageRecipientList rl on rl.MessageId = m.MessageId
	join Users t on rl.UserId = t.UserId
	join ContentTypes ct on m.ContentTypeId = ct.ContentTypeId
where
	m.Date &amp;gt;= @start and m.Date &amp;lt; @end
	and (@useruri is null or (f.UserUri = @useruri or t.UserUri = @useruri))
order by Time

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Substitute your desired date range, bearing in mind that the time is in UTC and optionally specify a @useruri in the form of &lt;em&gt;user@domain.com&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The P2P messages are fairly straight forward but the conference is a little quirky. That’s because of the way conference participants are stored. As such, you need to join the conferences tables with the participants table and the conference messages table to get all the information. This query will cause the query to take a little while to return data if no @useruri is specified.&lt;/p&gt;

&lt;p&gt;Last note, since this is in the LcsLog database, you’ll need to run this as a user in RTCComponentUniversalServices or another group with specific access to the SQL instance or that database.&lt;/p&gt;

&lt;p&gt;You can also use the same PowerShell technique as in &lt;a href=&quot;/2015/02/22/last-login-for-a-sip-uri.html&quot;&gt;Last Login for a SIP URI&lt;/a&gt; to run this via PowerShell.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Find That Troublesome Wi-Fi AP</title>
      <link>http://paul.vaillant.ca/2015/03/27/find-that-troublesome-wifi-ap.html</link>
      <pubDate>Fri, 27 Mar 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/03/27/find-that-troublesome-wifi-ap.html</guid>
      <description>&lt;p&gt;Soft clients are one of the best things about VoIP phone systems and the Lync client is no exception. Being able to have everything on one device gives the solution such portability and mobility. Most of the time this mobility, at least within an office environment, is also made possible because of WiFi.&lt;/p&gt;

&lt;p&gt;Fun fact, when Lync 2010 was released, voice and video over WiFi wasn’t officially support (well &lt;a href=&quot;http://blogs.technet.com/b/nexthop/archive/2012/10/26/lync-wi-fi-deployment-guide-for-real-time-communications-workloads.aspx&quot;&gt;voice/video weren’t validated over Wi-Fi&lt;/a&gt;). I’m sure that didn’t stop people from doing it anyway but its still a fun fact.&lt;/p&gt;

&lt;p&gt;Along came Lync 2013 with support, and in also the Lync SDN API which made it even better. Well does it? The Lync SDN API is great as long as you have either an HP, or an &lt;del&gt;aruba&lt;/del&gt;HP, Wi-Fi network, but what if you have something else? QoEMetrics to the rescue!&lt;/p&gt;

&lt;p&gt;Part of the data that Lync sends to QoE is the &lt;a href=&quot;http://en.wikipedia.org/wiki/BSSID&quot;&gt;BSSID&lt;/a&gt; of the wireless access point that its connected to. You can use this data to group together all the calls that went through each access point in your network. In addition, because of the way the data is stored, you can distinguish inbound and outbound streams. You would think this makes a difference but it does.&lt;/p&gt;

&lt;p&gt;I wrote this query while working with a customer who was getting reports of call problems that seemed overwhelming. We started by grouping the QoE reports by logical site and quickly realized that one site only seemed to be having problems. When we first dug into that site there didn’t seem to be a pattern until finally we grouped by BSSID and the pieces fell into place. Backed with this data that showed two APs as the common factor the problem was quickly resolved.&lt;/p&gt;

&lt;pre class=&quot;hljs sql&quot;&gt;&lt;code&gt;
use QoEMetrics;

declare @endtime datetime = CURRENT_TIMESTAMP;
declare @begintime datetime = DATEADD(day, -7, @endtime);

declare @SitePrefixes table (prefix nvarchar(max) COLLATE Latin1_General_CI_AI, name nvarchar(max) COLLATE Latin1_General_CI_AI);
insert into @SitePrefixes values ('10.10.%','Edmonton'), ('10.20.%','Toronto'), ('10.0.%','Datacenter');

with cdrs as (
	Select
		CallerSubIp.IpAddress CallerSubnet, CalleeSubIp.IpAddress CalleeSubnet, 
		callerBssid.MacAddress CallerBssid, calleeBssid.MacAddress CalleeBssid, 
		a.SenderIsCallerPAI AudioSenderIsCallerPAI,
		a.PacketLossRate*100.0 AudioPktLossRatePct, a.PacketLossRateMax*100.0 AudioPktLossRateMaxPct
	FROM 
		Session s
		inner join MediaLine m on s.ConferenceDateTime = m.ConferenceDateTime and s.SessionSeq = m.SessionSeq
		left join AudioStream a on 
			m.ConferenceDateTime = a.ConferenceDateTime and m.SessionSeq = a.SessionSeq and m.MediaLineLabel = a.MediaLineLabel
		left join dbo.IpAddress CallerSubIp on m.CallerSubnet = CallerSubIp.IpAddressKey
		left join dbo.IpAddress CalleeSubIp on m.CalleeSubnet = CalleeSubIp.IpAddressKey
		left join MacAddress callerBssid on m.CallerBssid = callerBssid.MacAddressKey
		left join MacAddress calleeBssid on m.CalleeBssid = calleeBssid.MacAddressKey
	Where 
		((callerBssid.MacAddress is not null or calleeBssid.MacAddress is Not null)) and 
		s.ConferenceDateTime &amp;gt;= @begintime and s.ConferenceDateTime &amp;lt; @endtime
), subnets as (
	select 
		case 
			when p.name is null then s.Subnet
			else p.name
		end as Site,
		s.Subnet
	from
		(select distinct CallerSubnet Subnet from cdrs
		 UNION
		 select distinct CalleeSubnet Subnet from cdrs) s
		left join @SitePrefixes p on s.Subnet like p.prefix
)

select 
	callerSub.Site CallerSubnet, CallerBssid, calleeSub.Site CalleeSubnet, CalleeBssid, 
	case
		when AudioSenderIsCallerPAI = 1 then 'caller-to-callee'
		else 'callee-to-caller'
	end as Direction, count(*) cnt,
	min(AudioPktLossRatePct) MinPktLossPct, avg(AudioPktLossRatePct) AvgPktLossPct, max(AudioPktLossRatePct) MaxPktLossPct,
	min(AudioPktLossRateMaxPct) MinPktLossMaxPct, avg(AudioPktLossRateMaxPct) AvgPktLossMaxPct, max(AudioPktLossRateMaxPct) MaxPktLossMaxPct
from cdrs
	join subnets callerSub on CallerSubnet = callerSub.Subnet
	join subnets calleeSub on CalleeSubnet = calleeSub.Subnet
where AudioPktLossRatePct is not null
group by callerSub.Site, CallerBssid, calleeSub.Site, CalleeBssid, AudioSenderIsCallerPAI having count(*) &amp;gt; 5
order by avg(AudioPktLossRatePct) desc

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This query returns caller/callee site &amp;amp; access point MAC address, direction of the audio stream (remember there are 2 for each call, caller -&amp;gt; callee and callee -&amp;gt; caller) along with min/average/max packet loss percentages. There’s actually 2 sets of packet loss stats, one is over the average over the whole of the stream and the second is the max of the packet loss samples over the individual streams. The second is important because not all calls are all bad; calls can also have periods of packet loss which result in audio quality problems that end users are just as ready to describe as a ‘bad call’ as a call where the whole call suffers from packet loss.&lt;/p&gt;

&lt;p&gt;This query makes extensive use of common table expressions to keep everything clean. You can adjust the date range if you want something other than the last 7 days but I find any more than this problems don’t stand out and any less you’re likely to miss non-persistent issues. Also add IP patterns for your sites (in the &lt;em&gt;insert into @SitePrefixes&lt;/em&gt; line instead of the 3 sample values I have) and the results at the end will be labeled with your site names instead of subnets. Keep in mind these are SQL patterns so % is the wildcard character.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Create a Report of Assigned Lync User Licensing</title>
      <link>http://paul.vaillant.ca/2015/03/24/create-a-report-of-assigned-lync-user-licensing.html</link>
      <pubDate>Tue, 24 Mar 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/03/24/create-a-report-of-assigned-lync-user-licensing.html</guid>
      <description>&lt;p&gt;I’ve talked about the importance of having data when making decisions before. Decisions isn’t the only thing data is good for. Another great us is checking to make sure things are as you expect them to be. So today we’re going to talk about licensing, specifically Lync user licensing. Lync licensing can be challenging because there is no magic check box in the product that correspond to the difference between a &lt;em&gt;“Standard”&lt;/em&gt; and &lt;em&gt;“Enterprise”&lt;/em&gt; CAL. Those features are governed by the conference policy assigned to the user. To complicate matters, that policy can be global, site or user scoped, so in some cases it may depend on what site the user is logged in at.&lt;/p&gt;

&lt;p&gt;There is a reference from Microsoft available, the &lt;a href=&quot;http://products.office.com/en-us/lync/microsoft-lync-licensing-overview-lync-for-multiple-users&quot;&gt;Lync pricing and licensing guide&lt;/a&gt;, that details the various features of each CAL. You can also consult the &lt;a href=&quot;http://pur.microsoft.com/products.aspx&quot;&gt;Microsoft Product Use Rights Document&lt;/a&gt; for further information.&lt;/p&gt;

&lt;p&gt;Those are big docs, so I’ll summarize the differences between the two here.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;STANDARD CAL&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Presence &amp;amp; P2P IM/audio/video/file transfer (no application sharing or white boarding)&lt;/li&gt;
  &lt;li&gt;Multi-party IM and file transfer (can’t initiate audio or video)&lt;/li&gt;
  &lt;li&gt;Attend conferences as an attendee (not a presenter)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;ENTERPRISE CAL&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ad-hoc multi-party meetings with audio and video, including PSTN dial out&lt;/li&gt;
  &lt;li&gt;P2P application sharing and white boarding&lt;/li&gt;
  &lt;li&gt;Scheduling &amp;amp; inviting attendees to meetings&lt;/li&gt;
  &lt;li&gt;Lync Room Systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve omitted the Plus CAL above for brevity since, unlike the Enterprise CAL, there’s a clear setting &lt;em&gt;“Enterprise Voice Enabled”&lt;/em&gt; on the Lync user object.&lt;/p&gt;

&lt;p&gt;So how do we check for this? We evaluate the conferencing policies in Lync to see if they properly limit the user to only the Standard CAL features.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
$enterpriseConferencingPolicies = Get-CsConferencingPolicy | where {
	!$(!$_.AllowIPAudio -and !$_.AllowIPVideo -and !$_.AllowUserToScheduleMeetingsWithAppSharing -and
		!$_.AllowAnonymousParticipantsInMeetings -and !$_.AllowPolls -and 
		$_.EnableAppDesktopSharing -eq 'None' -and !$_.EnableDialinConferencing)
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This looks for conferencing policies that allow for either audio or video, or scheduled meetings, or any of the other features listed above. Once we have this list, then we can evaluate all users to see if they are assigned on of these policies in order to determine what licensing they should have.&lt;/p&gt;

&lt;pre class=&quot;hljs powershell&quot;&gt;&lt;code&gt;
[array]$enterpriseConfPolicyIds = $enterpriseConferencingPolicies | %{ $_.Identity }
$assignedUserLicenses = Get-CsUser | foreach {
	$confPolicy = $($_ | Get-CsEffectivePolicy).ConferencingPolicy.ToString()
	[pscustomobject]@{
		user = $_; 
		lyncStandardCAL = $true; 
		lyncEnterpriseCAL = $enterpriseConfPolicyIds -contains $confPolicy; 
		lyncPlusCAL = $_.EnterpriseVoiceEnabled -or $_.RemoteCallControlTelephonyEnabled
	}
}

# we can get a nice summary by counting each license type
$sums = $assignedUserLicenses | measure lyncStandardCAL,lyncEnterpriseCAL,lyncPlusCAL
# and extracting it from the measure using group &amp;amp; select
$sums | group Property | select Name,@{n='Count';e={$_.Group[0].Count}}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In addition to the licensing for users, also keep in mind licensing for Lync Room Systems (&lt;a href=&quot;https://technet.microsoft.com/en-us/library/jj205277.aspx&quot;&gt;Get-CsMeetingRoom&lt;/a&gt;) and also for common area phones (&lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg412934.aspx&quot;&gt;Get-CsCommonAreaPhone&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;As an interesting side note, it’s also possible to perform the same check without using the Lync PowerShell cmdlets. You can get the SQL instance that stores the CMS (and the conferencing policies) by querying the configuration naming context of AD for &lt;em&gt;(objectClass=msRTCSIP-GlobalTopologySetting)&lt;/em&gt; and reading the attribute &lt;em&gt;msRTCSIP-BackEndServer&lt;/em&gt;. Then you can connect to the &lt;em&gt;xds&lt;/em&gt; database in this SQL instance query it for:&lt;/p&gt;

&lt;pre class=&quot;hljs sql&quot;&gt;&lt;code&gt;
SELECT Doc.Name,Item.Data FROM [Item] Item join Document Doc on Item.DocId = Doc.DocId where Doc.Name like '%MeetingPolicy%'

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can then deserialize the returned XML and you have exactly the same value as from Get-CsConferencingPolicy.&lt;/p&gt;

&lt;p&gt;Lastly you can get all the Lync objects from Active Directory as well. I like to use the query &lt;em&gt;(|(msRTCSIP-Line=*)(msRTCSIP-PrimaryUserAddress=*))&lt;/em&gt; since it returns &lt;em&gt;all&lt;/em&gt; Lync objects but you could further filter by objectClass if you wanted only users. The attribute &lt;em&gt;msRTCSIP-UserPolicies&lt;/em&gt; (which is a multi-value attribute) contains values in the form of [policyNumber]=[policyID]. The [policyNumber] for the conferencing policy is 1 so you just need to find the value that start with 1=. If there isn’t one, then the global or site policy apply.&lt;/p&gt;

&lt;p&gt;You can download a version I wrote in pure PowerShell that demonstrates this concept. The nice thing about PowerShell is that it’s so very close to .NET that it makes experimenting with things like this very easy. If anyone is interested let me know and maybe I’ll create a .NET library that does the above.&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Get-LyncUserLicensing.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Get-LyncUserLicensing.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Managing Your Lync Phone Numbers</title>
      <link>http://paul.vaillant.ca/2015/03/18/managing-your-lync-phone-numbers.html</link>
      <pubDate>Wed, 18 Mar 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/03/18/managing-your-lync-phone-numbers.html</guid>
      <description>&lt;p&gt;I love PowerShell! There, I said it, just in case anyone was wondering. I love that it let’s you dig in, mix and match data and get even more useful data out then what you started with. Case in point: I was working with a customer with a fairly large Lync deployment and a SIP trunk from ThinkTel who wanted to review the numbers they had on their SIP trunk and compare it to numbers assigned in Lync so they could identify any missing or available numbers. There are several example scripts around on how to do this. The key is that you have to pull from all the different kinds of objects that could be assigned phone numbers in Lync:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;hljs powershell&quot;&gt;
Get-CsUser -Filter {LineURI -ne $Null}
Get-CsUser -Filter {PrivateLine -ne $Null}
Get-CsAnalogDevice -Filter {LineURI -ne $Null}
Get-CsCommonAreaPhone -Filter {LineURI -ne $Null}
Get-CsRgsWorkflow | ?{ $_.LineURI }
Get-CsDialInConferencingAccessNumber -Filter {LineURI -ne $Null}
Get-CsExUmContact -Filter {LineURI -ne $Null}
Get-CsTrustedApplicationEndpoint -Filter {LineURI -ne $Null}
Get-CsMeetingRoom -Filter {LineURI -ne $Null}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I love RESTful APIs too! That’s the second part of this story. There’s a RESTful API for uControl, the ThinkTel web portal for managing SIP trunks, that let’s you get all of the DIDs on a SIP Trunk. Much easier than scraping a web page and trying to parse out the data from HTML.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;hljs powershell&quot;&gt;
$wc = New-Object System.Net.WebClient
$wc.Credentials = $(Get-Credential).GetNetworkCredential()
$sipTrunkDidsUri = &quot;https://api.thinktel.ca/REST.svc/SipTrunks/{0}/Dids?PageFrom=0&amp;amp;PageSize=100000&quot;
$sipTrunkDidsUrl = $sipTrunkDidsUri -f 7005551212
[xml]$didsXml = $wc.DownloadString($sipTrunkDidsUrl)
if($didsXml -and $didsXml.ArrayOfTerseNumber) {
	$didsXml.ArrayOfTerseNumber.TerseNumber | %{ $_.Number }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Replace &lt;em&gt;7005551212&lt;/em&gt; in the example above with your own SIP trunk pilot number.&lt;/p&gt;

&lt;p&gt;Now that we have all the assigned numbers and all the numbers on the trunk, it’s easy to loop through both and produce a list.&lt;/p&gt;

&lt;p&gt;One more RESTful API while we’re at it; you can use &lt;a href=&quot;http://localcallingguide.com&quot;&gt;Local Calling Guide&lt;/a&gt; to lookup the city for each of the numbers so that you can more easily find an available number for a new user based on where they need the number.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;hljs powershell&quot;&gt;
function LookupCity($tel) {
	$lcgUri = &quot;http://www.localcallingguide.com/xmlprefix.php?npa={0}&amp;amp;nxx={1}&quot;

	# we need a 10 digit number to extract the NPA/NXX from
	if($tel -match '^tel:+1(\d{10})(;ext=\d+)?$') {
		$tel = $tel = $tel.Substring(6,10)
	}
	if($tel -notmatch '^\d{10}$') {
		Write-Error &quot;Couldn't identify NPA/NXX in $tel&quot;
	}
	$npa = $tel.Substring(0,3)
	$nxx = $tel.Substring(3,3)

	if($npa -match &quot;^8(00|88|77|66|55|44|33|22)$&quot;) {
		&quot;Toll-free&quot;
	} else {
		$lcgUrl = $lcgUri -f $npanxx.Substring(0,3),$npanxx.Substring(3,3)
		[xml]$lcgXml = $wc.DownloadString($lcgUrl)
		if($lcgXml -and $lcgXml.root.prefixdata) {
			$lcgXml.root.prefixdata.rc + &quot;, &quot; + $lcgXml.root.prefixdata.region
		} else {
			Write-Warning &quot;Failed to identify city of $tel&quot;
			&quot;UNKNOWN&quot;
		}
	}
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I’ve put that all together into a script (couldn’t see that coming huh?): &lt;a href=&quot;/content/Get-LyncNumbers.ps1&quot;&gt;Get-LyncNumbers.ps1&lt;/a&gt;. Unless you have very few numbers, I definitely recommend piping the output of this script through &lt;a href=&quot;https://technet.microsoft.com/en-us/library/hh849932.aspx&quot;&gt;Export-CSV&lt;/a&gt; and viewing the data in Excel, or maybe &lt;a href=&quot;https://technet.microsoft.com/en-us/library/hh849920.aspx&quot;&gt;Out-GridView&lt;/a&gt; if you prefer that.&lt;/p&gt;

&lt;p&gt;Side plug: if you are a .NET developer looking to do rate center lookups by NPA/NXX, be sure to check out my other project &lt;a href=&quot;https://github.com/ThinkTel/ThinkTel.LocalCallingGuide&quot;&gt;ThinkTel.LocalCallingGuide&lt;/a&gt; on GitHub and also in &lt;a href=&quot;http://www.nuget.org/packages/ThinkTel.LocalCallingGuide/&quot;&gt;NuGet&lt;/a&gt;.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Keeping Lync Federated Domains Up To Date</title>
      <link>http://paul.vaillant.ca/2015/03/13/keeping-lync-federated-domains-up-to-date.html</link>
      <pubDate>Fri, 13 Mar 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/03/13/keeping-lync-federated-domains-up-to-date.html</guid>
      <description>&lt;p&gt;Federation is one of the best features of Lync. It let’s you connect with people outside your organization and get things done fast. Check out the &lt;a href=&quot;http://lyncdirectory.com/&quot;&gt;Lync Directory&lt;/a&gt; for a sampling of organizations that are openly federated with Lync.&lt;/p&gt;

&lt;p&gt;‘Open’ federation, what does that mean? When you try to connect to user@domain.com, and @domain.com is not a SIP Domain in your Lync environment, and you have enabled &lt;em&gt;partner domain discovery&lt;/em&gt; (which is an Access Edge Configuration option), Lync will look up the DNS SRV record &lt;em&gt;_sipfederationtls._tcp.domain.com&lt;/em&gt; and connect to the remote Lync environment.&lt;/p&gt;

&lt;p&gt;There are a number of safe guards built in. There’s a great reference of &lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg195674%28v=ocs.14%29.aspx&quot;&gt;Federation Safeguards for Lync&lt;/a&gt; on Technet. It says Lync 2010, but it gives you a sense of what’s going on. The key thing to note is that auto-discovered partners are subject to rate limiting. If there’s another Lync environment that you’re communicating heavily with, at some point you’re likely to run into these rate limits.&lt;/p&gt;

&lt;p&gt;End user’s will see them manifest as presence not updating, or IM messages not being sent or received. In the Edge Server event log, you’ll see event ID 14603 indicating that a remote partner has been rate limited and event ID 14601 showing all autodiscovered partners.&lt;/p&gt;

&lt;p&gt;How do you fix this? You use &lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg398628.aspx&quot;&gt;New-CsAllowedDomain&lt;/a&gt; to approve the domain for federation. Sure you could do it one domain at a time, but PowerShell to the rescue!&lt;/p&gt;

&lt;p&gt;You can use PowerShell to parse out the event log entries and call New-CsAllowedDomain. It can be a little tricky since the event log entries are on the Edge Server and you can’t run New-CsAllowedDomain on the Edge Server. I wrote a script &lt;a href=&quot;/content/Update-LyncFederatedDomains.ps1&quot;&gt;Update-LyncFederatedDomains.ps1&lt;/a&gt; that gives you a few options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1&lt;/strong&gt;: run on the Edge server and generate a script that you can run on the Front End. You can either use the -GUI option to get an interface that let’s you pick which domains to include in the script, or you can have the domains returned as PowerShell objects, filter them and use Update-LyncFederatedDomains to write them out to a script.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;hljs powershell&quot;&gt;Update-LyncFederatedDomains.ps1 -GUI&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&quot;hljs powershell&quot;&gt;Update-LyncFederatedDomains.ps1 | where Domain -match '.com$' | Update-LyncFederatedDomains.ps1 -FilePath .\path\to\front-end-script.ps1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Option 2&lt;/strong&gt;: run it on a machine like the Front-End that can call New-CsAllowedDomain. You have the same option as above to either use a GUI or to use the command line, but you’ll probably need to specify credentials to connect to the Edge server using -Credential. The script will attempt to automatically detect the Edge server, but if you want to force it, use -EdgeServer. In the second example below, it will take all discovered domains and try to call New-CsAllowedDomain in the generated script.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;hljs powershell&quot;&gt;Update-LyncFederatedDomains.ps1 -GUI -EdgeServer my-edge.acme.com -Credential $(Get-Credential)&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&quot;hljs powershell&quot;&gt;Update-LyncFederatedDomains.ps1 -Credential $(Get-Credential) -FilePath .\path\tp\front-end-script.ps1&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In the case where you use -GUI, you’ll need to click the &lt;em&gt;Save&lt;/em&gt; button to generate the file. Then, whether you ran -GUI or used -FilePath, run the generated script and good bye rate limiting messages!&lt;/p&gt;

&lt;p&gt;Happy keeping your federated domains approved!&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Lync Utility Belt</title>
      <link>http://paul.vaillant.ca/2015/03/06/lync-utility-belt.html</link>
      <pubDate>Fri, 06 Mar 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/03/06/lync-utility-belt.html</guid>
      <description>&lt;p&gt;People want to come in to my office all day long. Which is fine, except when I’m on my wireless headset, listening to someone, which of course looks like I’m not on the phone, so they start talking.
Before you ask, yes I’ve seen the &lt;a href=&quot;http://www.busylight.com/&quot;&gt;Busylight&lt;/a&gt;, I have one in fact, but never really liked it. So I set out to make something different.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/on-air-light.jpg&quot; alt=&quot;On-air light&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I was inspired by the idea of an on-air light that I could hang above my door so that my current status could be seen by people walking by. I had been wanting to get a &lt;a href=&quot;http://meethue.com&quot;&gt;Philips Hue&lt;/a&gt; light bulb and I figured this would be the perfect opportunity. I could even take it a step further then just on/off since the Philips Hue is an RGB LED bulb (it can show many different colors). It’s a neat system where you connect a base station to the network and then it becomes the API end point for controlling multiple individual light bulbs.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/hue-bulb.jpg&quot; alt=&quot;Philips Hue light bulb&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After getting my starter kit, I found a great library &lt;a href=&quot;https://github.com/Q42/Q42.HueApi&quot;&gt;Q42.HueApi&lt;/a&gt; and start to code up my app. I didn’t want anything too heavy, so it’s a systray only application. It uses the &lt;a href=&quot;http://www.microsoft.com/en-us/download/details.aspx?id=36824&quot;&gt;Microsoft Lync 2013 SDK&lt;/a&gt; to connect to Lync and get notified of presence updates. That way, when you pick up the phone and your presence goes to &lt;em&gt;‘in a call’&lt;/em&gt;, the app sends a color change command to the Hue light bulb. It also&lt;/p&gt;

&lt;p&gt;Once I had that going, I figured it would also be nice to connect it to Outlook so that my Lync presence would go &lt;em&gt;‘off work’&lt;/em&gt; after the day is over, so I added that as well. And lastly, I often wonder where a phone number is from if I don’t recognize the number so I added a toast notification for incoming calls that shows what city the number is from. The data is via [Local Calling Guide](http://localcallingguide.com].&lt;/p&gt;

&lt;p&gt;So why call it the Lync Utility Belt?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/lyncutilitybelt-logo.png&quot; alt=&quot;Lync Utility Belt logo&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Well, where would Batman be without his…&lt;/p&gt;

&lt;p&gt;Check it out on GitHub &lt;a href=&quot;https://github.com/pvaillant/LyncUtilityBelt&quot;&gt;LyncUtilityBelt&lt;/a&gt; you can also download the binary below. You’ll need .NET 4.5+.&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;https://github.com/pvaillant/LyncUtilityBelt/releases/download/v1.0.0/LyncUtilityBelt-1.0.0.zip&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; LyncUtilityBelt-1.0.0.zip &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;BTW: On-air light picture is from &lt;a href=&quot;http://www.freeimages.com/photo/190552&quot;&gt;http://www.freeimages.com/photo/190552&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Measuring Lync Conference Adoption</title>
      <link>http://paul.vaillant.ca/2015/03/03/measuring-lync-conference-adoption.html</link>
      <pubDate>Tue, 03 Mar 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/03/03/measuring-lync-conference-adoption.html</guid>
      <description>&lt;p&gt;I like data. There, I’ve said it and I’m not ashamed. The best decisions are made with good data. It helps take the decision making process from one of “I think” or “I hope” to “I know”.&lt;/p&gt;

&lt;p&gt;I often use data to help customers better understand their environment and their users. When you have more than a small number of users, you can’t just walk around and ask the same question to everyone.&lt;/p&gt;

&lt;p&gt;Recently I was working with a customer who had enabled all their users for every Lync feature quite some time ago. They were getting ready for an upgrade and wanted to re-gauge how the various Lync features were being used.&lt;/p&gt;

&lt;p&gt;Gauging adoption is important for so many reasons. With this data you can ensure that users are actually using the feature you’ve enabled them for. If they aren’t, you can work with them to find out why and either correct it or adjust the expected model. This last is key because all of the sizing for a product as complex as Lync is based on a &lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg398811.aspx&quot;&gt;user models&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first place I started with is how many people are hosting conferences and how many people are attending conferences. It’s important to gauge attendance as well as hosting because they have different implications. The great news is all the data you need is in the LcsCDR database!&lt;/p&gt;

&lt;p&gt;Conference attendees can be retrieved using:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;hljs sql&quot;&gt;
use LcsCDR;

with localUsers as (
	select distinct UserUri
	from Registration r join Users u on r.UserId = u.UserId
), confsAndUsers as (
    select 
        ConferenceStartTime, ConferenceEndTime, o.UserUri as orgUserUri, u.UserUri
    from Conferences c
        join Users o on c.OrganizerId = o.UserId
        join McuJoinsAndLeaves jl on jl.SessionIdTime = c.SessionIdTime and jl.SessionIdSeq = c.SessionIdSeq
        join Users u on jl.UserId = u.UserId
        join Mcus m on jl.McuId = m.McuId
        join UriTypes ut on m.McuTypeId = ut.UriTypeId
        left join FocusJoinsAndLeaves fjl on 
            fjl.SessionIdTime = c.SessionIdTime and fjl.SessionIdSeq = c.SessionIdSeq and fjl.UserId = jl.UserId
        left join ClientVersions cv on 
            fjl.ClientVerId = cv.VersionId
    where 
        (ut.UriType = 'conf:audio-video' or ut.UriType = 'conf:applicationsharing' or ut.UriType = 'conf:data-conf')
        and cv.ClientType != 256 and cv.ClientType != 16396
    group by ConferenceStartTime,ConferenceEndTime,o.UserUri,u.UserUri
)

select
    COUNT(*) as ParticipantCount, cu.UserUri
from
    confsAndUsers cu join localUsers lu on cu.UserUri = lu.UserUri
group by cu.UserUri

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What I’m doing at the start here is getting a list of &lt;em&gt;local users&lt;/em&gt; by getting the list of all users who have logged into this system. I do this because if an external user joins a meeting via federation, we have no means of knowing in the join/leave information and we only want to report on local users who have participated in meetings. Then I get a list of all the conferences and their organizers/participants. And lastly I select how many unique conferences each local user has participated in.&lt;/p&gt;

&lt;p&gt;One value I sometimes also include is the cv.Version which is the User-Agent string that identifies what kind of client the user was using. This can be useful in knowing if users have a preference for what kind of client/device they connect to meetings with.&lt;/p&gt;

&lt;p&gt;Similarly, conference organizers can be retrieved using:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;hljs sql&quot;&gt;
use LcsCDR;

with confsAndUsers as (
    select 
        ConferenceStartTime, ConferenceEndTime, o.UserUri as orgUserUri, u.UserUri
    from Conferences c
        join Users o on c.OrganizerId = o.UserId
        join McuJoinsAndLeaves jl on jl.SessionIdTime = c.SessionIdTime and jl.SessionIdSeq = c.SessionIdSeq
        join Users u on jl.UserId = u.UserId
        join Mcus m on jl.McuId = m.McuId
        join UriTypes ut on m.McuTypeId = ut.UriTypeId
        left join FocusJoinsAndLeaves fjl on 
            fjl.SessionIdTime = c.SessionIdTime and fjl.SessionIdSeq = c.SessionIdSeq and fjl.UserId = jl.UserId
        left join ClientVersions cv on 
            fjl.ClientVerId = cv.VersionId
    where 
        (ut.UriType = 'conf:audio-video' or ut.UriType = 'conf:applicationsharing' or ut.UriType = 'conf:data-conf')
        and cv.ClientType != 256 and cv.ClientType != 16396
    group by ConferenceStartTime,ConferenceEndTime,o.UserUri,u.UserUri
),
confsAndAttendeeCounts as (
    select 
        ConferenceStartTime,
        CONVERT(varchar,(ConferenceEndTime - ConferenceStartTime),108) as Duration,
        orgUserUri as OrganizerUri,
        COUNT(*) as AttendeeCount
    from confsAndUsers
    group by ConferenceStartTime,ConferenceEndTime,orgUserUri having COUNT(*) &amp;gt; 1
)

select 
    COUNT(*) as HostingCount,
    MIN(AttendeeCount) as MinAttendeeCount,
    AVG(AttendeeCount) as AvgAttendeeCount,
    MAX(AttendeeCount) as MaxAttendeeCount,
    OrganizerUri as UserUri
from confsAndAttendeeCounts 
group by OrganizerUri

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This returns all users who hosted conferences, along with how many conferences they hosted and the minimum, average and maximum number of participants. In this case we don’t need &lt;em&gt;localUsers&lt;/em&gt; since only local users are able to host meetings in the first place. One note: when I say &lt;em&gt;conferences&lt;/em&gt; I mean audio/video conference or application/desktop sharing (so &lt;em&gt;not&lt;/em&gt; multi-party IM). This corresponds to the Lync Enterprise CAL.&lt;/p&gt;

&lt;p&gt;Since both of these are dependant on the LcsCDR data, they are both subject to the standard warning that the time period you’re able to retrieve is set by the retention period of the cdr configuration (maybe I should have post just on that…). Once nice thing is these SQL scripts are compatible with both Lync 2010 and 2013.&lt;/p&gt;

&lt;p&gt;Like many of my other SQL scripts, I also have a PowerShell script for this. In case you’re wondering, I like the PowerShell script version mainly so I never have to open the SQL Management Studio to run it. It’s also nice to be able to use PowerShell to dive into the data and slice is up on the fly. Plus you can easily export it to Excel with &lt;a href=&quot;https://technet.microsoft.com/en-us/library/hh849932.aspx&quot;&gt;Export-CSV&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Get-LyncConferenceAdoption.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Get-LyncConferenceAdoption.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>ThinkTel in the NuGet</title>
      <link>http://paul.vaillant.ca/2015/02/26/thinktel-in-the-nuget.html</link>
      <pubDate>Thu, 26 Feb 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/02/26/thinktel-in-the-nuget.html</guid>
      <description>&lt;p&gt;Maybe a better title would have been &lt;em&gt;“ThinkTel uControl libraries now available via NuGet”&lt;/em&gt; but I was going for more of a &lt;em&gt;“ThinkTel in the house”&lt;/em&gt; feel. Because literary license&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. Before we get to the actual content, &lt;em&gt;DISCLAIMER: I work for ThinkTel&lt;/em&gt;. That said, all views are my own.&lt;/p&gt;

&lt;p&gt;Ok, with all that out of the way, if you are a &lt;a href=&quot;http://thinktel.ca&quot;&gt;ThinkTel&lt;/a&gt; customer this might be worth checking out. There are 2 libraries available via &lt;a href=&quot;http://nuget.org&quot;&gt;NuGet&lt;/a&gt; to help you manage your ThinkTel services:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://nuget.org/packages/ThinkTel.uControl.Api/&quot;&gt;ThinkTel.uControl.Api&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://nuget.org/packages/ThinkTel.uControl.Cdrs/&quot;&gt;ThinkTel.uControl.Cdrs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are .NET interfaces to our uControl system. If you aren’t a ThinkTel customer, or don’t know what uControl is, here’s a quick synopsis. ThinkTel is a &lt;a href=&quot;http://en.wikipedia.org/wiki/Competitive_local_exchange_carrier&quot;&gt;CLEC&lt;/a&gt; phone company in Canada (among other things) and uControl is our real-time service management portal that lets customers do things like create new SIP trunks, manage bindings, order &lt;a href=&quot;http://en.wikipedia.org/wiki/Direct_inward_dial&quot;&gt;DIDs&lt;/a&gt; and update v911 address information. If you you want to know more feel free to contact me or your account manager.&lt;/p&gt;

&lt;p&gt;On to the interesting things. In addition to being a web portal, uControl is also a RESTful API so you don’t have to use our web interface, you can call the API from within your own systems. Our customers can do this for various reason; integrating phone number management with user provisioning/HR systems, automated service reconciliation, charge back accounting of usage to departments, wholesale level integration for rebranding purposes and many, many more scenarios.&lt;/p&gt;

&lt;p&gt;There’s much more that our API can do then we have exposed in this library but it has all the basic things in it. If you are interested in something you can do in uControl but don’t see it in the API, or are curious about how you could use it for your own purposes but aren’t sure where to start, feel free to contact me.&lt;/p&gt;

&lt;p&gt;Next up; using uControl via PowerShell!&lt;/p&gt;

&lt;div class=&quot;footnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;http://www.theatlantic.com/technology/archive/2013/11/english-has-a-new-preposition-because-internet/281601/&quot;&gt;English Has a New Preposition, Because Internet&lt;/a&gt; &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    
    <item>
      <title>Last Login for a SIP URI</title>
      <link>http://paul.vaillant.ca/2015/02/22/last-login-for-a-sip-uri.html</link>
      <pubDate>Sun, 22 Feb 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/02/22/last-login-for-a-sip-uri.html</guid>
      <description>&lt;p&gt;Ever wanted to know when the last time someone logged in was? Ever wanted from where a user last logged in? Or even what client or client version someone is using? You could ask the user to provide this information but that would probably annoy them and give you information with a varying degree of accuracy. I always suggest you never ask users for something that you can figure out yourself and in this case all the information you need is in the LcsCDR database.&lt;/p&gt;

&lt;p&gt;What is the LcsCDR database? It’s one of the two databases of the Monitoring role in Lync. It’s also in both Lync 2010 and Lync 2013, although the schema changed slightly between the two versions. The schema is available on &lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg398570.aspx&quot;&gt;TechNet&lt;/a&gt; if you’re really interested. The following query will retrieve the last 10 logins (aka registrations) for a given user SIP URI.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;hljs sql&quot;&gt;
USE LcsCDR
SELECT TOP 10
    r.RegisterTime,
    r.DeRegisterTime,
    drt.DeRegisterReason,
    cv.Version as ClientVersion,
    r.IpAddress,
    r.ResponseCode,
    s.ServerFQDN as Registrar,
    p.PoolFQDN as Pool,
    e.EdgeServer,
    dbo.FormatMacAddr(d.MacAddress) as MacAddress,
    m.Manufacturer,
    hv.Version as HardwareVersion
FROM 
    Registration as r
    join Users as u on r.UserId = u.UserId
    join ClientVersions as cv on r.ClientVersionId = cv.VersionId
    join Servers as s on r.RegistrarId = s.ServerId
    join Pools as p on r.PoolId = p.PoolId
    left outer join EdgeServers as e on r.EdgeServerId = e.EdgeServerId
    left outer join DeRegisterType as drt on r.DeRegisterTypeId = drt.DeRegisterTypeId
    left outer join Devices as d on r.DeviceId = d.DeviceId
    left outer join Manufacturers as m on d.ManufacturerId = m.ManufacturerId
    left outer join HardwareVersions as hv on d.HardwareVersionId = hv.VersionId
WHERE 
    u.UserUri = 'john.doe@domain.com'
ORDER BY
    r.RegisterTime DESC

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One quick note: if you were just interested in Lync 2013, this query could be made a little simplier by using the RegistrationView instead of the Registration table and joining a bunch of other tables. Unfortunately the RegistrationView in Lync 2010 is very different so by writing it out like this it’s portable* between the two (* Lync 2010 doesn’t have an IpAddress field in the Registration table so just remove that line).&lt;/p&gt;

&lt;p&gt;Replace &lt;em&gt;john.doe@domain.com&lt;/em&gt; and run this against the database for the pool the user is located in and you should get back the user’s last 10 logins. You can replace &lt;em&gt;10&lt;/em&gt; with how ever many logins you want, or remove &lt;em&gt;TOP 10&lt;/em&gt; to get all their logins (up to the configured retention; see &lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg398298.aspx&quot;&gt;Get-CsCdrConfiguration&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A few notes: 
 * you check &lt;a href=&quot;https://technet.microsoft.com/en-us/library/gg398142.aspx&quot;&gt;TechNet&lt;/a&gt; for a list of possible DeRegisterReason
 * while ResponseCode will normally be 200 (OK), sometimes it can be something else. Check out the &lt;a href=&quot;http://en.wikipedia.org/wiki/List_of_SIP_response_codes&quot;&gt;list of SIP response codes&lt;/a&gt; in those cases
 * MacAddress, Manufacturer and HardwareVersion will only have values if the user was logged in on a handset&lt;/p&gt;

&lt;p&gt;In large environments with multiple pools, or in cases where users are moved around, you can use Get-CsService to get a list of monitoring databases and run this query against all of them. PowerShell can definitely make this task much easier:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;hljs powershell&quot;&gt;
$sql = @&quot;
    ....
&quot;@
$ds = New-Object System.Data.DataSet
Get-CsService -MonitoringDatabase | foreach {
    $dbSvr = if($_.SqlInstanceName) { 
        $_.PoolFQDN + &quot;\&quot; + $_.SqlInstanceName 
    } else { 
        $_.PoolFqdn 
    }
    $connStr = &quot;Server=&quot; + $dbSvr + &quot;;Integrated Security=True&quot;
    $conn = New-Object System.Data.SqlClient.SqlConnection $connStr
    $conn.open()
    $cmd = New-Object System.Data.SqlClient.SqlCommand $sql,$conn
    $da = New-Object System.Data.SqlClient.SqlDataAdapter $cmd
    [void]$da.fill($ds) 
    $conn.Close()
}
$ds.Tables[0] | sort RegisterTime -Descending | select -Last 10

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can actually download this SQL that uses a version of this PowerShell (with nice parameters and Lync 2010/2013 database detection) to run it against all Monitoring databases. Just drop it on a machine with the Lync PowerShell module and run it as a user that has access to the LcsCDR database (CSAdministrator member).&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Get-LyncLogins.ps1 -UserUri john.doe@domain.com -Last 10
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Want to get some data out of Lync but not sure how? Let me know and I’ll try to write some PowerShell and/or SQL for it.&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;/content/Get-LyncLogins.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Get-LyncLogins.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Lync Backup Script</title>
      <link>http://paul.vaillant.ca/2015/02/19/lync-backup-script.html</link>
      <pubDate>Thu, 19 Feb 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/02/19/lync-backup-script.html</guid>
      <description>&lt;p&gt;Oh Lync, let me count the ways I love thee. Top of the list would be how I can manage everything with PowerShell. For an administrator, it’s not just about being able to do things without having to wait for a GUI, but it’s also about being able to create scripts and to automate or simplify the tasks that have to be done every day.&lt;/p&gt;

&lt;p&gt;Who doesn’t like sitting back while a machine does all the hard work instead of feverishly clicking buttons and being frustrated at the responsiveness of the GUI?&lt;/p&gt;

&lt;p&gt;PowerShell is also great for being able to get information, like the current configuration of the various parts of a Lync deployment. There are a few scripts out there already that leverage the Lync PowerShell cmdlets to create a snapshot of an environment but I was looking for something more. I wanted to be able to track how an environment changed. I wanted to be notified about those changes too so that I, and anyone else I was managing that environment with, would know when anyone else made a change. Big systems aren’t managed by one person after all.&lt;/p&gt;

&lt;p&gt;In the Cisco/networking world this has been around for a long time. The typical example is &lt;a href=&quot;http://en.wikipedia.org/wiki/RANCID_(software)&quot;&gt;RANCID&lt;/a&gt;. The way RANCID works is by storing configs in CVS or SVN (those are &lt;a href=&quot;http://en.wikipedia.org/wiki/Revision_control&quot;&gt;version control&lt;/a&gt; systems if you aren’t familiar with those acronyms). Why can’t we do something similar for Lync?&lt;/p&gt;

&lt;p&gt;Enter &lt;a href=&quot;https://github.com/thinktel/Backup-Lync&quot;&gt;Backup-Lync.ps1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It runs as a scheduled task on any machine that has the Lync PowerShell module installed and access to the Lync environment. It uses the various get-cs* PowerShell cmdlets to get data, System.Linq.Xml to format the files nicely (basically to prettify the XML so that it’s on multiple lines rather than being all one big giant line) and &lt;a href=&quot;http://en.wikipedia.org/wiki/Git_(software)&quot;&gt;GIT&lt;/a&gt; via libgit2sharp (a .NET interface to &lt;a href=&quot;https://libgit2.github.com/&quot;&gt;libgit2&lt;/a&gt;) as it’s version control system.&lt;/p&gt;

&lt;p&gt;In addition to the configuration data, it can also keep track of user data (contacts, conferences, etc). It keeps these in one file per user and is extremely handy if someone deletes contacts by accident or if a user is disabled accidentally. It’s amazing how fast Disable-CsUser will purge out all the users data.&lt;/p&gt;

&lt;p&gt;Give it a try and let me know what you think.&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;download&quot; href=&quot;https://raw.githubusercontent.com/ThinkTel/Backup-Lync/master/Backup-Lync.ps1&quot;&gt;&lt;i class=&quot;fa fa-file-text-o&quot;&gt;&lt;/i&gt; Backup-Lync.ps1 &lt;i class=&quot;fa fa-download&quot;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Hello, World!</title>
      <link>http://paul.vaillant.ca/2015/02/16/hello-world.html</link>
      <pubDate>Mon, 16 Feb 2015 00:00:00 UTC</pubDate>
      <author>Paul Vaillant</author>
      <guid>http://paul.vaillant.ca/2015/02/16/hello-world.html</guid>
      <description>&lt;p&gt;What better way to start a new blog then those famous first words. Well famous in computer science/IT/programming. If this doesn’t sound familiar to you I’ll give you a minute to check the following reference: &lt;a href=&quot;http://en.wikipedia.org/wiki/%22Hello,_world!%22_program&quot;&gt;“Hello, world!” on Wikipedia&lt;/a&gt;. Go ahead, I’ll wait.&lt;/p&gt;

&lt;p&gt;Done?&lt;/p&gt;

&lt;p&gt;Ok, now that we have that straight, I’ll say that I’m hoping to be able to put up new content about once a week. In a good week it might be twice.&lt;/p&gt;

&lt;p&gt;That is all. Feel free to check out my &lt;a href=&quot;https://twitter.com/paulvaillant&quot;&gt;twitter&lt;/a&gt; or &lt;a href=&quot;https://github.com/pvaillant&quot;&gt;github&lt;/a&gt; accounts for anything you might find interesting in a mean time.&lt;/p&gt;
</description>
    </item>
    
  </channel> 
</rss>