Powershell While Loop not Working as Intended - powershell

So here's what I'm attempting to do:
I manually input a name, and then I want to get a list of users who work under the person whose name I input (extensionattribute9 is who the user works under). However, for each person that works under that person, I also want to run the process for them, and see if anyone works under them as well. I want this process to continue until no one works under the current user.
I've managed do to this up to 3 times without a while loop, but as I don't know how deep I would have to go to get everyone, I feel using a while loop would be better overall, especially in terms of code length.
Here is the code I currently have:
$users = Get-ADUser -Filter * -Properties extensionattribute9,Displayname,mail,title
$users | ForEach-Object {
if ($_.extensionattribute9 -like '*Lynn, *')
{
$_ | select Displayname,userprincipalname,title,extensionattribute9
$name = $_.Displayname
while ($_.extensionattribute9 -ne $null){ $users | ForEach-Object {
if ($_.extensionattribute9 -eq $name)
{
$_ | select Displayname,userprincipalname,title,extensionattribute9
$name=$_.Displayname
}
}
}
}
}
When I run the code I get a user (User A) under 'Lynn', and then a user under User A. After that, nothing. The program still continues to run, but nothing gets returned. I'm guessing it's stuck in an infinite cycle, but I don't know where better to put the while loop. Help?

It sounds like you are trying to do a recursive search with nested while/for-each loops which can end badly. You can try something like this:
Function Get-Manager {
param([Object]$User)
$ManagerName = $User.extensionattribute9
# Recursion base case
if ($ManagerName -eq $null){
return
}
# Do what you want with the user here
$User | select Displayname, userprincipalname, title, extensionattribute9
# Recursive call to find manager's manager
$Manager = Get-ADUser -Filter "Name -like $ManagerName"
Get-Manager $Manager
}
# Loop through all ADusers as you did before
$Users = Get-ADUser -Filter * -Properties extensionattribute9,Displayname,mail,title
Foreach ($User in $Users) {
Get-Manager $User
}
Please note I don't have experience using Powershell with Active Directory so the syntax may be incorrect, but shouldn't be hard to fix. Hope this helps!

I'm not familiar with Powershell, but one possible reason you're having trouble is that $_ is being used to mean two different things, depending on whether you use it inside the while loop or not. Is Powershell really smart enough to know what you mean?
More important: the code
$_ | select Displayname,userprincipalname,title,extensionattribute9
$name = $_.Displayname
appears in two places close together. This is a definite code smell. It should appear once and only once.
When you're traversing a hierarchy and you don't know how deep it will go, you must use a recursive algorithm (a function that calls itself). Here it is in pseudocode:
function myFunc ($node) {
//report the name of this node
echo "node $node found." ;
// check to see if this node has any child nodes
array $children = getChildNodes ($node) ;
if (count($children) == 0) {
//no child nodes, get out of here
return ;
}
//repeat the process for each child
foreach($children as $child) {
myFunc($child) ;
}
}

Related

Why does this Powershell ForEach loop get slower with each iteration?

My code is working as expected. I'm really just curious to know if anyone has any idea why what I describe below might be happening. That said, if anyone has any ideas for further optimising the routine, I'd gratefully accept them in the spirit of every day being a school day!
The script is querying all our domain controllers to get the most recent lastLogon attribute for all users in a particular OU. (I am doing this instead of using the lastLogonTimeStamp attribute because I need the absolute most recent logon as of the moment the script is run.)
During testing, to check the code was doing what I expected it to do, I added some console output (included in the snippet below).
When I did this I noticed that with each iteration of the SECOND ForEach ( $DC in $AllDCs ) loop, there was a noticeable pause before the nested loop wrote its first line of console output. The duration of the pause increased with each iteration of the outer loop, and the speed of the inner loop's subsequent output also dropped noticeably. Over the course of the run, looking at the output of a dozen or so DCs, I'd estimate the rate console lines were being written dropped by at least a factor of 4.
$AllDCs = Get-ADDomainController -Filter *
$AllRecords = #{}
ForEach ( $DC in $AllDCs ) {
$UserList = Get-ADUser -filter * -Server $DC -SearchBase $ini.OUDN.NewStartsInternal -SearchScope OneLevel -Properties lastLogon
$UserList = $UserList | Where { $_.SamAccountName -match $ini.RegEx.PayNo }
$AllRecords.Add($DC.Hostname,$UserList)
}
$Logons = #{}
ForEach ( $DC in $AllDCs ) { ; this loop is the one I'm talking about
ForEach ( $User in $AllRecords.$($DC.HostName) ) {
If ( $Logons.ContainsKey($User.SamAccountName) ) { ;this line amended on advice from mklement0
If ( $Logons.$($User.SamAccountName) -lt $User.lastLogon ) {
$Logons.$($User.SamAccountName) = $User.lastLogon
Write-Host "Updated $($User.SamAccountName) to $($User.lastLogon)" -ForegroundColor Green
} Else {
Write-Host "Ignored $($User.SamAccountName)"
}
} Else {
$Logons.Add( $User.SamAccountName , $User.lastLogon )
Write-Host "Created $($User.SamAccountName)" -ForegroundColor Yellow
}
}
}
I'm not really any under any time constraints here as we're only talking a couple hundred users and a dozen or so domain controllers. I've already reduced the runtime by a huge amount anyway. Previously it was looping through the users and querying every DC for every user one by one which, unsurprisingly, was taking far longer.
UPDATE:
I implemented mklement0's suggestion from the first comment below, and if anything the script is actually running more slowly than before. The delays between iterations of the outer loop are longer, and the output from the inner loop seems subject to random delays. On average I'd say the inner loop is getting through about 2 to 3 iterations per second, which to my mind is extremely slow for looping through data that is already held in local memory. I know PowerShell has a reputation for being slow, but this seems exceptional.
The script normally runs on a VM so I tested it on my own computer and it was a LOT slower, so this isn't a resource issue with the VM.
UPDATE 2:
I removed all the Write-Host commands and simply timed each iteration of the outer loop.
First of all, removing all the console writes increased performance dramatically, which I expected, although I didn't realise by how much. It easily cut the run time to a fifth of what it had been.
In terms of the loop times, the strange behaviour is still there. Out of twelve iterations, the first seven are done within 1 second, and getting through the final five takes about 35 seconds. This behaviour repeats more or less the same every time. There is nothing different about the hostnames of the final five DCs compared to the first seven that may be slowing down the hashtable lookup.
I'm very happy with the run time as it is now, but still utterly perplexed about this weird behaviour.
So, this is my take on your code, I didn't change many things but I have a feeling this should be a bit faster but I may be wrong.
Note, I'm using LDAPFilter = '(LastLogon=*)' instead of Filter = '*' because, if it's an attribute that is not replicated accross the domain it might save time when querying each Domain Controller. Change it back to Filter = '*' if that didn't work :(
It should avoid bringing users without LastLogon attribute set which could save some time.
$AllDCs = Get-ADDomainController -Filter *
$logons = #{}
$params = #{
LDAPFilter = '(LastLogon=*)' # Use this instead if that didn't work => Filter = '*'
Server = ''
SearchBase = $ini.OUDN.NewStartsInternal
SearchScope = 'OneLevel'
Properties = 'lastLogon'
}
foreach($DC in $AllDCs) {
$params.Server = $DC
$UserList = Get-ADUser #params
foreach($user in $UserList) {
if($user.samAccountName -notmatch $ini.RegEx.PayNo) {
continue
}
if($logons[$user.samAccountName].LastLogon -lt $user.LastLogon) {
# On first loop iteration should be always entering
# this condition because
# $null -lt [datetime]::MaxValue => True AND
# $null -lt [datetime]::MinValue => True
# Assuming $user.LastLogon is always a populated attribute
# which also explains my LDAPFilter = '(LastLogon=*)' from before
$logons[$user.samAccountName] = $user
}
}
}

Is there a way to capture which input objects have no results in Get-ADUser while using Filter?

I have a list of potential account ids. I would like to use Get-ADUser to query if the account or a variation of it exists in the environment. I would then like to capture the data including which accounts ids from my original list don't have any accounts in the environment.
I have successfully captured data for account ids that have an account or a variation of the account id in the AD environment. I am having difficultly with capturing the account ids from my original list that do not produce any results using Get-ADUser
foreach ($user in $inputdata)
{$user = $user + "*"
$(try {Get-ADUser -filter {SamAccountName -like "$user"} -properties Description} catch {$null}) | % {if ($_ -ne $null) {[pscustomobject]#{"ID"=$_.SamAccountName;"DN"=$_.DistinguishedName;"Desc"=$_.Description}}
else {$noaccount += $user}
}
My pscustomobject populates properly with data from everyone that does have an account. But there are no values in $noaccount even though there are ids in my list that do not have accounts in the environment. What should I do to capture the instances which do not have accounts using Get-ADUser?
Also, no error is outputted.
The following should achieve what you want.
$noaccount = [Collections.Generic.List[String]] #()
foreach ($user in $inputdata) {
$userToCheck = Get-ADUser -Filter "SamAccountName -like '$user*'" -properties Description
if ($userToCheck) {
[pscustomobject]#{"ID"=$userToCheck.SamAccountName
"DN"=$userToCheck.DistinguishedName
"Desc"=$userToCheck.Description
}
}
else {
$noaccount.Add($user)
}
}
Explanation:
$noaccount is initialized as a generic list of strings so that we can use the .Add() method rather than the inefficient += operator.
$userToCheck will contain a found user object or $null depending on whether the query found a result. If a user is found, the if condition is $true and your custom object is output. If no user is found, the else condition is triggered and the data stored in $user is added to the $noaccount collection.
I changed the -Filter slightly to remove the script block notation because it is not a script block. The online documentation of the command teaches bad habits by demonstrating the use of script block notation. Instead the filter should be surrounded by double quotes with the values on the inside surrounded by single quotes. The double quotes will allow for PowerShell interpolation to expand variable within. The single quotes will be passed in literally so that the value is interpreted as a string by Get-ADUser.
With your attempt, the try {} block would rarely throw an error and would not throw an error just because an account was not found. You would have to remove the -Filter in favor of the -Identity parameter to produce errors when no object is found. You will still see errors if there are connectivity issues between your session and the domain server though. When your Get-ADUser command produced no output, nothing would get piped into the the foreach {} script block. Therefore, your if {} else {} would never be evaluated.
Enhancement Considerations:
Following some insight provided by Lee_Dailey, instead of adding the not found accounts to a separate collection, you could incorporate them into your custom object output. Maybe you could add a new property that states whether or not they are found. See below for an example:
$noaccount = [Collections.Generic.List[String]] #()
foreach ($user in $inputdata) {
$userToCheck = Get-ADUser -Filter "SamAccountName -like '$user*'" -properties Description
if ($userToCheck) {
[pscustomobject]#{"User" = $user
"ID"=$userToCheck.SamAccountName
"DN"=$userToCheck.DistinguishedName
"Desc"=$userToCheck.Description
"In_AD" = "Yes"
}
}
else {
[pscustomobject]#{"User" = $user
"ID"=$null
"DN"=$null
"Desc"=$null
"In_AD" = "No"
}
}
}

handle ADIdentityNotFoundException without stopping the program

I have to loop for each object in an input file, doing a Get-ADUser on each object, and I want to handle the ADIdentityNotFoundException error without stopping the loop. Is there a better way I can accomplish this (simplified for example's sake):
Import-Csv $input | Foreach-Object {
$manager = "BLANK"
if ($user = Get-ADUser $_."samaccountname" -properties * ) {
# I don't think I need this in an IF{} since the line below won't work
# so $manager will be equal to the last value set, "BLANK", but
# this makes it easier to understand what I want to happen
$manager = $user."manager"
# I need more properties (thus -properties *) but again just an example
}
}
In essence, if the Get-ADUser lookup is successful, set $manager = $user."manager"
If it is not successful, do not stop the loop, do not copy the value of the previous user, have $manager = "BLANK"(or whatever). The issue I have with the try/catch solutions, is that ADIdentityNotFoundException doesn't trigger the catch unless I add -ErrorAction Stop, which will cause the undesired result of the program terminating.
I'm not sure why your program would be terminating. Using the below sample code loops through all the users in the array. I have purposely entered an incorrect username into the second value of the array (position [1]):
$users = "username1", "username2", "username3" #username2 is purposely incorrect
foreach ($i in $users){
try{
$user = Get-ADUser -Identity $i -Properties * -ErrorAction Stop
Write-Host "Found"
}catch{
Write-Host "Not found"
}
}
my output is
found
not found
found

ADSI/System.DirectoryServices.DirectorySearcher result parsing

A trivial question, but hopefully really obvious for those who know.
Search constructor:
$Search = New-Object System.DirectoryServices.DirectorySearcher
(([adsi]"LDAP://ou=Domain Users,dc=example,dc=pri"),'(objectCategory=person)',
('name','employeeID'))
I want to exclude results where the employeeID attribute does not exist.
This works:
$users = $Search.FindAll()
ForEach ($u in $users) {
If ($u.properties.employeeid) {
Write-Host $($u.properties.name)
}
}
The following does not work - no output. However, when the IF statement is omitted, results are output.
ForEach ($user in $($Search.FindAll())) {
If ($user.properties.employeeID) {
Write-Host $($user.properties.name)
}
}
Is it a syntax issue in the second example, or do I just need to temporarily store results in an object before running conditional statements on them?
(To circumvent any discussion on why not use the ActiveDirectory module and Get-ADUser, it's for a user that cannot have the module installed on their workstation, nor be granted perms to invoke it via a PSSession on a host where it is installed.)
Update: found a slightly nicer way of doing the where clause:
$searcher.FindAll() | where { ($_.properties['employeeid'][0]) }
Just remove if statement and filter search results:
$users = $Search.FindAll() | Where-Object {-not [string]::IsNullOrEmpty($_.properties.employeeID)}

Would this work better with loops rather than functions?

The following code works, but I'm interested in knowing if there would have been a more efficient way of writing the script, may be by using loops, or it this is the correct way to write such a script? The problem I had when trying to use loop statements was that I couldn't work out how to put the $Dialog "OK" into a loop where it can then loop back to itself if the IP Address still wasn't valid.
The idea of the script is to get the (first three octets of an) IP address, and see if it's valid (i.e not 0.0.0.0 or 169.254.*) before storing it as a variable, and if it isn't valid to throw a dialog box to give to administrator the opportunity to correct it, and then check again, and so on.
function Check-IP
{
$IPSiteAddress = Get-IPAddress
if ($IPSiteAddress -like "0.*" -or $IPSiteAddress -like "169.254.*") {DialogBox-IP}
}
function Get-IPAddress
{
(Get-WmiObject Win32_NetworkAdapterConfiguration |
Where { $_.IPAddress } |
Select -Expand IPAddress).split('.')[0..2] -join '.'
}
function DialogBox-IP
{
$IPDialog = [System.Windows.Forms.MessageBox]::show( "This computer doesn't have a valid IP Address.
Please correct the IP Address and click OK, or click Cancel to exit.","No Network Connection",1)
if ($IPDialog -eq "OK") {Check-IP} else {exit}
}
Check-IP
$IPSiteAddress = Get-IPAddress
If anyone has a nicer solution or, any thoughts, I'll love to hear them
function Check-IP
{
param ($IPSiteAddress)
return !($IPSiteAddress -like "0.*" -or $IPSiteAddress -like "169.254.*")
}
function Get-IPAddress
{
(Get-WmiObject Win32_NetworkAdapterConfiguration | Where { $_.IPAddress } | Select -Expand IPAddress).split('.')[0..2] -join '.'
}
while (!(Check-IP Get-IPAddress))
{
DialogBox-IP
}