Finding computer names in a hashtable

Brass Contributor

Hi,

I need a script that can find a list of computers from Active Directory stored in a hashtable and return the CanonicalName when there is a match. I know how to get all AD computers from domain controller that has a global catalog and import a list of computers from a text file but I'm not sure how you search for computers from a list against the hashtable.

 

Import-Module ActiveDirectory
#DC with global catalog
$server = "contoso.com:3268"
#Get all computers from active directory
$allADComputers = Get-ADComputer -Filter * -Properties Name, CanonicalName -Server $server | Where-Object { $_.OperatingSystem -notlike "*Server*" -and $_.CanonicalName -notlike "*Server*" -and $_.CanonicalName -notlike "*Recycle*" -and $_.CanonicalName -notlike "*Domain*" -and $_.CanonicalName -notlike "*Service Accounts*" } | Select-Object Name, CanonicalName | Sort-Object Name
#Get computers from text file
$computers = Get-Content -Path "$PSScriptRoot\computers.txt"

 

I could probably use foreach loops to step through every item in the array and hashtable but I would expect that approach to be very inefficient. What is an efficient way to find computers from a text file in the hashtable? I have done some searching without much luck so far.

 

Thank you!

Rob

5 Replies
Can you please tell me how to post projects on this website?
Testing with this additional code, I can find out if a computer is in the hashtable but this might not be very efficient:
foreach ($computer in $computers) {
$found = $false
foreach($name in $allADComputers.Name) {
if($computer -eq $name) {
Write-Host "match"
$found = $true
}
}
if(-not($found)) {
Write-Host "$($computer): no match"
}
}

This code block finds a computer in the hashtable but the CanonicalName value includes the CanonicalName for every computer in Active Directory. So, each computer found has 100s of CanonicalNames equal to each computer that is in the hashtable.

 

 

$report = @()
foreach ($computer in $computers) {

     foreach($name in $allADComputers.Name) {
          if($computer -eq $name) {
               $report += New-Object -TypeName PSObject -Property @{"ComputerName" = $computer;"ActiveDirectoryPath" = $allADComputers.CanonicalName}
               
          } 
     }
}

 

 

How do I get this to set the CanonicalName one time for the computer that is found?

Hi @robmo,

 

With the following statement, you're dumping all CanonicalNames indeed.

$allADComputers.CanonicalName

 I think this should do the trick:

$report = @()
foreach ($computer in $computers) {
     foreach($comp in $allADComputers) {
          if($computer -eq $comp.Name) {
               $report += New-Object -TypeName PSObject -Property @{"ComputerName" = $computer;"ActiveDirectoryPath" = $comp.CanonicalName}
               
          } 
     }
}

In the second foreach loop you'll retrieve the full record and only match on name in the if statement. In the report you can make use of the CanonicalName value for that specific computer entry.

 

Regards.

Ruud

@robmo 

 

Before jumping into it, it's worth calling out that there are no HashTable objects involved here, just plain old-fashioned Array objects - which is what you get out of calls to Get-ADComputer and the likes (unless there's zero or one results - but I won't deep-dive that here.)

 

Here's an illustration to make the point:

 

LainRobertson_0-1673855254894.png

 

The green-boxed area is an actual hashtable. The red-boxed area is what you get from the commandlets, which is an array.

 

The reason it's important to call this out is that if you're really looking for efficiency, you need to consider which .NET classes are best-suited to your requirements, and this is almost never the PowerShell defaults of System.Array or System.Collections.HashTable. But then, it also usually doesn't matter since most people aren't running sufficient volumes of iterations for their daily administrative tasks.

 

PowerShell is built for ease-of-use ahead of efficiency. But that doesn't mean you can't do simple things that can scale out to make a big difference.

 

One of those things is using server-side filtering wherever you can ahead of client-side filtering, which is something you should be using in your Get-ADComputer command.

 

While you might be running this particular query on-premise with particularly low latencies, by the time you get to working with cloud environments like Azure AD and Graph, you want to be leveraging server-side filtering every chance you get (particularly true when you're dealing with ~80 ms round trips as we do here in Australia.)

 

Next, you don't want to hold your entire directory's worth of computers in a local variable if there's no need. That approach is a quick way to exhaust client resources.

 

Instead, pipe the results through to the subsequent code blocks so that once that particular computer object has been processed, it can be garbage-collected from memory on the next cycle. This particular tip is easily the most impacting thing you can do to maintain performance when you start working with hundreds-of-thousands of objects. But again, if you're talking only a thousand objects, it's  in the "who cares how you do it" category. It's more about forming good habits.

 

I'm going to forgo the exploration of case sensitivity relates to your example (specifically, in the .NET context) and just jump straight to the PowerShell suggestion:

 

 

Import-Module ActiveDirectory
#Get computers from text file
$Computers = Get-Content -Path "$PSScriptRoot\computers.txt"

#DC with global catalog
$Server = "contoso.com:3268"

#Get all computers from active directory
$Results = Get-ADComputer -Filter { (operatingSystem -notlike "*server*") } -Properties name, canonicalName -Server $Server |
    Where-Object { ($_.canonicalName -notmatch "server|recycle|domain|service.accounts") -and ($Computers -contains $_.name) } |
        Select-Object Name, CanonicalName;
    
$Results | Sort-Object -Property Name;

 

 

I haven't bothered assigning the sorted output (line 13) to a variable, but you could if there was a need for further processing.

 

Notice that the "$_.OperatingSystem -notlike "*Server*"" has been moved over into the server-side "-Filter" clause. Because canonicalName is a constructed attribute, it cannot be moved across and has to remain as a client-side filter. But the general rule of thumb is to move whatever you can to server-side filtering.

 

I've also done the text file-based filtering in the same "Where-Object" clause - there's no need for any additional loops (though clearly the "-contains" statement effectively does this under the hood.)

 

Normally, I don't sort objects within PowerShell unless they're smaller result sets (under 1,000 is my personal habit but there's no science behind that), but since you have it, I've included it.

 

The problem with commandlets like Sort-Object, "Format-Table -AutoSize" and others is that they cause all the data to be pinned into memory until all the commandlets feeding into them are complete, which leads to the scaling issue mention above.

 

What I have done to limit the impact is decouple the Get-ADComputer from the Sort-Object, meaning the size of the data set held in memory will only grow proportionally to how many entries exist in your text file holding the computer names.

 

Leaving out the sort altogether would be something to consider if you had ten or hundreds of thousands of objects, but I doubt that's the case, so by all means leave it in.

 

Cheers,

Lain