Mapping Printers
Normally I’m a big fan of using Group Policy to map printers and network drives, it’s just so much easier to use and configure and anyone can do it. No extra scripting knowledge needed. And it just works. Simple is best. But sometimes it doesn’t and you need to go to plan B. Mapping printers at work turned into just that situation and here’s how I solved it.
Group Policy And Why They Didn’t Work
About a year ago I went through and changed our automatic printer mappings to use Group Policy instead of my trusty vbScript based logon script. What I did was create a group that I put users into, then created a group policy and changed permissions on the GP to only apply the policy if a user was a member of that group. I then used the Print Manager MMC on Windows 2008 to deploy that printer to that GP, so I ended up with a one-to-one-to-one relationship of group, to GP to printer share. Worked great but we did notice a minor slowdown in login times as Windows has to process all of these printer Group Policies.
I should mention that I apply all of the policies to all of my users, which gives me the flexibility of giving anyone any printer at any time (we do have a few travelers). But this resulted in over 20 policies. Then I added our remote site and that added about 10 more. Then we acquired another company and that added about 15 more. Suddenly login times were getting really slow and the number of GP’s to manage just got out of hand. So I thought, what about Item Level Targeting? It’s a great feature that allows you to apply particular options to an object within the policy. So I created 3 new polices, one for each site, and put all of their printers in those.
Unfortunately login times got much worse after that. Doing some research on the interwebs confirmed my worst fear that there really is no fix for this and ILT on Group Policies is just plain slow–especially if you use security groups. Great!
If You Can’t Beat It, Script it!
Now I need to be able to map printers from a script but I don’t want just any old vbScript, that’s going backwards. Time to put some PowerShell muscle into it. Luckily, I had another project from PowerGUI someone had asked for that gave me the code to find all of the groups a user belongs to, without using Remote Server Administration Tools–which is good because there’s no way I’m installing that on every PC! Here’s the code:
Function Get-Groups { Param ( [string]$User = $env:USERNAME ) $Searcher = New-Object System.DirectoryServices.DirectorySearcher $Searcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry $Searcher.Filter = "(&(objectCategory=User)(SamAccountname=$User))" $Searcher.SearchScope = "Subtree" $Found = $Searcher.FindOne() [String[]]$Groups = $Found.Properties["memberof"] Return $Groups }
Simple use of ADSI to find the user object and enumerate the groups into a variable and return it. I cast the object into a String array just to make sure the data is in a nice normal state and will play nice later. That’s one of the biggest pieces right there. But I also need to know what’s already mapped, since there’s no need to map a printer if the user already has it. This turned into a very simple WMI call:
$Printers = Get-WmiObject Win32_Printer -Filter "Network = True" | Select -ExpandProperty Name
Turns out that any network printer has a property Network that’s always true, so by filtering on that I get an instant return of what I need which saves me from having to loop through everything each time I map a printer. Since I only need the name I use -ExpandProperty on the Select cmdlet to just create a nice normal array with just the names in it.
The Function
Here’s a great time for a function since I have something I want to call over and over again (once for each printer) that’s not already in a loop. So I created a function to actually map the printer. Now, one thing I’ve noticed is that mapping a printer using the Wscript.Network ComObject doesn’t always work. Sometimes there’s an error, so I want the script to try 3 times before calling it quits and moving on. Here’s a pared down version of what I ended up using:
Function Set-MapPrinter { Param ( [string]$Share, [string]$Group ) If ($Groups -like "*$Group*") { If ($Printers -notcontains $Share) { $Retry = 0 $ErrorActionPreference = "Stop" Do { Try { $Network.AddWindowsPrinterConnection($Share) $Retry = 3 } Catch { $Retry ++ If ($Retry -eq 3) { # Show user the error, but Window will only stay open for 8 seconds $Shell.Popup("Unable to map printer: $Share`n`nError: $($Error[0])",8,"Mapping Printer Error",0) | Out-Null } Else { Start-Sleep -Seconds 2 } } } Until ($Retry -eq 3) $ErrorActionPreference = "Continue" } } }
The first thing we need to do is make sure the user is a member of the specified group. This was a problem because the “Get-Group” function returns the name of the group in FQDN format, not Display Name! It turns out you can use -Like against an array and it will work just fine, so that took care of checking group membership–and using a simple string array really paid off here! Next we need to check if the printer is already mapped, so we can check the share name that was passed to the function against our $Printers array using -contains and see if it’s exists. If it does, simply move on, but if not then we can start doing things.
Now we need to map the printer, and remember how I wanted to try 3 times if there is a problem? To do this I setup a simple Do/Until loop using a manually controlled variable ($Retry). Speaking of errors, since object methods don’t have an -ErrorAction parameter I still need to create a stop level action if there is an error. To do this I set the global script action to “Stop” using the $ErrorActionPreference variable. Now if it fails on the drive map it will trigger my Catch scriptblock. But if it succeeds I just set my $Retry variable to it’s top level which will allow me to escape my Do loop.
On a fail we’ll wait a couple of seconds and try again. On the 3rd failure we’ll actually use good old Wscript.Shell–whose object is stored in $Shell during the setup of the script–to produce a pop up for the user. I like using this pop up over the .Net pop up because this one actually has a timing mechanism which allows me to automatically close the script after a set time (8 seconds in this case). That way the script can continue on without the user intervening.
And that’s it, I’m now able to edit this script and add these lines to it to include more printers:
Set-MapPrinter -Share “\\PrintServer1\printer1” -Group “Accounting”
Set-MapPrinter -Share “\\PrintServer1\printer2” -Group “Customer Service”
One line, one printer and one group membership and it all goes much faster than Group Policies. Which is too bad. Hopefully Microsoft will fix that because I really do prefer the Group Policy method, but until they do I hope you enjoy the script.
Other Stuff
Now, I couldn’t just leave the script at this level, I actually did a little bit more. One was I gave the script the ability to log events in the System event log using the source “Set-MapPrinter”. This was for success and failures, with the failure message including the actual error message involved. This will allow an administrator to take a look at things and try to figure out what went wrong. I also added a lot of verbose output so administrators can do some extensive testing before deploying it and be comfortable with what the script is doing.
The full script can be found over at Spiceworks:
New version released tonight. Have added support for nested groups.
I like scripting. But if you wanted to avoid having to script you could of used Item-Level targeting in the GPO. I had to do this in mapping a drive to a non critical data center. The Item-leveling targeting can be a very powerful feature.
Absolutely. Problem is once you get 25-30 or more printers GPO really starts slowing down. Item level targeting, using groups is one of the most expensive checks you can make in GPO. This was resulting in LONG login times (it’s the first couple of paragraphs of this post). The script cut this down to a couple of seconds.