List a Printer in Active Directory Using a CNAME

(See also David’s comment below, which uses the native ActiveDirectory module)

This was a fun one for me…

One of the great questions we are dealing with now is how to make printers easy to locate on the network. Traditionally, users mapped a printer by UNC path only. This is not always helpful when identifying location and features. Although our printers are published in Active Directory, browsing the directory for a printer is not standard practice. I have to take some of the blame for this since I worked at our Help Desk for several years and reinforced this method. Now, I am trying to convince others that browsing the directory is much simpler and allows for greater freedom in printer naming (i.e.: including location information in the printer name is no longer critical).

However, after tackling how to list more than 20 directory printers in the first “Add Printer Wizard” screen of Windows 7 (http://community.spiceworks.com/how_to/show/1374), I unearthed a problem: When browsing the directory, Windows will map the printer via its print server’s Windows name, rather than a CNAME (or, alias). That is kind of a no-brainer when you consider how that information gets into AD from the server, but it is an important point to note. Why is this a problem? We use an alias for printer mappings so we can swap print servers without axing all printing. If everyone maps directly to the server’s Windows name, they will lose their mapping when that server is replaced. Other IT shops use DNS round-robin to load-balance print servers. Adding a printer from the browser negates that benefit.

There are two options at this point. First, you can choose to add the printers to AD manually. This allows you to modify the UNC path and server name without a fuss. However, it does not update features, model, location, etc. automatically. When you chose “List in the directory” from the print server, all of this is handled automatically. The second option, which I suggest, is to let the print server list your printers and update them for you. This, of course, requires a workaround for the alias.

The Quest for Option 2

Naturally, I hit the Googles. When a few searches turned up nothing, I had a realization: All of this information is in AD and has to be accessible. I turned to Quest’s AD cmdlets and began exploring the properties of the AD object known as a “printqueue”. I was surprised to find these objects nested within the print server’s container. Three properties carried importance to me: serverName, shortServerName, and uNCName (interesting capitalization, by the way). In the end, the only property in this list that determines the network path to the server is uNCName. But, the other properties are good to update for display purposes.

Using Quest Active Roles, I was able to update the relevant properties:


set-QADobject CN=SOMESERVER-SomePrinter,CN=SOMESERVER,OU=Servers,DC=domain,DC=com -objectattributes @{servername="prints.domain.com";shortservername="prints";uNCName="\\prints.domain.com\SomePrinter"}

You can also use the printer’s name instead of its distinguished name. For example, instead of “CN=SOMESERVER-SomePrinter,CN=SOMESERVER,OU=Servers,DC=domain,DC=com” you could simply use “SOMESERVER-SomePrinter”. In most organizations, this should be distinguished enough.

Directory-Wide Update Script


#Add the Quest Active Roles AD Management snapin but silently continue if it fails
 Add-PSSnapin Quest.ActiveRoles.ADManagement -EA 0

#Variables for the print servers' real names, based on DN
 $PrintServersDN = @("CN=SOMESERVER-SomePrinter,CN=SOMESERVER,OU=Servers,DC=domain,DC=com")
 $PrintServersDNS = @()

#Variables for the print server's CNAME/alias
 $PrintServerAlias = "prints"
 $PrintServerAliasDnsSuffix = "domain.com"
 $PrintServerAliasLong = "$PrintServerAlias.$PrintServerAliasDnsSuffix"

#Get all the print servers' FQDNs and add them to the $PrintServersDNS array
 ForEach ($s in $PrintServersDN){
 $serverDNS = (Get-QADComputer $s | select DnsName).DnsName.tolower()
 $PrintServersDNS += $serverDNS
 }

$printers = get-qadobject -type printqueue -includeallproperties | where {$PrintServersDNS -contains $_.servername}

If ($printers -ne $null){

Foreach ($p in $printers){
 $printShareName = $p.printsharename
 $printerDN = $p.DN
 $uncName = "\\$PrintServerAliasLong\$printShareName"
 set-QADobject $printerDN -objectattributes @{servername="$PrintServerAliasLong";shortservername="$PrintServerAlias";uNCName="$uncName"}
 }
 }

Else{Write-Host "No changes to be made"}

I was excited by the results. The Add Printer Wizard picked up the new alias and mapped the printer according to the server’s alias. The downside? Every time I made a configuration change on the print queue, the server (as mentioned earlier) automatically updated AD with the print server’s Windows name. That’s when the wonderful Task Scheduler came to my rescue. I could simply setup a task triggered by 306 events in the Microsoft>Windows>PrintService>Operational log. But, this took a lot more study and brainstorming than I expected. And I learned a lot more about the Task Scheduler.

Here was the big question: “How can I run a script every time a printer’s configuration changes that will not have to update the whole directory every time?” As I considered triggering only one event that had a long enough pause to cover all changes in the even of a mass-update, I stumbled across this post: http://blogs.technet.com/b/otto/archive/2007/11/09/find-the-event-that-triggered-your-task.aspx. It shows how to create and pull variables from a scheduled task using Value Queries. This may not be news to you, but it was to me. Good news. I was able to write a script (shared below) that triggers at each printer configuration change and updates only that printer. Then, I hit another wall: a race condition. When the script fired right away, it would not detect any changes in AD because of either replication or a delayed write. Delaying the task by 30 seconds did the trick. It’s not perfect, but it does work. You can tweak your own settings. I just have to avoid changing two printers less than 45 seconds apart.

Single Update Script


Param(
[string]$PrinterName
)

#Add the Quest Active Roles AD Management snapin but silently continue if it fails
Add-PSSnapin Quest.ActiveRoles.ADManagement -EA 0

#Get the printer's share name (in case it is different than the printer's name)
$PrinterShareName = (Get-ItemProperty hklm:\system\currentcontrolset\control\print\printers\$PrinterName)."Share Name"

#Variables for the print server's real names
$PrintServer = (Get-Item env:computername).Value
$PrintServerDNS = (Get-QADComputer $PrintServer).DnsName

#Variables for the print server's CNAME/alias
$PrintServerAlias = "prints"
$PrintServerAliasDnsSuffix = "domain.com"
$PrintServerAliasLong = "$PrintServerAlias.$PrintServerAliasDnsSuffix"

#Find the printer object in AD
$PrinterADname = "$PrintServer-$PrinterShareName"
$PrinterADobject = Get-QADObject -Type printqueue $PrinterADname -IncludeAllProperties | where {$_.servername -eq $PrintServerDNS}

If ($PrinterADobject -ne $null){

$printerDN = $PrinterADobject.DN
$uncName = "\\$PrintServerAliasLong\$PrinterShareName"
set-QADobject $printerDN -objectattributes @{servername="$PrintServerAliasLong";shortservername="$PrintServerAlias";uNCName="$uncName"}
Write-Host "Done"
}

Else{Write-Host "No changes to be made"}

Please note, this script is intended for Server 2008. Server 2003 stores its printer elsewhere in the registry. While modifying the script to detect and respond to Server 2003 and Server 2003 x64, I realized it probably would not be helpful anyway as a triggered event.

To complete the process, I created a task (following the steps in the linked Technet blog above) which set “param1” to the task’s variable “$(param1)”. This was passed in as the “$PrinterName” parameter variable in the PowerShell script. Here is the Value Query I created in the task:


<ValueQueries>
<Value name="param1">Event/UserData/PrinterSet/Param1</Value>
</ValueQueries>

Further configurations included setting the task to delay itself by 30 seconds and not launch a new instance if triggered. Otherwise, you may get 20 parallel processes. That’s basically it:

  1. Create your script
  2. Create a scheduled task based on an event 306 in the PrintService>Operational log, delay it by 30 seconds, and make sure it passes the printer name parameter to your script

The directory-wide script is good for cleanup, but the single update script is better for regular updates. This should keep your directory up-to-date with your print server(s) CNAME.

23 thoughts on “List a Printer in Active Directory Using a CNAME

  1. Luke says:

    very useful article. 🙂 one question – how do I pass that param1 into the provided script? 🙂 I’ve managed to trigger it on 306 events, I’ve put into the .xml before re-importing the scheduled task. The task triggers fine, but changes the UNC share in AD to \\server.domain.com\ without appending the queue name itself at the end. I’m wondering what am I doing wrong…. :/

    • Hi Luke. I am glad to hear you find this useful. Currently, I have the following in the “Add arguments” textbox in my scheduled task:

      -File “c:\path\to\script\update-single-printer-AD.ps1” $(param1)

      “$(param1)” represents param1 from the event’s value query, which is the printer’s name. Give that a try and see if it works.

      -Jason

      • Luke says:

        Eventually figured it out, that I’m not passing that parameter at all. I’ve reached the solution yesterday around midnight 🙂 Now the only culprit I’m seeing is the situation when someone updates another printer withing that 30 seconds (~850 printers on the server). Probably directory wide re-publish of all printers on a weekly basis…. Thanks again. 🙂

      • Glad to hear you got it working. Yeah, that timing is a pain. I agree that a scheduled directory-wide update is probably your best bet for keeping things consistent.

  2. EES says:

    Hi,

    Really nice article..I have a few questions please

    I have setup the task to run the script but there are a few issues.

    My actions in the task are :

    program/script box: powershell
    Add arguments (optional) box : -file “C:\Temp\Printer AD Update.ps1” $(param1)

    However, when the script runs I get the error
    “Cannot find path “HKLM:\System\currentcontrolset\control\print\printers\$(param1) because it does not exist”

    The code for retrieving the printer share name seems to be causing this

    $PrinterShareName = (Get-ItemProperty hklm:\system\currentcontrolset\control\print\printers\$PrinterName).”Share Name”

    My settings are also configured as such in the xml file. I have also re-imported this into task scheduler and deleted the previous one

    true
    <QueryList><Query Id=”0″ Path=”Microsoft-Windows-PrintService/Operational”><Select Path=”Microsoft-Windows-PrintService/Operational”>*[System[Provider[@Name=’Microsoft-Windows-PrintService’] and EventID=306]]</Select></Query></QueryList>
    PT1M

    Event/UserData/PrinterSet/Param1

    Do you know why this error is occuring?

  3. EES says:

    Yes I am, Thank you for your response.

    I figured it out as there were redirected printers being added to the Print Queues anytime I RDP’d to the server. Thanks for your help

    • Hi Dee,

      Thanks for the question. The Quest tools will need to be installed wherever the script runs. So, if the script runs on the print server, the Quest tools will need to be installed there. Although, I really should go back and rewrite this with Microsoft’s ADodule.

      Thanks,

      Jason

  4. Dee says:

    I’m a little slow but for the Directory-Wide Update Script in the first section

    #Variables for the print servers’ real names, based on DN
    $PrintServersDN = @(“CN=SOMESERVER-SomePrinter,CN=SOMESERVER,OU=Servers,DC=domain,DC=com”)
    $PrintServersDNS = @()

    Do I need to update the $PrintServersDN string to match my environment or leave it as is? If I have to update it could you provide an example? Thanks

    • $PrintServersDN refers to the Distinguished Name (DN) of the printer object in Active Directory. You will need to change it for your environment. DN’s are written from left to right, starting with the most specific object like this (as an example):

      CN=computer123,OU=organizationalUnit,DC=example,DC=com

      Shared printers are containers within the print server object. Active Directory stores the printers name in the format of “PRINTSERVERNAME-printername”. So, if your print server was named “PRINTSERVER1” and your printer was named “printer2” (keep in mind, this generally shouldn’t be case-sensitive), and the printer server was in the “servers” OU in the example.com domain, the printer’s DN would be:

      CN=PRINTSERVER1-printer2,CN=PRINTSERVER1,OU=servers,DC=example,DC=com

      If the print server was nested in a deeper OU, then it might look something like:

      CN=PRINTSERVER1-printer2,CN=PRINTSERVER1,OU=westregion,OU=servers,DC=example,DC=com

      In ADUC, the structure would look something like this:

      example.com -> servers -> westregion -> PRINTSERVER1 -> PRINTSERVER1-printer2

      Just a long way of saying yes, you will need to change it for your environment. Take a look at where your print server is in AD and reverse to order as in the examples to come up with the Distinguished Name. I hope this helps. Sorry if anything ends up being misleading: I haven’t worked with this script in a while.

      Thanks,

      Jason

  5. it was working fine for a bit – but now when I launch the script to update all printers I’m getting:

    get-qadobject : An operation error occurred.
    At C:\Users\lszczepanski\Desktop\ADUpdatePrinterAll.ps1:19 char:13
    + $printers = get-qadobject -type printqueue -includeallproperties | where {$Print …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Get-QADObject], DirectoryAccessException
    + FullyQualifiedErrorId : Quest.ActiveRoles.ArsPowerShellSnapIn.DirectoryAccess.DirectoryAccessException,Quest.Act
    iveRoles.ArsPowerShellSnapIn.Powershell.Cmdlets.GetGenericObjectCmdlet

    We’ve recently added some new domain controllers (2012) and the script seems to be a hit and miss now. Any ideas? 🙂

  6. Hi Luke,

    It could be a firewall issue. Since it started after adding some 2012 DC’s and it is hit and miss, it is likely only throwing these errors when connecting to those new servers (just a guess). There is probably some difference between the different types of DCs, or there could even be an incompatibility between the Quest snapins and Server 2012. I’ve not used Quest ActiveRoles for a while and haven’t done much production work with ADDS 2012, but I would start by trying to identify differences between the DCs and maybe check for updates with Quest. Alternatively, you might have better luck if you use the newer AD PowerShell module (Import-Module ActiveDirectory). This has a similar cmdlet: Get-ADObject. You can use the -Properties switch to specify which properties to pull.

    I keep saying I need to revisit this script and switch it to the AD module… We’ll see if that happens.

    Good luck!

    -Jason

  7. David Prows says:

    I got this to work for me to update a single print server.

    Import-Module ActiveDirectory

    #Variables to Modify
    #Enter name of Print Server (not the FQDN)
    $serverName = “ServerName”
    #Enter DistinguishedName of Print Server (ex. “CN=ServerName,CN=Computers,DC=Domain,DC=com”)
    $serverDN = “CN=serverName,CN=Computers,DC=domain,DC=com”
    #Variables for the print servier’s CNAME/alias
    $PrintServerAlias = “PrintServer”
    $PrintServerAliasDnsSuffix = “domain.com”

    ########Do not Modify below this########

    #Change Directory to location of Printers
    sl AD:\$serverDN

    #Variables not to change
    $PrintServerAliasLong = “$PrintServerAlias.$PrintServerAliasDnsSuffix”
    $printers = Get-ADObject -Filter ‘ObjectClass -eq “printQueue”‘ -Properties *

    #Process each Printer and Change Servername to AliasName
    ForEach ($p in $printers){
    $printShareName = $p.printShareName
    $uncName = “\\$PrintServerAliasLong\$printShareName”
    $path = “$p”
    Set-ItemProperty -Name servername -Value $PrintServerAliasLong -path Ad:\”$($path)”
    Set-ItemProperty -Name shortservername -Value $PrintServerAlias -path Ad:\”$($path)”
    Set-ItemProperty -Name uNCName -Value $uncName -path Ad:\”$($path)”
    }

    • David Prows says:

      I was able to modify my script to also update only one printer that is on that print server when it is modified (using your method from above and triggering on event 306). I am sure someone could probably clean it up a bit, but it works for me.

      Param(
      [string]$printerName
      )

      Import-Module ActiveDirectory

      #Variables to Modify
      #Enter name of Print Server (not the FQDN)
      $serverName = “serverName”
      #Enter DistinguishedName of Printer (ex. “CN=ServerName-PrinterName,CN=ServerName,CN=Computers,DC=Domain,DC=com”)
      $printerDN = “CN=$serverName-$printerName,CN=$serverName,CN=Computers,DC=domain,DC=com”
      #Variables for the print servier’s CNAME/alias
      $printServerAlias = “PrintServer”
      $printServerAliasDnsSuffix = “domain.com”

      ########Do not Modify below this########

      #Change Directory to Location of Printer
      sl AD:\$printerDN

      #Variables not to change
      $printServerAliasLong = “$printServerAlias.$printServerAliasDnsSuffix”
      $printer = Get-ADObject -Filter ‘ObjectClass -eq “printQueue”‘ -Properties *
      $printShareName = $printer.printShareName
      $uncName = “\\$printServerAliasLong\$printShareName”
      $path = “$printer”

      #Change Printer Properties
      Set-ItemProperty -Name servername -Value $printServerAliasLong -path AD:\”$($path)”
      Set-ItemProperty -Name shortservername -Value $printServerAlias -path AD:\”$($path)”
      Set-ItemProperty -Name uNCName -Value $uncName -path AD:\”$($path)”

  8. jrp78 says:

    Amazing article! In regards to the race condition you mentioned, I’m also seeing eventID’s 332,334 and 336(Publishing a printer in the Active Directory) right after 306. Rather than trigger an update on 306 and add a 30 delay to the script, I am looking into doing triggers on 332,334,336 with about a five second delay. These events not only have the printer name(Param1) but also the DC(Param2) on which the update event is being performed. My thought is I can use the DC name in Param2 and run the Get-ADObject cmdlet with the -server switch and pass the same DC which is in the parameter. This way, you don’t really have to worry about replication. The PS script will be updating on the DC where the object was just created. If I get it working, I’ll post all the changes I made to make this happen. Let me know if you tried this and it didn’t work or if I’m missing something and you think that’s not a good idea.

    • That’s an awesome idea. Let me know how the testing goes, and I can update the post with credit to you. I haven’t done anything with printer management for a few years, so I won’t have much luck testing myself. Given the attention and feedback this post has collected over the years, I should probably do a bit of a rewrite with other people’s feedback included.

      Thanks for sharing!

      -Jason

      • jrp78 says:

        Jason, I did end up using my idea above BUT it still wasn’t 100% reliable 😦 I guess the race condition was still a problem. If I was more of a powershell guru, I can imagine that some logic testing to make the script wait based on some loop checking would work. In the end, I have scheduled tasks triggered on event ID’s 332 and 336(334 was not of use) with a seven second delay and also added the catch all script of yours to run on event 306 with a 1 minute delay. I have had this setup in place for 6 months with no issues.

Leave a comment