Monday, March 11, 2013

Lync Server 2013 - Bulk Updating Contact Groups

I have been working on a rollout of Lync 2013 at my company over the last couple weeks. We are moving from an OpenFire/Spark setup to Lync mostly because we wanted to give our users the ability to do video chats and screen share's easily and Spark did not really provide this functionality for us.

The biggest thing we found lacking in Lync 2013 is no central management of Contact Groups. With Spark we setup the groups and they are loaded whenever a user signs on.



Our users really like having this list and we did not want them to have to manually configure all of the groups. I was not able to find any specifics of exactly how to do this on the web so I decided to roll up my sleeves and do it myself.  There are a few more things I would like to add to it and clean up but I wanted to get it out in the wild for other people to use.

Currently this script will only update the Contact Groups for a user who has logged into the server at least once. Additionally if the user has already added some of the groups in the past it will retain the original order of the previously added groups and append the new ones on at the bottom.

You will need 7-Zip installed on your link server to run this script.

Here are the basic steps to use the script:
  1. Create a reference account and add the Contact Groups you want to load for your other users.

  2. Log into your Lync Server and from the Lync Management Shell export the user you updated with the following command.

    Export-CsUserData -PoolFqdn "YOURLINKPOOL" -UserFilter first.last@something.com -FileName "c:\temp\ExportedUserData.zip"
    

  3. Extract the Zip file and open the DocItemSet.xml file.  I recommend using Notepad++ with the XML Tools Plug to format that file into something readable.

    We are interested in the "ContactGroups" XML node.  Each ContactGroup entry in this list corresponds to one of the contact groups you added to the reference account.  Copy and paste this out of the XML file.  It should look something like this:

     
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
     
    
  4. You're ready to copy the script below. Update variables as you see fit in the the Settings section on top. You will definitely have to update the following variables:

    $lyncpoolfqdn - to your own FQDN

    $replacement - paste your default ContactGroups XML in place of my sanitzed version


##########################################################################################################################
#
#    Name:            Update_Lync_2013_Groups.ps1    
#    Author:         Charles Ulrich
#    Date:            03/08/2013
#    Description:    Script to update the Contact List for 1 or All Lync 2013 users.
#
#    Requirements:    1. MUST BE RUN ON LYNC SERVER - Export-CSUserData does not like Remote Shell or Implicit Remoting
#                    2. 7-Zip must be installed - Used to manage the ZIP files from the export and needed for the import.
#                    3. If not running under Powershell v3.0 see comments in MultipleSelectionBox function.
#
#
##########################################################################################################################

# Settings

#Lync Server
$lyncpoolfqdn = "YOURLINKSERVERPOOL"

#Base File Path
$BaseFilePath = "c:\scripts\lync_user_export"

#7-Zip Paths
$7ZipPath = "c:\Program Files\7-zip\7z.exe"
$7ZipOutputDir = $BaseFilePath + "\working"
$7ZipOutputParam = "-o" + $7ZipOutputDir
$7ZipIncludeFiles = $7ZipOutputDir + "\*.xml"

#Lync XML and ZIP file paths
$LyncXMLFile = $7ZipOutputDir + "\DocItemSet.xml"
$ExportFileNamePath = $BaseFilePath + "\ExportedUserData.zip"
$UpdatedFileNamePath = $BaseFilePath + "\UpdatedUserData.zip"

# replacement node with child nodes
[xml]$replacement = @'
    <ContactGroups>
      <ContactGroup Number="1" DisplayName="fg=="/>
      <ContactGroup Number="2" DisplayName="WJvopsjer3ojfaopj" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="3" DisplayName="SFDEW3523j" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop39357673425hdfbsart3462twqgaedfgajoisdfoipqhuy38tpy32u980hfiaosdhiogpkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="4" DisplayName="35uyh" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="5" DisplayName="fbdsfh346" ExternalUri="Pfaohiweifgouwe27y0ahsvokja;sdC9n3904kjlkJKSL904="/>
      <ContactGroup Number="6" DisplayName="sfdhw3466" ExternalUri="Pfaohvbasrtyq4tyasv58osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="7" DisplayName="asetg4363y6" ExternalUri="Pfaohiweifgouwe90vsdgwerhjudfbsdfkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="8" DisplayName="634tsagasdg" ExternalUri="Pfaohidsvsdgherhysdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="9" DisplayName="ngfd56w3" ExternalUri="Pfaohiweifgouwe90ufjdasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="10" DisplayName="asdgsa46" ExternalUri="Pfaohiweifgouwe90ufSDfsdtewtgvjopaiwejr302582385ioskjgl;skdaopsok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="11" DisplayName="agasg" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="12" DisplayName="235wgsdfa" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="13" DisplayName="236gvbasetaw" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="14" DisplayName="asdgawqt3aqw" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="15" DisplayName="casdrwqt5" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
    </ContactGroups>
'@


Function MultipleSelectionBox ($inputarray,$prompt,$listboxtype) {

# Taken from Technet - http://technet.microsoft.com/en-us/library/ff730950.aspx
# This version has been updated to work with Powershell v3.0.
# Had top replace $x with $Script:x throughout the function to make it work. 
# This specifies the scope of the X variable.  Not sure why this is needed for v3.
# http://social.technet.microsoft.com/Forums/en-SG/winserverpowershell/thread/bc95fb6c-c583-47c3-94c1-f0d3abe1fafc

$Script:x = @()

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") 

$objForm = New-Object System.Windows.Forms.Form 
$objForm.Text = $prompt
$objForm.Size = New-Object System.Drawing.Size(300,600) 
$objForm.StartPosition = "CenterScreen"

$objForm.KeyPreview = $True

$objForm.Add_KeyDown({if ($_.KeyCode -eq "Enter") 
    {
        foreach ($objItem in $objListbox.SelectedItems)
            {$Script:x += $objItem}
        $objForm.Close()
    }
    })

$objForm.Add_KeyDown({if ($_.KeyCode -eq "Escape") 
    {$objForm.Close()}})

$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Size(75,520)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"

$OKButton.Add_Click(
   {
        foreach ($objItem in $objListbox.SelectedItems)
            {$Script:x += $objItem}
        $objForm.Close()
   })

$objForm.Controls.Add($OKButton)

$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Size(150,520)
$CancelButton.Size = New-Object System.Drawing.Size(75,23)
$CancelButton.Text = "Cancel"
$CancelButton.Add_Click({$objForm.Close()})
$objForm.Controls.Add($CancelButton)

$objLabel = New-Object System.Windows.Forms.Label
$objLabel.Location = New-Object System.Drawing.Size(10,20) 
$objLabel.Size = New-Object System.Drawing.Size(280,20) 
$objLabel.Text = "Please make a selection from the list below:"
$objForm.Controls.Add($objLabel) 

$objListbox = New-Object System.Windows.Forms.Listbox 
$objListbox.Location = New-Object System.Drawing.Size(10,40) 
$objListbox.Size = New-Object System.Drawing.Size(260,20) 

$objListbox.SelectionMode = $listboxtype

$inputarray | ForEach-Object {[void] $objListbox.Items.Add($_)}

$objListbox.Height = 470
$objForm.Controls.Add($objListbox) 
$objForm.Topmost = $True

$objForm.Add_Shown({$objForm.Activate()})
[void] $objForm.ShowDialog()

Return $Script:x
}


#Load Lync Powershell Commands
Import-Module Lync

#Who should we update
$sora = Read-Host "Do you want to update (A)ll users or a (S)ingle user? (S or A)"

If ($sora -eq "S")
    {
        # Get user to update 
        $userlist = Get-CSUser
        $user_email=MultipleSelectionBox $userlist.UserPrincipalName "Choose Lync 2013 User" "One"
        
        #Export Single Users Data
        Export-CsUserData -PoolFqdn $lyncpoolfqdn -UserFilter $user_email -FileName $ExportFileNamePath
    }
Else
    {
        #Export All Users Data
        Export-CsUserData -PoolFqdn $lyncpoolfqdn -FileName $ExportFileNamePath
    }

#Extract the Exported Zip file. Requires 7-Zip
Write-Host " "
&$7ZipPath e $ExportFileNamePath $7ZipOutputParam
Write-Host " "
Write-Host " "
$original = [xml] (Get-Content $LyncXMLFile)

#Set our loop counter to 0
$count = $original.DocItemSet.DocItem.Count + 1

Write-Host "########################################################################"

#Loop through all DocItem Elements and replace any with ContactGroups in them
For ($i=0; $i -lt $count; $i++) {
        If (($original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count -gt 0)) 
            {
                # get the target node
                
                Write-Host " "
                Write-Host "Working on XML Node: " $original.DocItemSet.DocItem[$i].Name
                Write-Host " "
                Write-Host "Contact Groups Before: "$original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count
                Write-Host " "
                $inner = $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups
                
                # import the replacement values
                $new = $original.ImportNode($replacement.ContactGroups, $true)
                
                # replace old node with new one (replacement node)
                $dump = $original.DocItemSet.DocItem[$i].Data.HomedResource.ReplaceChild($new, $inner)
                
                Write-Host "Contact Groups After: "$original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count
        }
    }

Write-Host " "
Write-Host "########################################################################"
Write-Host " "

#Remove blank xmlns tags created by importing the node
$original = [xml] $original.OuterXml.Replace(" xmlns=`"`"", "")

# save changes (full path to file)
$original.Save($LyncXMLFile)

# create updated zip file
& $7ZipPath a $UpdatedFileNamePath $7ZipIncludeFiles

Write-Host " "
Write-Host "########################################################################"
Write-Host " "
Write-Host "The XML file has been updated with the default groups."
Write-Host " "
Write-Host "If you want to take a look at the file its path is"
Write-Host " " 
Write-Host $LyncXMLFile
Write-Host " "
Write-Host "This file will be deleted once this script has finished."
Write-Host " "
Write-Host "########################################################################"
Write-Host " "

$sure = Read-Host "About to upload the changes to the server.  Are you sure? (Y or N)"

If ($sure -eq "Y")
    {
        Write-Host " "
        Write-Host "Updating Server with the new Contact Group Settings."
        Write-Host " "
        Write-Host "The user(s) will have no Contact groups until they" 
        Write-Host "log off and back on to Lync."
        
        If ($sora -eq "S")
            {
                # Update the server with the new User Data
                Update-CsUserData -Filename $UpdatedFileNamePath -UserFilter $user_email
            }
        Else
            {
                # Update the server with the new User Data
                Update-CsUserData -Filename $UpdatedFileNamePath
            }
        Write-Host " "
    }
Else
    {
        Write-Host " "
        Write-Host "Update Aborted!!!!!!"
        Write-Host " "
        Write-Host "Please Come Again!  :) " 
        Write-Host " "
    }

Write-Host "I will clean up the files and close the window after you hit Enter."
Write-Host " "

PAUSE

# Clean Up
Remove-Item ($BaseFilePath + "\*") -recurse

Friday, March 8, 2013

Powershell Multi-Select List Box Function

I use a bit of code for creating a multiple selection list box from an old technet article (http://technet.microsoft.com/en-us/library/ff730950.aspx) in a number of my powershell scripts.

Recently I was putting together a script on my new Windows 8 box and found that the function no longer returned what was selected. I ended up finding the solution to my problem here.

The issue seems to be related to the way Powershell 3.0 handles variable scopes. To make it work I had to add the script scope to the variable in the function.

Originally the variable was $x, now its $Script:x.

In case anyone finds themselves in the same boat i did here is an updated version of the function.

Function MultipleSelectionBox ($inputarray,$prompt,$listboxtype) {

# Taken from Technet - http://technet.microsoft.com/en-us/library/ff730950.aspx
# This version has been updated to work with Powershell v3.0.
# Had to replace $x with $Script:x throughout the function to make it work. 
# This specifies the scope of the X variable.  Not sure why this is needed for v3.
# http://social.technet.microsoft.com/Forums/en-SG/winserverpowershell/thread/bc95fb6c-c583-47c3-94c1-f0d3abe1fafc
#
# Function has 3 inputs:
#     $inputarray = Array of values to be shown in the list box.
#     $prompt = The title of the list box
#     $listboxtype = system.windows.forms.selectionmode (None, One, MutiSimple, or MultiExtended)

$Script:x = @()

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") 

$objForm = New-Object System.Windows.Forms.Form 
$objForm.Text = $prompt
$objForm.Size = New-Object System.Drawing.Size(300,600) 
$objForm.StartPosition = "CenterScreen"

$objForm.KeyPreview = $True

$objForm.Add_KeyDown({if ($_.KeyCode -eq "Enter") 
    {
        foreach ($objItem in $objListbox.SelectedItems)
            {$Script:x += $objItem}
        $objForm.Close()
    }
    })

$objForm.Add_KeyDown({if ($_.KeyCode -eq "Escape") 
    {$objForm.Close()}})

$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Size(75,520)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"

$OKButton.Add_Click(
   {
        foreach ($objItem in $objListbox.SelectedItems)
            {$Script:x += $objItem}
        $objForm.Close()
   })

$objForm.Controls.Add($OKButton)

$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Size(150,520)
$CancelButton.Size = New-Object System.Drawing.Size(75,23)
$CancelButton.Text = "Cancel"
$CancelButton.Add_Click({$objForm.Close()})
$objForm.Controls.Add($CancelButton)

$objLabel = New-Object System.Windows.Forms.Label
$objLabel.Location = New-Object System.Drawing.Size(10,20) 
$objLabel.Size = New-Object System.Drawing.Size(280,20) 
$objLabel.Text = "Please make a selection from the list below:"
$objForm.Controls.Add($objLabel) 

$objListbox = New-Object System.Windows.Forms.Listbox 
$objListbox.Location = New-Object System.Drawing.Size(10,40) 
$objListbox.Size = New-Object System.Drawing.Size(260,20) 

$objListbox.SelectionMode = $listboxtype

$inputarray | ForEach-Object {[void] $objListbox.Items.Add($_)}

$objListbox.Height = 470
$objForm.Controls.Add($objListbox) 
$objForm.Topmost = $True

$objForm.Add_Shown({$objForm.Activate()})
[void] $objForm.ShowDialog()

Return $Script:x
}
After an extremely long hiatus........I am going to try this once again.  I have some good stuff coming. The first post I am working on will be posted in the next few days.  It is a powershell script for updating Lync 2013 Contact Groups from the server side.