Chris Whitfield's profileScripts N' ThingsBlogListsGuestbookMore Tools Help

Scripts N' Things

PowerShell is your friend Mkay
May 05

Managing ISA With PowerShell - Primer

So, in the recent past, I had a need to build out a new ISA array for a special project. Now, you might be thinking at this point 'OK, sure, but why PowerShell? Isn't that overkill?' and you would be right under normal circumstances. The thing that made this particular job a tad more painful was the need to generate 50 network definitions and access rules in order to accommodate the specific use to which ISA would be put. Now, I don't know about you, but the need to generate 50 rules, each with minor yet predictable variations, just screams script.

So now maybe you are asking 'But why reinvent the wheel since there are tons of examples out there using VBScript that do pretty much the same thing right?' and the answer is…yes and no. There are examples out there for creating networks and creating rules both (see http://www.isascripts.org for some really great examples), but I needed to generate 50 of them and I didn't necessarily want to generate a script, I just wanted to DO it. In cases like this especially, even when working with COM objects, PoSh is still the best tool for the job…or so my thinking went. I quickly discovered that, as great as ISA is as a product, its COM interface could use some work.

So we'll start this journey at the beginning by initializing the COM object in PoSh, which can be done as so:

PS C:\>$root = New-Object –com "FPC.Root" –Strict

Now you will notice that I used the –Strict switch on my object. Running the cmdlet through the help indicates that this is done to cause an alert to be raised when the COM object uses an interop assembly so that you can more readily distinguish between actual COM objects and COM-callable wrappers for .NET objects. The reasons for needing this distinction in this case are not entirely clear to me, not being a programmer, but this appears to be the preferred method in all examples I have found, so we'll go with it.

Next thing we need to do is set up bindings for a couple of the elements that we will be working with, namely networks, rule elements, and rules within an array. If we pass our $root object to Get-Member, we can see that the best method to use is likely going to be the 'GetContainingArray' method like so:

PS C:\>$array = $root.GetContainingArray()

This works fine when you have only a single array, but I don't think this will bind properly to what we need if you have multiple arrays. In that case, it might be better to bind in a slightly different manner using the name of the array like so:

PS C:\>$array = $root.arrays | Where-Object {$_.name –like 'MyArray'}

My preference in this instance will be to do a 'Do-Until' loop, so we need a simple counter place holder that we will go ahead and start at 1:

PS C:\>i = 1

Don't forget that, before every new Do – Until loop that uses $i, you will first need to set $i back to equal 1 before starting your loop. Otherwise, you will not loop since the value of $i would still be greater than 50 and would stop. Alternately you could probably use different variables for each loop, but I think it's just as easy the other way.

Next we want to create our networks. If we pass $networks to Get-Member, we quickly see that there is no obvious method for 'Adding' a new network. This is what I was referring to earlier on the COM interface needing a bit of work. In my mind, a properly built COM object would provide what you need via Get-Member even if it is an inherited item from a parent. Unfortunately, even checking the parent objects doesn't reveal a method for adding a new network, so how do we do it?

For this I had to resort to downloading and combing through the ISA SDK. True, this is available online via MSDN, but it's much easier to search for what I am looking for with the CHM format. In addition to this, not all of the samples are available in full format online so that is another reason to have the SDK installed since these examples are then installed locally.

After reviewing the CHM, it turns out that the majority of COM interfaces on ISA have an 'Add' method even when it isn't otherwise reflected via Get-Member. Why this is the case is beyond me, but we'll roll with it. When calling the 'Add' method for the COM objects associated with ISA, it always returns an instance of the new object for further manipulation. One of the nice aspects of this is that none of the changes are committed until you call the 'Save' method. This is helpful because we need to specify all the settings we want for our new network before we commit (not that we have commitment issues, we just like to work up to it gently). Since an object is returned, we will want to bind it to a variable so we can work with it which would look something like this:

PS C:\>$net = $networks.Add(("MyNetwork" + $i))

Now we have the variable $net bound to our new network which is called MyNetwork plus whatever the current value of 'i' is at the time. You may have noticed that I used double parenthesis (if you didn't its ok, I don't expect you to know everything this time…but next time there will be a test). The reason for this is a matter of execution priorities within PoSh. I want it to figure out what '"MyNetwork" + i' is before it executes the 'Add' method since the method likely lacks the processing instructions to do it after. Methods are simple…they want what they want when they want it and only the way they want it (kinda like my two year old) so we don't want to startle or scare our little method by asking it to do odd things. Thus, we do the processing first and provide only the results. I could have done this outside of the method call, but then I would have to assign it to a variable and increase the number of lines in my script. Doing it this way instead has the same effect, but requires less effort in my opinion.

So now we have a new object called $net. We can pass this to Get-Member, and it provides us with a handy little list of the remaining properties to be set. Each item that we care about needs to be set before we commit the changes like so:

PS C:\>$net.AutoDiscoveryPort = "80"

PS C:\>$net.Description = ("Test isolation network" + $i)

PS C:\>$net.EnableAutoDiscoveryPort = $true

PS C:\>$net.EnableFirewallClients = $true

PS C:\>$net.EnableWebProxyClients = $true

PS C:\>$net.IpRangeSet.IP_From = ("192.168." + $i + ".1")

PS C:\>$net.IpRangeSet.IP_To = ("192.168." + $i + ".50")

PS C:\>$net.LDT = "mydomain.com"

 

Of course, if you ran $net through Get-Member, you will probably notice that I didn't specify every property and that some items even have several sub elements. As to why some and not others, some items have a default setting that was already what I needed while others required changing and yet others cannot be set. Items that cannot be set are usually obvious via the output of Get-Member because they indicate only {get} and not {set} which indicates they are read only properties. This could be because the property is managed by the system, or it could be because these properties actually contain 'enumeration collections' which are themselves objects and require you to set the sub objects rather than the property itself. This is the case with the 'IpRangeSet' property. Get-Member showed this property with only the ability to {get} and so running $net.IpRangeSet through Get-Member showed me the sub object properties that needed to be set. This is also evident in the SDK help file as the property is shown as read only, but derived from a sub object whose properties you can set. Others might indicate that the property is completely read only without a reference to another object and these you just have to accept the defaults for. In my case, the above properties were all I needed to set, but you might need others so I would strongly suggest grabbing the SDK from the Microsoft downloads site if you think you need other properties (though you can always sift through each property using Get-Member as well…but my way is shorter…trust me). The last thing I need to do is to save my changes like so:

PS C:\>$net.Save()

It's important to note that, while this network is available now to ISA to use in the creation of rules and the like, it isn't actually active yet. If you have worked with ISA for any length of time, you will probably realize that this is because we haven't applied the changes yet (and no, save is NOT the same as apply).

So, all of the above goes into a Do – Until loop, of course with an increment on each loop for $i making this whole portion of the script look like this:

PS C:\>Do {

> $ net = $networks.Add(("MyNetwork" + $i))

> $net.AutoDiscoveryPort = "80"

> $net.Description = ("Test isolation network" + $i)

> $net.EnableAutoDiscoveryPort = $true

> $net.EnableFirewallClients = $true

> $net.EnableWebProxyClients = $true

> $net.IpRangeSet.IP_From = ("192.168." + $i + ".1")

> $net.IpRangeSet.IP_To = ("192.168." + $i + ".50")

> $net.LDT = "mydomain.com"

> $net.Save()

> $i ++

> } Until ($i –gt 50)

 

The next part, of course, is to define the network set and routing rules for these networks. Rather than loop this though, we can simply make them the next logical step in our script like so:

PS C:\> $netset = $array.NetworkConfiguration.NetworkSets.Add("All Test Iso Networks")

PS C:\> $netset.Networks.Add("External", 0)

PS C:\> $netset.Networks.Add("Internal", 0)

PS C:\> $netset.Networks.Add("Local Host", 0)

PS C:\> $netset.Networks.Add("Quarantined VPN Clients", 0)

PS C:\> $netset.Networks.Add("VPN Clients", 0)

PS C:\> $netset.Save()

 

You may have noticed that we didn't add any of the MyNetwork networks we created before. We could have done this via another Do – Until loop, but we don't have to go to all that trouble. Instead, we use the all-inclusive option for creating a network set and specify only the networks we want Excluded from this set. We do this by calling the 'Add' method (since we are referencing an existing object) and specifying the value for the Name property that represents the network we wish to exclude, and then setting either a 0 to exclude or a 1 to include the network. Since the only networks that exist on this ISA array other than the defaults are the ones I created, this represents a much smaller list than adding in all 50 of my networks I created.

Next, we have to define the routing relationship between this network and other networks. We do this almost the same way we did with the NetworkSet as follows:

PS C:\> $netrule = $array.NetworkConfiguration.NetworkRules.Add("MyNetwork to Internal")

PS C:\> $netrule.DestinationSelectionIPs.Networks.Add("Internal", 1)

PS C:\> $netrule.DestinationSelectionIPs.Networks.Add("VPN Clients", 1)

PS C:\> $netrule.SourceSelectionIPs.Networks.Add("All Test Iso Networks", 1)

PS C:\> $netrule.RoutingType = 0

PS C:\> $netrule.Save()

 

Since we already had the network set available, we simply added it to the source and our other 'Internal' networks to the destination. These network rules cover traffic in both directions, so there is no need to create any additional rules for traffic headed the other way. The only other item we set was the RoutingType, which controls whether the network uses a NAT or Route style when navigating traffic. Setting a value of '0' sets the type to Route while a value of '1' indicates NAT. The way I figured this out was to review the FpcCfg.vbs available in the 'inc' directory where you have installed the SDK for ISA. It provides value options for just about every item you might need to set. This was cross referenced with the information provided in the help file which indicates only that a value of either fpcRoute (0) or fpcNAT (1) needs to be set. I searched the vbs for those values to find out what they translated to.

The next piece, in my case, was to populate user sets that were tied to groups in AD. This was so that I could restrict access to each network to only those people who had been granted access. This process is fairly simple as well…except for the binding to the right AD user part which is slightly annoying. In my case, I had pre-created 50 AD groups with the same name, but incremented numbers on the end just to keep things consistent. I did that with PoSh as well using the Quest AD cmdlets, but will not cover that step in this post. The general process for adding each user set looks like this:

PS C:\> $uset = $array.RuleElements.UserSets.Add(("MyNetwork" + $i))

PS C:\> $uset.Accounts.Add(("mydomain\MyNetworkGrp" + $i))

PS C:\> $uset.Save()

 

As before, you will want to stick that into a loop like we did with the creation of the networks themselves. You could be asking yourself why we didn't go ahead and create all of this inside of one loop at this point. Some things, such as the networks and usersets, we probably actually could, but others, like the Access Rules themselves and the network set, require the creation of the other elements first.

So now we are up to the next to last part, which is creating the Access Rules to allow traffic to actually flow between the networks. You see, it isn't enough to simply specify a network has a routed or NATed relationship, you also have to specify what traffic is allowed to flow for travel between any two networks unless they use the same interface on ISA. In my case, it was fairly simple as all I wanted to do was allow inbound access from the internal networks to the desired MyNetwork based upon being in the correct AD group. You will also likely want to create a rule that allows your individual networks outside for web browsing or whatever, but I will not demonstrate that here on this post since the general process will be covered by the add rules loop I am about to show you. The only difference is that you will add the Network set instead of an individual network and the 'All Users' user set instead of a specific AD group user set. The process for creating the new access rule is as follows:

PS C:\> $accrule = $array.ArrayPolicy.PolicyRules.Add(("Int to MyNetwork" + $i))

PS C:\> $accrule.SourceSelectionIPs.Networks.Add("Internal", 0)

PS C:\> $accrule.AccessProperties.DestinationSelectionIPs.Networks.Add(("MyNetwork" + $i), 0)

PS C:\> $accrule.AccessProperties.UserSets.Add(("MyNetwork" + $i), 0)

PS C:\> $accrule.Action = 0

PS C:\> $accrule.Save()

PS C:\> $i ++

 

As before, the above will be enclosed in a Do – Until loop just like the first one. As before, I only set those properties that I felt needed to be set for my purposes and there are still many more that could be set. As shown, the above will generate an access rule that allows unrestricted access on any protocol for users on the internal network who are a member of the correct group for the chosen destination network.

The very last step is to commit all our changes and additions to ISA to make them live which we do like this:

PS C:\> $root.ApplyChanges()

In this instance, we have to commit or dump these changes at the root level because we are accessing an array. If you are using ISA standard, you could actually do this using the $array instead of $root I believe.

Anyway, I hope this post proves useful to someone and, as always, please feel free to reply with your suggestions for improvements or questions if you are stuck.

Merddyn

February 05

Quickie Inventory Example

So, after I posted the prior post, I realized that in my sleep deprived state I had overwritten this one. I'll skip the boring bits and just provide the script.

 

Function Get-QuickInv {

    BEGIN{

        Write-Host "Getting inventory from selected systems"

        $outobj = @()

    }

    PROCESS{

        $csinfo = gwmi -class Win32_ComputerSystem -computer $_ | select name,manufacturer,model,numberofprocessors

        $procinfo = gwmi -class Win32_Processor -computer $_ | select -first 1 currentclockspeed

        $diskinfo = gwmi -class Win32_LogicalDisk -computer $_ | where {$_.DriveType -eq 3} | select name,size,freespace

        $nicinfo = gwmi -class Win32_NetworkAdapter -computer $_ | where {$_.AdapterTypeID -eq 0 -and $_.PhysicalAdapter -eq $True}

        

        $output = New-Object System.Object

        $output | Add-Member -type NoteProperty -name System -value $csinfo.Name

        $output | Add-Member -type NoteProperty -name Manufacturer -value $csinfo.Manufacturer

        $output | Add-Member -type NoteProperty -name Model -value $csinfo.Model

        $output | Add-Member -type NoteProperty -name NumProcs -value $csinfo.NumberOfProcessors

        $output | Add-Member -type NoteProperty -name ClockSpeed -value $procinfo.CurrentClockSpeed

        $output | Add-Member -type NoteProperty -name NumNICs -value ($nicinfo.count)

        foreach($disk in $diskinfo){

            $output | Add-Member -type NoteProperty -name ("Drive-" + $disk.name) -value $disk.name

            $output | Add-Member -type NoteProperty -name ("Size-" + $disk.name) -value ([math]::truncate($disk.size/1mb))

            $output | Add-Member -type NoteProperty -name ("FreeSpace-" + $disk.name) -value ([math]::truncate($disk.freespace/1mb))

        }

    

        $outobj += $output

    }

    END{

        Write-Host "Inventory complete."

        Write-Output $outobj

    }

}

 

Enjoy!

 

Merddyn

Quickie Little Exchange 2003 Inventory Example

(Edited)

So the same colleague of mine hit me up for a little more help. He needed some specific information in a nice readable format for 75 Exchange 2003 servers. Some of this info could be gotten from the Exchange 2003 PowerPak for PowerGUI, but not cleanly associated with a hierarchy. Some of the info was available via WMI, but not all of it. The only place where almost everything could be found was AD. The script I made for him is below, though keep in mind this was put together while I was suffering from a bout of insomnia, so I may have the formatting off, and my little test environment only has a single Exchange server of course, but it should work to get all the info, just not sure how it will format. I expect it will break though, since I don't think I looped things effectively, but I'm finally tired so am going to bed. I will try to post an update some time tomorrow when I fix it. This new approach uses XML instead of the way I was doing it before, which was to create a custom object and try and stuff everything in there. The problem with this was that child objects were not readily associated with their parent objects which was the whole point, otherwise the PowerGUI PowerPak would have worked just fine.

There are some theoretical downsides to this (more like manual steps since I didn't put it into the script yet) in that you will want to tie the output to a variable like so:

56# [xml]$testing = Get-QuickExchInv

After doing this, you can save to a file like this:

57# $testing.save("C:\testtest.xml")

This will save the output of the command to an XML file on your C: drive called 'testtest.xml' so you can review it as well as easily re-import it to work with it as an object. I tried simply outputting to CSV or XML via the cmdlets, but this didn't go well. The CSV didn't enumerate everything and the XML changed all the greater than and less than symbols to other text which broke the whole XML idea.

Function Get-QuickExchInv {

        

#requires -version 2

	Write-Host "Getting inventory from selected systems"

        $root = New-Object system.directoryservices.directoryentry("LDAP://RootDSE")

        $msexch = New-Object system.DirectoryServices.DirectoryEntry("LDAP://CN=Microsoft Exchange,CN=Services,CN=Configuration," + $root.defaultnamingcontext)

        $exchorgs = $msexch.children | Where-Object {$_.objectClass -contains 'msExchOrganizationContainer'}

        

        $output = "<ExchangeOrg>"

        $output += "<Organization Name=""$(($exchorgs.name).ToString())"">"

                

        $admgrps = ($exchorgs.children | Where-Object {$_.objectClass -contains 'msExchAdminGroupContainer'}).children

        $output += "<AdmGrps>"

        foreach($admgrp in $admgrps){

            $output += "<Group Name=""$(($admgrp.name).ToString())"">"

            $exchsrvrs = ($admgrp.children | Where-Object {$_.objectClass -contains 'msExchServersContainer'}).children

            $output += "<Servers>"

            foreach($srv in $exchsrvrs){

                $mysrv = $srv.name

                $output += "<Server Name=""$(($srv.name).ToString())"">"

                $istores = ($srv.children | Where-Object {$_.objectClass -contains 'msExchInformationStore'}).children

                $output += "<InfoStores>"

                foreach($is in $istores){

                    $output += "<Store Name=""$(($is.name).ToString())"">"

                    $ismdbs = $is.children | Where-Object {$_.objectClass -contains 'msExchMDB'}

                    $output += "<StoreDBs>"

                    foreach($ismdb in $ismdbs){

                        $mailboxcnt = (Get-WmiObject -Namespace 'root\MicrosoftExchangeV2' -Class 'Exchange_Mailbox' -computer $mysrv | ?{$_.StoreName -eq $ismdb.name}).count 

                        $output += "<DBName>""$(($ismdb.name).ToString())""</DBName>"

                        $output += "<DBPath>""$(($ismdb.msExchEDBFile).ToString())""</DBPath>"

                        $output += "<DBMailBoxCount>""$mailboxcnt""</DBMailBoxCount>"

                    }

                    $output += "</StoreDBs>"

                    $output += "</Store>"

                }

                $output += "</InfoStores>"

                $output += "</Server>"

            }

            $output += "</Servers>"

            $output += "</Group>"    

        }

        $output += "</AdmGrps>"                

        $output += "</Organization>"

        $output += "</ExchangeOrg>"

    Write-Host "Inventory complete."

    Write-Output $output

}

 

If you come up with a better approach before I do, please feel free to share!

Enjoy!

Merddyn

January 28

New Project – My ‘Uber’ AD Info Tool – P1

As usual, it has been some time since I have posted anything, but sadly, I have had little opportunity to do any significant scripting lately. Fortunately, that has changed.

One of the many services my company provides is to perform health checks for AD, MOSS, Exchange, etc just like MS consulting services does. Unlike MCS however, we don't have their nifty tools that do a healthy chunk of the work for you to gather data. Yes, we are an MS partner, but even partners don't get to use these tools. Of course, there are a myriad of tools out there that get this info both commercial and free. The commercial option is great if our customer already owns it, but I tend to want to steer clear of this option because it sometimes gives the impression to the client that you have some agenda regarding this or that product. While this may sometimes be the case, it can be damaging to the trust relationship. This leaves me with the free alternatives.

There are many hundreds of tools out there to gather all kinds of info, each with its own way of getting and presenting data which, of course, then must be massaged into a bigger picture and analyzed, then made presentable. This is a lot of work…and I really hate work (yes, in spite of how hard I work to avoid work). Even MCS realizes this, but nobody wants to reinvent the wheel, so MCS built their tools to leverage the existing ones. Basically they made a bunch of wrapper WSF scripts and a nifty interface and called it a day (which is really why I am surprised they won't let partners use it).

So, to that end, I have started a pet project to build a tool for use by my company. The advantage to any readers of this blog, of course, is that you get to enjoy the benefits of some of my labor since this is where I will be posting the different sub pieces as I get them done so that I don't lose them down the road. Hopefully this helps others out and, who knows, maybe I'll even accept a few beta testers from outside my company when I get further along. The scripts, however, are free to use as you will and, as always, please feel free to drop me a line if you have improvements.

With goal number 1 being to avoid commercial products (even the free ones), I know I have to use scripting either via ADSI or .NET…and frankly I have never been a huge fan of ADSI and I have some vague notion that I will eventually be able to do some programming down the road so .NET it is. The MCS AD Rapid Assessment Tool, which I do happen to have an older copy of (and no, I won't send it to you), is being used as my guide for my own AD Info tool. I am not copying it, just using it to make sure I get all the pieces.

Part 1 is going to focus on the AD Sites aspect of AD replication; Namely collecting a bunch of useful info about sites and putting it into a usable format. For this I will be using the System.DirectoryServices.ActiveDirectory namespace for which you can find detailed information on the MSDN library here - http://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectory.aspx

Since I will eventually be trying to get a rather large chunk of info, I will start by binding to the forest.

$forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()

This gets the forest of which the system I am running the script from is a member. There are lots of useful pieces in here and I can see already it's a good start.

   

Name

Description

ApplicationPartitions

Gets a collection of all application partitions in the forest.

Domains

Gets a collection of all domains in the forest.

ForestMode

Gets the operating mode of the forest.

GlobalCatalogs

Gets a collection of all global catalogs in the forest.

Name

Gets the name of the forest.

NamingRoleOwner

Gets the domain controller that holds the FSMO naming master role for the forest.

RootDomain

Gets the first domain that was created in a forest.

Schema

Gets the schema of the forest.

SchemaRoleOwner

Gets the domain controller that holds the FSMO schema master role for the forest.

Sites

Gets a collection of sites that are contained in the forest.

 

Since site information is what I want right now, I'll just try binding to sites like this:

$sites = $forest.sites

Now $sites produces a relatively nice listing of all my sites

PS C:\Users\BobbyBuche> $sites

 

 

Name : Default-First-Site-Name

Domains : {merddyn.net}

Subnets : {10.1.1.0/24}

Servers : {WIN-OA56LC50JDR.merddyn.net}

AdjacentSites : {TestSite}

SiteLinks : {DEFAULTIPSITELINK}

InterSiteTopologyGenerator : WIN-OA56LC50JDR.merddyn.net

Options : None

Location : Dallas

BridgeheadServers : {}

PreferredSmtpBridgeheadServers : {}

PreferredRpcBridgeheadServers : {}

IntraSiteReplicationSchedule : System.DirectoryServices.ActiveDirectory.ActiveDirectorySchedule

 

Name : TestSite

Domains : {}

Subnets : {10.1.2.0/24}

Servers : {}

AdjacentSites : {Default-First-Site-Name}

SiteLinks : {DEFAULTIPSITELINK}

InterSiteTopologyGenerator :

Options : None

Location : Houston

BridgeheadServers : {}

PreferredSmtpBridgeheadServers : {}

PreferredRpcBridgeheadServers : {}

IntraSiteReplicationSchedule :

 

Well, almost nice…there are all those elements inside the curly braces {} that, while fine in my little bitty environment, could be a problem in a larger one with many sites, subnets, and servers, etc. So I need to find a way to expand that information. I am sure there is some way to do this iteratively, but I am not sure how to do this so I'll start out by trying to bind to a specific site and see if I can get to it that way.

As it turns out though, binding to a site via .NET is a bit harder than I anticipated and there are not any examples of this I could find anywhere using PowerShell. There are lots about adding subnets or sites, just none about modifying existing sites. A quick look through the namespace (Of course, finding this took a little longer than a 'quick look' but the details wouldn't help you so I'll skip em) reveals another promising class: System.DirectoryServices.ActiveDirectory.ActiveDirectorySite

This class has a method called 'FindByName' that looks like it will allow me to bind to a single site by name, something I couldn't do via $forest.sites. Unfortunately, it has two overloads that are required. One is, of course, the string that is the name of the site, but the first one is 'DirectoryContext' and the MSDN page on the subject isn't very enlightening. So with no clue how to bind to it, and after several failed attempts, back to Google I go with a search for PowerShell and the namespace with the DirectoryContext thrown in. Mr. Richard Siddaway (http://richardsiddaway.spaces.live.com) comes to my rescue with an example of how to set up a context which, coupled with the MSDN information allows me to get this:

$context = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]"forest"

$forcon = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext($context,$forest)

 

This lets my bind to a specific site by name like this:

$mytestsite = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::FindByName($forcon, "TestSite")

 

My output from typing $mytestsite still produces similar output to above, but now I can type $mytestsite.subnets, for example, and get an expanded listing of the subnet. I could, at this point, create a custom object and add everything to it using Add-Member, but I remembered another way that I tried out (and it worked) using Format-Table as shown here:

$siteslist = $sites | ft name,

                        @{Label="Domains";Expression={$_.domains}},

                        @{Label="Servers";Expression={$_.servers}},

                        @{Label="Adjacent Sites";Expression={$_.adjacentsites}},

                        @{Label="Site Links";Expression={$_.sitelinks}},

                        intersitetopologygenerator,

                        Options,

                        Location,

                        @{Label="Bridgehead Servers";Expression={$_.bridgeheadservers}},

                        @{Label="Preferred SMTP Bridgeheads";Expression={$_.preferredsmtpbridgeheadservers}},

                        @{Label="Preferred TPC Bridgeheads";Expression={$_.preferredrpcbridgeheadservers}}

 

This creates a nicely formatted table output with each item expanded like I wanted. You may have noted that I didn't include the subnet info. This is on purpose as I plan to gather that later along with some additional info on undefined subnets.

In part 2, I plan to move on to getting detailed Replication Configuration, Replication Status, as well as the aforementioned subnet info and AD Convergence checks (Brandon Shell has a great script for this over on his blog at http://bsonposh.com which I am totally gonna steal…er...borrow). Hopefully it won't be another 4 months before I can get to it, but there may be a few other posts in the meantime for some other things I got going on.

Until next time, Enjoy!

Merddyn

September 13

Updating AD Users En-Mass…the Easy Way

Well, it's easier anyway.

So what's the deal you ask? Well, it's a TV show where they have these briefcases and…oh…you meant with the title.

*clears throat*

I knew that. I was just testing you.

So the deal is this; I recently had a colleague who was looking for a way to update a few users…about 173,000 users to be imprecise. Now this is certainly something that could be done with the good old trusty VBScript, for which there are literally thousands of examples for on the internet…except for the fact that each user had an inconsistent number of fields and values to be updated and seven of the columns actually had to be combined into a single value, but only the ones that had data to avoid dumping in blank data.

Now I have written some pretty decently complex VBScripts in the past, but the logic associated with checking each column for each user and acting on it dynamically was really something much easier done in PowerShell. So, of course, I wrote up a quickie little PowerShell function using the Quest AD Cmdlets and sent it on its merry way. I also try to steer clear of VBScript these days if at all possible since, if I don't make myself use the newer tech, I will never get used to it in time before I HAVE to be able to use it. If you work predominantly in the Microsoft space like I do, the PoSh wave is coming and you WILL have to know how to use PowerShell, even if you never write a single script (but you will write scripts, cause it's just too easy to do not to).

After that was off and away though, I took a quick little jaunt around the net in my handy dandy Google-Mobile to see what others had done for this type of mass update using PowerShell. What I found was mostly a lot of specific use snippets or updates for only a single AD attribute, but not much in the way of mass user updating that was flexible for any situation. I found this fairly odd since, as anyone who has been using PowerShell for any amount of time knows, you really don't need to even use scripts most of the time, just good one-liners and the pipeline. For example, let's say you had a spreadsheet with users who were moving from one office to another and the spreadsheet had columns for username, street , city, and zip (let's assume they are staying in the same state). You could simply update the username column to be 'domain\username' and then run the following one-liner:

Import-CSV C:\usermoves.csv | Set-QADUser $_.username –StreetAddress $_.street –City $_.City –PostalCode $_.zip

As long as you have data in every column, you're done. Even empty columns will just cause the data already in the attribute to be deleted, but it should still work just fine as long as that's ok. But what about the scenario with my colleague and what about all those poor souls out there who haven't yet discovered how great and easy PowerShell is? They deserve a solution too don't they, something that they can easily find in their wanderings on the net?

… Well maybe not precisely 'deserve' since they could theoretically take the time to learn PoSh rather than depending on the kindness of strangers, but I'm a softy…a giver you might say.

So, what I did, was write a function called Update-BTFuser that you can see below. I wrote it with the requirement that you would have to have the Quest AD Cmdlets installed in order to run the function, but the Quest cmdlets are free so I didn't think that too onerous. Yes, you could do the same thing using ADSI, and there are those of my peers who might think less of me for not doing it that way, but in my mind, why should I reinvent the wheel when Quest's wheel is so nice and shiny and pretty…like one of those sparkly 22 inch jobs with the spinners…if you're into that kind of thing anyway…you get the point.

So, if Quest's AD cmdlets are soooo great, why did I write a function to implement them you might ask? Quests cmdlets ARE great, but they don't have a native ability to map your column titles to a particular execution path (if you work for Quest on these cmdlets…hint hint *wink*). In addition, there are a several options in the Set-QADuser cmdlet, as an example, that don't map to a particular AD attribute name such as –PasswordNeverExpires. So the below function deals with all that and also compensates for having empty cells without clearing the existing attribute values by checking for a 'clear' value. If the cell is merely empty, it gets ignored, but if it contains the word 'clear', then it deletes the existing attribute data. In addition, it looks for PasswordNeverExpires and UserMustChangePassword columns and, if the value of the column is Y, it sets the value to True and N will set it to False (empty is still ignored). It even provides for a UserPassword column for updating passwords en-mass. I even accounted for the ObjectAttributes switch so that you can specify your own custom attributes, or those not covered by me in the function (which shouldn't be too many). I didn't account for every possible switch for the cmdlet, but I figure that with most of them there that you can add any that you need and I left out.

So, without further prattling on by me (despite how much I KNOW you love hanging on my every typed word *wink*), here is a link to a spreadsheet template that you can use, complete with examples of what goes in the different fields, and the function is below.

# ==============================================================================================

#

# Microsoft PowerShell Source File -- Created with SAPIEN Technologies PrimalScript 2007

#

# NAME: Update-BTFUser

#

# AUTHOR: Christopher Whitfield, BT

# DATE : 9/12/2008

#

# COMMENT: Must have Quest AD Cmdlets installed to use this function. Must also provide input

#          from CSV file with columns named for AD attributes via the pipeline.

#

# ==============================================================================================

 

Function Global:Update-BTFUser {

    begin {

 

        # Set error behavior

        $erroractionpreference = "SilentlyContinue"

 

        # Get domain

        $domprompt = Read-Host "Please specify the netbios domain name:"

        

        # Set dom variable for script

        if($domprompt){

            $dom = $domprompt + "\"

        }

        else {

            $dom = $ENV:USERDOMAIN + "\"

        }

        

        # Create placeholder variables

        $processed = 0

        $procerrors = 0

        $probuserout = @()

        $execcmd = @()

            

    }

    process {

        if($in){

            foreach ($in in $input){

                $user = $dom + $in.sAMAccountName

                

                # Set initial execution cmd string

                $execcmd = " -identity $user"

                

                # Append to execution cmd string with data from cells using following steps:

                #     First check to see if cell has data and ignore if empty

                #     Next check to see if cell is marked 'clear' which indicates to delete current value from attribute

                #    If cell is not marked 'clear', add data in cell to cmd with appropriate switch

                

                if($in.givenName){

                    if($in.givenName -like "clear"){

                        $execcmd += " -firstname ''"

                    }

                    else {

                        $execcmd += " -firstname '$in.givenName'"

                    }

                }

                

                if($in.sn){

                    if($in.sn -like "clear"){

                        $execcmd += " -lastname ''"

                    }

                    else {

                        $execcmd += " -lastname '$in.sn'"

                    }

                }

                

                if($in.initials){

                    if($in.initials -like "clear"){

                        $execcmd += " -initials ''"

                    }

                    else {

                        $execcmd += " -initials '$in.initials'"

                    }

                }

      

                if($in.displayName){

                    if($in.displayName -like "clear"){

                        $execcmd += " -DisplayName ''"

                    }

                    else {

                        $execcmd += " -DisplayName '$in.displayName'"

                    }

                }

      

                if($in.userprincipleName){

                    if($in.userprincipleName -like "clear"){

                        $execcmd += " -userprinciplename ''"

                    }

                    else {

                        $execcmd += " -userprinciplename '$in.userprincipleName'"

                    }

                }

      

                if($in.homedrive){

                    if($in.homedrive -like "clear"){

                        $execcmd += " -homedrive ''"

                    }

                    else {

                        $execcmd += " -homedrive '$in.homedrive'"

                    }

                }

      

                if($in.homedirectory){

                    if($in.homedirectory -like "clear"){

                        $execcmd += " -homedirectory ''"

                    }

                    else {

                        $execcmd += " -homedirectory '$in.homedirectory'"

                    }

                }

      

                if($in.profilepath){

                    if($in.profilepath -like "clear"){

                        $execcmd += " -profilepath ''"

                    }

                    else {

                        $execcmd += " -profilepath '$in.profilepath'"

                    }

                }

    

                if($in.scriptpath){

                    if($in.scriptpath -like "clear"){

                        $execcmd += " -logonscript ''"

                    }

                    else {

                        $execcmd += " -logonscript '$in.scriptpath'"

                    }

                }

      

                if($in.mail){

                    if($in.mail -like "clear"){

                        $execcmd += " -email ''"

                    }

                    else {

                        $execcmd += " -email '$in.mail'"

                    }

                }

      

                if($in.wWWHomePage){

                    if($in.wWWHomePage -like "clear"){

                        $execcmd += " -webpage ''"

                    }

                    else {

                        $execcmd += " -webpage '$in.wWWHomePage'"

                    }

                }

      

                if($in.company){

                    if($in.company -like "clear"){

                        $execcmd += " -company ''"

                    }

                    else {

                        $execcmd += " -company '$in.company'"

                    }

                }

      

                if($in.department){

                    if($in.department -like "clear"){

                        $execcmd += " -department ''"

                    }

                    else {

                        $execcmd += " -department '$in.department'"

                    }

                }

      

                if($in.manager){

                    if($in.manager -like "clear"){

                        $execcmd += " -manager ''"

                    }

                    else {

                        $execcmd += " -manager '$in.manager'"

                    }

                }

      

                if($in.title){

                    if($in.title -like "clear"){

                        $execcmd += " -title ''"

                    }

                    else {

                        $execcmd += " -title '$in.title'"

                    }

                }

      

                if($in.physicalDeliveryOfficeName){

                    if($in.physicalDeliveryOfficeName -like "clear"){

                        $execcmd += " -office ''"

                    }

                    else {

                        $execcmd += " -office '$in.physicalDeliveryOfficeName'"

                    }

                }

      

                if($in.streetAddress){

                    if($in.streetAddress -like "clear"){

                        $execcmd += " -streetAddress ''"

                    }

                    else {

                        $execcmd += " -streetAddress '$in.streetAddress'"

                    }

                }

      

                if($in.l){

                    if($in.l -like "clear"){

                        $execcmd += " -city ''"

                    }

                    else {

                        $execcmd += " -city '$in.l'"

                    }

                }

      

                if($in.st){

                    if($in.st -like "clear"){

                        $execcmd += " -StateOrProvince ''"

                    }

                    else {

                        $execcmd += " -StateOrProvince '$in.st'"

                    }

                }

      

      

                if($in.postalCode){

                    if($in.postalCode -like "clear"){

                        $execcmd += " -PostalCode ''"

                    }

                    else {

                        $execcmd += " -PostalCode '$in.postalCode'"

                    }

                }

      

                if($in.postOfficeBox){

                    if($in.postOfficeBox -like "clear"){

                        $execcmd += " -PostOfficeBox ''"

                    }

                    else {

                        $execcmd += " -PostOfficeBox '$in.postOfficeBox'"

                    }

                }

      

                if($in.telephoneNumber){

                    if($in.telephoneNumber -like "clear"){

                        $execcmd += " -PhoneNumber ''"

                    }

                    else {

                        $execcmd += " -PhoneNumber '$in.telephoneNumber'"

                    }

                }

      

                if($in.facsimileTelephone){

                    if($in.facsimileTelephone -like "clear"){

                        $execcmd += " -fax ''"

                    }

                    else {

                        $execcmd += " -fax '$in.facsimileTelephone'"

                    }

                }

      

                if($in.homePhone){

                    if($in.homePhone -like "clear"){

                        $execcmd += " -HomePhone ''"

                    }

                    else {

                        $execcmd += " -HomePhone '$in.homePhone'"

                    }

                }

      

                if($in.mobile){

                    if($in.mobile -like "clear"){

                        $execcmd += " -MobilePhone ''"

                    }

                    else {

                        $execcmd += " -MobilePhone '$in.mobile'"

                    }

                }

      

                if($in.pager){

                    if($in.pager -like "clear"){

                        $execcmd += " -pager ''"

                    }

                    else {

                        $execcmd += " -pager '$in.pager'"

                    }

                }

      

                if($in.info){

                    if($in.info -like "clear"){

                        $execcmd += " -notes ''"

                    }

                    else {

                        $execcmd += " -notes '$in.info'"

                    }

                }

      

                if($in.description){

                    if($in.description -like "clear"){

                        $execcmd += " -description ''"

                    }

                    else {

                        $execcmd += " -description '$in.description'"

                    }

                }

      

                if($in.PasswordNeverExpires){

                    if($in.PasswordNeverExpires -like 'N'){ $pne = $False }

                    else { $pne = $True }

                    

                    $execcmd += " -PasswordNeverExpires '$pne'"

                }

      

                if($in.UserMustChangePassword){

                    if($in.UserMustChangePassword -like 'N'){ $umcp = $False }

                    else { $umcp = $True }

                    

                    $execcmd += " -PasswordNeverExpires '$umcp'"

                }

      

                if($in.UserPassword){

                    $execcmd += " -UserPassword '$in.UserPassword'"

                }

      

      

                if($in.ObjectAttributes){

                    $execcmd += " -ObjectAttributes @{$in.ObjectAttributes}"

                }

      

                # Execute combined command for current user

                Set-QADUser $execcmd

                

                # Increment number of processed users

                $processed += 1

      

                # Check for errors and store for output

                if($error) {

                    $procerrors += 1

                    $probusers = New-Object System.Object

                    $probusers | Add-Member -type NoteProperty -name "User" -value $in.sAMAccountName

                    $probuserout += $probusers

                }

            }

        }

        else {

            throw "You must provide pipeline input from associated CSV file or another CSV with columns named for attributes. Exiting..."

            exit

        }

    }

    end {

        Write-Host "Finished updating $processed users!"

        Write-Host "Had $procerrors problems updating employeeIDs for the following people:"

        $probuserout | Sort-Object User

    }

}

# ==============================================================================================

As always, please feel free to provide your own comments or feedback and especially improvements. I am far from the best of PoSh scripters, and I know there are often better ways and I love to learn them.

Until next time…

Merddyn

 

Chris Whitfield

Thanks for visiting!
Please wait...
Sorry, the comment you entered is too long. Please shorten it.
You didn't enter anything. Please try again.
Sorry, we can't add your comment right now. Please try again later.
To add a comment, you need permission from your parent. Ask for permission
Your parent has turned off comments.
Sorry, we can't delete your comment right now. Please try again later.
You've exceeded the maximum number of comments that can be left in one day. Please try again in 24 hours.
Your account has had the ability to leave comments disabled because our systems indicate that you may be spamming other users. If you believe that your account has been disabled in error please contact Windows Live support.
Complete the security check below to finish leaving your comment.
The characters you type in the security check must match the characters in the picture or audio.
Public folders