Archive for Messaging

Migration to Office 365/Exchange Online

I recently had a chance to help a small business move their 70 mailboxes to the Exchange Online service. This company was running Windows 2008 with Exchange 2007 on premises installation and we helped them migrate to Office 365/Exchange Online over a couple weekends. During the migration we ran into several issues. I couldn’t find many online resources where people documented these errors, so I wanted to write them down on this site.

Issue #1
Symptoms:
Outlook Anywhere (RPC over HTTPS) service was configured, running and available to end users. However we received various errors when trying to establish a migration endpoint. Even though the service was working, the Remote Connectivity Analyzer (https://testconnectivity.microsoft.com/) was failing on Outlook Anywhere tests.
Resolution:
Looking around online, we thought the issue was related to the Exchange 2007 service pack level, so we upgraded to SP3. I’m not sure if this actually helped, but I figured it was worth mentioning as it may have fixed other issues we could have seen during the migration. The actual fix for this issue (and unfortunately I can’t find the Office 365 communities post where we found the suggestion) was to create a hosts file entry on the exchange server containing the IP, server name and FQDN of the internal Exchange server name. DNS was working perfectly, so I’m not sure why this was needed. However, after the entry was added the Remote Connectivity Analyzer tests started working and we were able to move forward with the migration.

Issue #2
Symptoms:
Batch loaded into Office 365 environment to begin migration, but after several hours the task fails
Error log may mention “MigrationPermanentException: Error: MapiExceptionLogonFailed: Unable to make connection to the server”
Resolution:
Verify permissions, specifically Receive-As rights to the database (http://community.office365.com/en-us/forums/158/t/18911.aspx).

Add-ADPermission -Identity "Mailbox Store" -User "Trusted User" -ExtendedRights Receive-As

Issue #3
Symptoms:
When opening Outlook as a user when the PC is joined to the domain, the on premises mailbox is opened instead of the Office 365 mailbox. When using a non-domain joined test PC, the Office 365 mailbox was opened.
Resolution:
This was caused by the way Exchange handles autodiscover. You can read more about the process here: http://msdn.microsoft.com/en-us/library/office/jj900169(v=exchg.150).aspx. We found a pair of scripts (ExportO365UserInfo.ps1 and Exchange2007MBtoMEU.ps1) available here: http://community.office365.com/en-us/wikis/exchange/845.aspx which allow you to convert the users active directory account into a mail enabled user that references Office 365 instead of a mailbox user in the on premises install.

This was my first experience with Office 365/Exchange Online. I was surprised at how complicated some of the migration steps were. With the whole ‘cloud-based’ self service model, I assumed that the migration path would be just a few clicks. With the handful of lessons learned from going through this process once, I would feel more comfortable doing another migration (but its not something I would volunteer for). I hope someone finds this post helpful.

Using ADSI to clear an attribute

I have many scripts that write information into Active Directory. Sometimes I need to do the opposite — remove or blank out an attribute.

All of the following examples will require a distinguishedName value in AD, so I will set a common variable here. You will need a valid distinguishedName defined in a varible like this if you want to follow along.

$distinguishedName = "CN=Brian Wuchner,OU=Test Users,DC=bwuch,DC=local"

The first test we will do is to update the displayName of the user. This is easy…we don’t care what the existing value is, we are just going to set it to whatever string value we want. We will do this by creating an object representing the Directory Entry of my user, updating the displayName attribute of that object and finally updating/committing that change back to the directory as follows.

1
2
3
$objUser = New-Object DirectoryServices.DirectoryEntry "LDAP://$distinguishedName"
$objUser.displayName="Wuchner, Brian"
$objUser.SetInfo()

That is a rather straight forward process. However, it becomes slightly more complex if we want to clear the attribute and make it null. One would think you could simply think you could set the displayName equal to $null using something like this:

1
2
$objUser = New-Object DirectoryServices.DirectoryEntry "LDAP://$distinguishedName"
$objUser.displayName=$null

However, if you try that you will most likely get a chance to see the following error message:

Exception setting "displayName": "Value cannot be null.

As a follow up, you might think “I’ll just set the attribute to an empty string” like this:

1
2
3
$objUser = New-Object DirectoryServices.DirectoryEntry "LDAP://$distinguishedName"
$objUser.displayName=""
$objUser.SetInfo()

The only problem with that…setinfo will fail with the following error.

setinfo : Exception calling "setinfo" with "0" argument(s): "The attribute syntax specified to the directory service is invalid.

Does that mean its time to give up? Are we just going to have to live with bad data in AD? Never!

There are at least two ways to clean this up. The first way is my favorite (I’ve been using it since vbscript). You simply need to refer to the Microsoft article HOW TO: Use ADSI to Set LDAP Directory Attributes and you’ll realize this can be accomplished with PutEx.

1
2
3
$objUser = New-Object DirectoryServices.DirectoryEntry "LDAP://$distinguishedName"
$objUser.PutEx(1, 'displayName', 0)
$objUser.SetInfo()

In the above example, we use the constant ADS_PROPERTY_CLEAR by placing a ‘1’ as the first argument in our method, then specifying the displayName as the attribute we want to clear, and finishing up by providing the desired value. Since we want the value to be clear, and we’ve already specified the ‘1’ to clear, we just need to specify something so the method has enough parameters. I use 0 just for fun.

Another option is to call the Remove method and specify the value we want to remove. The downside is the attribute MUST have a value or you get an error when trying to clear it. We can overcome that with a simple if statement like this:

1
2
3
4
5
$objUser = New-Object DirectoryServices.DirectoryEntry "LDAP://$distinguishedName"
if ($objUser.displayName) {
  $objUser.displayName.Remove("$($objUser.displayName)")
  $objUser.SetInfo()
}

It doesn’t matter which of the options I use, the end result is the same — a clear attribute. I hope this post helps if you ever need to clear out an attribute.

Compare two CSV files to trend mailbox growth

I had an interesting request the other day. A co-worker had two CSV files containing mailbox information from the same Exchange server, but from different parts of the day. He wanted to know if there was any way to find which users had the most growth in their mailbox size.

Here is an example of how to get mailbox size for all users on the same Exchange server:

Get-MailboxStatistics -Server EXCHANGE01 | 
Select-Object -Property DisplayName, 
@{N="SizeMB";E={$_.TotalItemSize.Value.ToMB()}}, Identity | 
Export-Csv file2.csv -NoTypeInformation

The first thought that came to mind had involved Group-Object and Measure-Object. I threw something together that got the job done, but decided to clean it up a bit and thought I’d share it here.

1
2
3
4
5
6
7
Import-Csv file1.csv, file2.csv | Group-Object -Property Identity | %{
      $thisDetail = $_ | Select-Object -ExpandProperty Group| Measure-Object SizeMB -Maximum -Minimum
      New-Object psobject -Property @{
            DisplayName = [string]($_.Group[0].DisplayName)
            GrowthMB = [float]($thisDetail.Maximum - $thisDetail.Minimum)
      } | Select DisplayName, GrowthMB
} | Sort-Object GrowthMB -Descending | Select-Object -First 10

Hope someone else finds this helpful!

Improving performance with Get-MailboxFolderStatistics

In a previous post I mentioned some performance improvements I made in an Exchange script that used Get-MailboxFolderStatistics. A portion of this script selected the size (in megabytes) of the top 5 folders in each users mailbox. I made a subtle change to the code that saved about 35 minutes on the execution time of the script.

For purposes of the following examples, I measured the execution time of this block of code using my Exchange mailbox. My mailbox has about 250 folders and is about 1.4GB in size.

Previous version of code takes 0.2329 seconds to complete.

1
2
3
4
5
6
7
measure-command {
# Previous
$stats = Get-MailboxFolderStatistics $mbx.identity
$folders = $stats | Select-Object folderpath,@{name="Size(MB)";Expression={$_.FolderAndSubfolderSize.toMB()}}
$folders = $folders | Where-Object {$_."Size(MB)" -ne 0} | Sort-Object -Property "Size(MB)" -Descending | Select-Object -First 5
$folders
}

Updated version of code takes 0.1418 seconds to complete.

1
2
3
4
5
6
7
measure-command {
# Updated
$stats = Get-MailboxFolderStatistics $mbx.identity
$folders = $stats | Where-Object {$_.FolderAndSubfolderSize -gt 1048576} | Sort-Object -Property "FolderAndSubfolderSize" -Descending | Select-Object -First 5
$folders = $folders | Select-Object folderpath,@{name="Size(MB)";Expression={$_.FolderAndSubfolderSize.toMB()}}
$folders
}

On my account, this is a savings of only 0.0911 seconds. That doesn’t sound like enough savings to implement, does it? Actually, if you take my mailbox example and expand it by 40,000 users it would be a savings of about 1 hour. My mailbox is likely larger than the average user, which explains why the savings is closer to 35 minutes instead of 60.

Can you notice the change in the code? It is subtle…nearly the same code/commands are being used — only the order was changed slightly.

In the previous version of the script, all of the folders were selected into an object and the ToMB method was called on the FolderAndSubfolderSize property. The next line of code selects only the first five non-zero entries.

In the updated version of the code, only the first five non-zero entries were added to the object. The next line of code then called the ToMB method on only the first five folders. In my example, this is a savings of about 245 calls on the ToMB method.

Working with the lastLogon attribute in PowerShell

I have been spending a lot of time working on Powershell scripts. Specifically, I have been re-writing many VB scripts and processes into more efficient powershell versions. In my last post I mentioned that I planed to share how I made a task that took 8 hours to execute run in only 28 minutes. There are two parts to this efficiency — Start-Job and Group-Object. In a future post I’ll write more about the Start-Job functionality, but the greatest benefit was from Group-Object — and thats what I’ll focus on in this post.

The task I re-wrote looked at about 50,000 user accounts and collected many details about the accounts for auditing purposes. Due to some constraints, I needed to use the lastLogon attribute instead of lastLogonTimeStamp. You can read more about the constraints here: http://www.rlmueller.net/Last%20Logon.htm, but this line pretty much sums it up: Because the lastLogon attribute is not replicated in Active Directory, a different value can be stored in the copy of Active Directory on each Domain Controller. In VB script, I was using an LDAP bind to each domain controller for each user account and then evaluated the lastLogon attribute, which was very inefficient.

Here is the powershell version of this code, which is much more efficient and flexible (as you can get the last login time from each/all domain controllers very easy).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$lastLogons = @()
$domainControllers | %{ # I used an LDAP query for computers with primaryGroupID=516 to get a list of domain controllers distinguished names
    $objDC = New-Object DirectoryServices.DirectoryEntry "LDAP://$_"
    $dcName = $objDC.dnsHostName.ToString()
    $de = New-Object System.DirectoryServices.DirectoryEntry ("LDAP://$dcName")
    $Rech = New-Object System.DirectoryServices.DirectorySearcher($de)
    $Rech.filter = "(&(objectCategory=User)(!objectClass=Computer)(lastLogon>=1))"
    $Rech.SearchScope = "subtree"
    $Rech.sizelimit = "90000"
    $Rech.pagesize = "90000"
    $Rech.PropertiesToLoad.Add("distinguishedName");
    $Rech.PropertiesToLoad.Add("lastLogon");
    $liste = $Rech.FindAll()
    $lastLogons += ($liste | select @{n='DN';e={$_.properties.distinguishedname}},@{n='LastLogon';e={$_.properties.lastlogon}}, @{n='DC';e={$dcName}})
}
 $groupedLastLogin = $lastLogons | Group-Object -Property DN -AsHashTable -AsString
 
# To get an individuals last login information, just select it from the hash table
$trueLastLogin = $groupedLastLogin.Item($objUser.distinguishedName.ToString()) | sort LastLogon -Descending | select -First 1
$lastDC = $trueLastLogin.DC
$lastLogonTrueDate = [datetime]::FromFileTimeUTC($trueLastLogin.lastLogon)

After creating this code, I attempted to select user information from the $lastLogons variable. This was very slow — even slower than the previous VB script that did excessive LDAP binds. The efficiency in this script was gained by grouping the objects into a hash table ($groupedLastLogin) by distinguished name, and then accessing the specific key value as needed.

I know several of my posts have strayed from the normal VMware/vSphere/PowerCLI topics recently, but I have some plans to get back on track early next year. Please stay tuned!