Combine Output With Add-Member

Sometime in the year 1793 (the exact date is unknown), the Italian artist Leonardo Da Vinci was strolling through the newly-opened Musée de Louvre when he happened upon his famous portrait of the Mona Lisa. "Mamma mia!" he exclaimed upon looking at the painting. "I forgot to paint the eyebrows!"

There's a good lesson behind that story, although we should mention the fact that not everyone accepts it as a true story; for example, many scholars have claimed that, seeing as how he died in the year 1519, it's very unlikely Leonardo Da Vinci would have visited the Louvre in 1793. And, of course, there's also the inconvenient fact that the Mona Lisa actually did have eyebrows; it’s just that those eyebrows are no longer visible, either because they've simply faded over time or because they were erased by someone attempting to clean the painting.

Minor quibbles like those aside, however, the lesson of the story remains as valuable today as it was in 1793: everyone, even a genius like Leonardo Da Vinci, sometimes looks back on his creation and thinks, "You know, given the chance, I would have done things a little differently." Like they say, hindsight is foresight.

Note. To the best of our knowledge, Leonardo Da Vinci was not the one who said, "Hindsight is foresight." However, he did say "Although nature commences with reason and ends in experience it is necessary for us to do the opposite, that is to commence with experience and from this to proceed to investigate the reason."

And no, we're not totally sure what he meant by that, either. But, then again, we don't speak Italian.

So what does any of that have to do with Microsoft Lync Server 2010? Well, for one thing, we like to consider Lync Server the Mona Lisa of the unified communications world. For another, and to be perfectly honest, after we released Lync Server to manufacturing we took a closer look at it and exclaimed, "Mamma mia! We forgot to paint the eyebrows!"

Of course, after we calmed down we realized that we were never supposed to paint eyebrows on Lync Server 2010 in the first place. But then we realized yet another thing. As you probably know, in Lync Server's implementation of Windows PowerShell there are two different cmdlets that return user information: Get-CsUser and Get-CsAdUser. We won't go into much detail about the differences between these two cmdlets; for that, see the article Get-CsUser: The Real Story. What we will do, however, is note that, although there is some overlap between the two cmdlets, Get-CsAdUser primarily brings back information for generic Active Directory attributes such as department, job title, and home phone number; in other words, attributes shared by all Active Directory user accounts regardless of whether or not those accounts have been enabled for Lync Server. In comparison, Get-CsUser returns information about Lync Server-specific attributes, like the dial plan or the external access policy that has been assigned to the user. These are attributes which are only part of user accounts that have been enabled for Lync Server. East is east, west is west, and ne'er the twain meet.

Note. Yes, it would be useful to be able to see a table that compares the attribute values returned by the two cmdlets, wouldn't it? Wonder where we could find one of those ….

If you've ever seen the Mona Lisa in person (and you should: it's actually pretty cool) then you know that the fact that she doesn't have eyebrows is no big deal; to be honest, you don't even notice that she doesn't have any eyebrows. And, in general, the same thing is true of Get-CsAdUser and Get-CsUser: most of the time you won't even notice that you have to use two different cmdlets to return user information. Two cmdlets? Big deal.

Like we said, most of the time you won't even notice that you have to use two different cmdlets to return user information. However, there are exceptions to this general rule. For example, suppose you'd like to return the following information for each user who's been enabled for Lync Server:

· Display name

· Department

· Job title

· Registrar pool

· Dial plan

So how are we going to do that? Well, the display name is easy: after all, both Get-CsUser and Get-CsAdUser can return the display name. The other four attributes pose a problem, however, and a big problem at that. Get-CsAdUser can return the department and job title, but not the Registrar pool or dial plan. Meanwhile, Get-CsUser can return the Registrar pool and dial plan, but not the department or job title. Uh-oh ….

So do we have a problem here? Sure looks that way, doesn't it? But, as Leonardo Da Vinci once said, "Marriage is like putting your hand into a bag of snakes in the hope of pulling out an eel." With that in mind, let's reach into our bag of Windows PowerShell tricks and see if we can pull out an eel.

Before we go searching for that eel, however, let's stop to clarify the problem we're having here, just to make sure we're all on the same page. (You should be on one of the Lync Server PowerShell blog pages right now, the one that keeps going on and on about Leonardo Da Vinci.) Suppose we had a hypothetical cmdlet named Get-FavoriteColor and suppose that cmdlet returned information like this:

Identity : Ken Myer

FavoriteColor : Orange

Suppose we also have a second hypothetical cmdlet – Get-FavoriteNumber – that returns information like this:

Identity : Ken Myer

FavoriteNumber : 13

What we'd like to have is way to combine the output of these two cmdlets, like so:

Identity : Ken Myer

FavoriteColor : Orange

FavoriteNumber : 13

Can we do that? You bet we can. We'll have to write a little script to do it, but it will definitely be a little script, and it won't be hard to do at all. So does that mean we can also find a way to combine the output of Get-CsUser and Get-CsAdUser? See for yourself:

$userIDs = Get-CsUser | Select-Object Identity, RegistrarPool, DialPlan

foreach ($user in $userIDs)

    {

        $x = Get-CsAdUser -Identity $user.Identity

        $x | Add-Member -MemberType NoteProperty -Name RegistrarPool -Value $user.RegistrarPool

        $x | Add-Member -MemberType NoteProperty -Name DialPlan -Value $user.DialPlan

        $x | Select-Object DisplayName, Department, Title, RegistrarPool, DialPlan

    }

Let's take a look at this script line-by-line and see if we can explain how it works. As you no doubt recall, we want to run a command that returns the following data for each user who's been enabled for Lync Server:

· Display name

· Department

· Job title

· Registrar pool

· Dial plan

With that in mind, our first line of code uses the Get-CsUser cmdlet to return the Identity, RegistrarPool, and DialPlan attributes for each Lync Server-enabled user, then stores all that information in a variable named $userIDs:

$userIDs = Get-CsUser | Select-Object Identity, RegistrarPool, DialPlan

 

Why did we limit the returned data to the attributes Identity, RegistrarPool, and DialPlan? Well, to begin with, we need the Identity to help us differentiate one user from another. Second, we need the RegistrarPool and DialPlan attributes because only Get-CsUser returns that information for us. The other attributes we're interested in – DisplayName, Department, and Title – can all be retrieved using Get-CsAdUser.

Let's pretend that we have only two users who have been enabled for Lync Server. That means that $userIDs is going to contain information similar to this:

Identity

RegistrarPool

DialPlan

CN=Ken Myer,OU=Finance,DC=litwareinc,DC=com

atl-cs-001.litwareinc.com

Redmond

CN=Pilar Ackerman,OU=Finance,DC=litwareinc,DC=com

atl-cs-001.litwareinc.com

Paris

Got that? Good. In that case, our next step is to set up a foreach loop that loops through each of the user accounts stored in $userIDs. That's what this line of code is for:

foreach ($user in $userIDs)

Now it's time to roll up our sleeves and get to work. Inside our foreach loop, the first thing we do is execute this line of code:

$x = Get-CsAdUser -Identity $user.Identity

What are we doing here? Well, what we're doing here is calling the Get-CsAdUser cmdlet in order to return information for a user. Which user? Well, because this is our first time through the loop, we're retrieving information for the first user (Ken Myer) whose account information is stored in $userIDs; that's why we use $user.Identity as the parameter value for the Identity parameter. When this command finishes running, we'll have two sets of information for Ken Myer: we'll have his Identity, RegistrarPool, and DialPlan tucked away in the variable $user, and we'll have all his "generic" Active Directory data tucked away in the variable $x.

Now that's pretty impressive, except for one thing: that's not the least bit impressive at all. Why not? Well, we currently have Ken Myer's data in two different places. But whoop-dee-doo: we've always had Ken Myer's data in two different places. What we really want to do is combine all that data into a single data source. And guess what? That's what these two lines of code are for:

$x | Add-Member -MemberType NoteProperty -Name RegistrarPool -Value $user.RegistrarPool

$x | Add-Member -MemberType NoteProperty -Name DialPlan -Value $user.DialPlan

Let's take a closer look at what these two lines of code do. In the first line, we're taking the variable $x (which contains Ken Myer's generic Active Directory information) and piping that object to the Add-Member cmdlet. If you aren't familiar with the Add-Member cmdlet, well, you should be: it's pretty dang cool. Among other things, Add-Member enables you to add a property of your own (any property you want) to an object. As we've already determined (even if we haven't spelled it out quite this technically), the Microsoft.Rtc.Management.ADConnect.Schema.ADUser object returned by Get-CsAdUser does not include the attributes RegistrarPool or DialPlan. We can't return the Registrar pool or dial plan for a user by using Get-CsAdUser; that's because Get-CsAdUser doesn't support either of those attributes.

Note. In case you're wondering, Get-CsUser returns instances of the Microsoft.Rtc.Management.ADConnect.Schema.OCSADUser object.

In case you were wondering.

Because of this, and as Leonardo Da Vinci so memorably put it, "If an object does not support a specific property or attribute one must use the Add-Member cmdlet to add the property or attribute to the object." Get-CsAdUser doesn't support the RegistrarPool or the DialPlan attributes? That's fine; then we'll make Get-CsAdUser support the RegistrarPool and the DialPlan attributes.

In order to do that, we pipe $x to Add-Member, and then include the following three parameters and parameter values:

· MemberType. This parameter tells Add-Member what it is we're adding to the object. The NoteProperty member type indicates that we're adding a data container to the object, sort of like adding a field to a database table.

· Name. This parameter is simply the name we want to give the new property. Get-CsUser calls this particular property RegistrarPool; that was good enough for us, so we assigned the value RegistrarPool to the Name parameter.

· Value. This parameter specifies the value to be assigned to the new property. This is the key right here. After all, we don't want to assign any old value to this property; instead, we want to assign the property the value of the RegistrarPool attribute we retrieved using Get-CsUser. Fortunately that's easy to do: as you recall, that information is stored in $user.RegistrarPool. Therefore, we use $user.RegistrarPool as our parameter value. Which actually makes sense, right?

So what's the net effect of all this? The net effect is that $x, the variable containing the generic Active Directory information for Ken Myer, now has a new property and a new property value, both copied from the data returned by Get-CsUser. Filtering out all the attributes we don't care about, that means $x looks like this:

DisplayName

Department

Title

RegistrarPool

Ken Myer

Finance

Manager

atl-cs-001.litwareinc.com

In other words, we've successfully combined the output of Get-CsAdUser and Get-CsUser. And in our next line of code we'll do the exact same thing, this time adding the DialPlan attribute to $x:

$x | Add-Member -MemberType NoteProperty -Name DialPlan -Value $user.DialPlan

That means that $x should now look something like this:

DisplayName

Department

Title

RegistrarPool

DialPlan

Ken Myer

Finance

Manager

atl-cs-001.litwareinc.com

Redmond

Wow. That is cool, isn't it?

OK. At the moment, $x contains all the Active Directory attribute values returned by Get-CsAdUser along with the two new attribute values – RegistrarPool and DialPlan – that we just added. Of course, we don't actually want all the attribute values; we only want to see five of those values displayed on screen. Therefore, we use this command, and the Select-Object cmdlet, to filter out everything except the attributes of interest:

$x | Select-Object DisplayName, Department, Title, RegistrarPool, DialPlan

When we run this command we'll see data like this displayed onscreen:

DisplayName : Ken Myer

Department : Finance

Title : Manager

RegistrarPool : atl-cs-001.litwareinc.com

DialPlan : Redmond

And then we're going to go back to the top of the foreach loop and repeat the process for the next user (Pilar Ackerman) who has account information stored in $userIDs. By the time this second iteration completes our screen is going to look like this:

DisplayName : Ken Myer

Department : Finance

Title : Manager

RegistrarPool : atl-cs-001.litwareinc.com

DialPlan : Redmond

DisplayName : Pilar Ackerman

Department : Shipping

Title : Receiving Clerk

RegistrarPool : atl-cs-001.litwareinc.com

DialPlan : Paris

In other words, we've managed to combine data retrieved from Get-CsAdUser and data retrieved from Get-CsUser. Mamma mia!

Note. In case you're wondering, "Mamma mia" translates as "Mother of mine" or, more simply, "My mother."

To recap, we first retrieved information by using Get-CsUser, then took selected bits of that information and added it to the data retrieved by using Get-CsAdUser. That approach works for two reasons: 1) both cmdlets use the same user Identity, which means we can be sure that Get-CsUser and Get-CsAdUser are returning information for the same person; and, 2) any person who has been enabled for Lync Server will also have an Active Directory user account. Why do we care about reason 2? Well, reason 2 is important because it means that something will always come back when we call Get-CsAdUser. Suppose we have a third user – April Reagan – who has a valid user account but has not yet been enabled for Lync Server. Will April cause problems with our script? Nope; that's because we first return all the Lync Server-enabled users and then go searching for their corresponding Active Directory accounts, accounts we know exist. Because April doesn't have a Lync Server account we never deal with her at all.

But suppose we flipped this around, suppose we first retrieved information using Get-CsAdUser and then tried to find the corresponding Lync Server accounts. In that case, we are going to have problems; after all, April doesn't have a Lync Server account. In turn, that means that we'll get an error message each time we call Get-CsUser and each time we call Add-Member for April Reagan. If you call get-CsUser and the user doesn't exist, you'll get an error message.

Note. Of course, there is a way to work around those error messages: just include the ErrorAction parameter and the parameter value SilentlyContinue when calling cmdlets:

$x = Get-CsUser -Identity $user.Identity –ErrorAction SilentlyContinue

The preceding command won't return any data for April Reagan; as we noted, she doesn't have a Lync Server account. However, it also won't print an error message to the screen. That's because SilentlyContinue means just what the name suggests: just keep going, without saying a word.

Bonus Script: The Real Da Vinci Code

In the best-selling book (and hit movie) The Da Vinci Code by Dan Brown, much of the action centers around the cryptex, a cylindrical device created by Leonardo Da Vinci for encrypting messages. In particular, everyone in the movie is trying to decrypt – or prevent others from decrypting – a secret message that promises to rock the entire world.

Well, as it turns out, the people in the book (and the movie) got it all wrong. As it turns out, Leonardo's secret message will rock the world, but it has nothing to do with the origins of Christianity. Instead, it's a script that combines output from Get-CsUser and Get-CsAdUser and displays that data in a table. As you might recall, the script we showed you earlier displays data in list format, like this:

DisplayName : Ken Myer

Department : Finance

Title : Manager

RegistrarPool : atl-cs-001.litwareinc.com

DialPlan : Redmond

Ah, but Leonardo's script displays that same data in tabular format, like this:

DisplayName Department Title RegistrarPool DialPlan

Ken Myer Finance Manager atl-cs-001.litwareinc.com Redmond

No wonder people call him a genius.

We won't bother explaining how the script works; we'll just mention that, as the data is retrieved, it's stored in an array named $data. After all the data has been retrieved and combined, that array is then piped to the Format-Table cmdlet, like so:

$data | Format-Table

At any rate, here's the real Da Vinci code:

$data = @()

$userIDs = Get-CsUser | Select-Object Identity, RegistrarPool, DialPlan

foreach ($user in $userIDs)

    {

        $x = Get-CsAdUser -Identity $user.Identity

        $x | Add-Member -MemberType NoteProperty -Name RegistrarPool -Value $user.RegistrarPool

  $x | Add-Member -MemberType NoteProperty -Name DialPlan -Value $user.DialPlan

        $data += ($x | Select-Object DisplayName, Department, Title, RegistrarPool, DialPlan)

    }

$data | Format-Table

Note. Although The Da Vinci Code has been criticized for historical inaccuracies and misrepresentations, we like to believe that the book is historically accurate. After all, The Da Vinci Code states that Leonardo Da Vinci was a former Grand Master of the Priory of Sion, an organization that was actually created in France in 1956. Our feeling is this: if Leonardo Da Vinci could be the Grant Master of the Priory of Sion in 1956, well, that lends credibility to our belief that he was visiting the Louvre in 1793.

Food for thought, eh?