Error handling through the pipeline

This topic contains 8 replies, has 4 voices, and was last updated by Profile photo of Adnan Rashid Adnan Rashid 2 years, 4 months ago.

  • Author
    Posts
  • #17631
    Profile photo of JohnRock Bilodeau
    JohnRock Bilodeau
    Participant

    I was wondering if someone would be able to give me some tips on handling non terminating errors through the pipeline?

    Here is an example of what i would like to do.

    Get-ADUser -Filter {Enabled = 'False'} -Properties Modified | Where Modified -LT (Get-Date).AddDays(-30) | Remove-ADUser -Confirm:$false 

    Basically find all users that are disabled where the last modified time stamp is less than 30 days ago and delete them.
    So what i'm looking for is how to handle and error if one where to occur so that it could be logged

    I thought of using try catch, but if an error occurs then the entire command stops. I want it to be a non terminating error, so that it will continue to process all the users that it find, but then have a way to see if there where any error so that i can log them

    Any suggestions?

  • #17633
    Profile photo of Don Jones
    Don Jones
    Keymaster

    You really need to move from a one liner into a more structured script. The pipeline is great for quick, linear tasks. But when logic (IF and error, THEN this) starts to get involved, you've moved out of what the single-pipeline model is for.

  • #17636
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    Generally, to deal with non-terminating errors (where there's no expected terminating errors happening), I prefer to use the -ErrorVariable parameter. For example:

    Get-ADUser -Filter {Enabled = 'False'} -Properties Modified |
    Where Modified -LT (Get-Date).AddDays(-30) |
    Remove-ADUser -Confirm:$false -ErrorVariable removeUserErrors
    
    # Here, $removeUserErrors will be an empty collection if everything worked, or will contain
    # one or more ErrorRecord objects if any of the calls to Remove-ADUser failed.
    
    if ($removeUserErrors.Count -gt 0) {
        $removeUserErrors | Out-File some-File.txt
    }
    
  • #17638
    Profile photo of JohnRock Bilodeau
    JohnRock Bilodeau
    Participant

    Is there any specific advantage (say speed maybe) to doing this in a foreach loop or through the pipeline using the error variable?

    Is there a "best practice" or are both solution equally acceptable?

    I'm in the process of writing an AD user management script that handles account creation, modification, disabling, and deletion, and have a good chuck of it completed, so i want to be sure that my error handling is rock solid.

    Thanks

  • #17639
    Profile photo of Don Jones
    Don Jones
    Keymaster

    If you're going to be processing multiple objects, dealing with them individually in a ForEach loop lets you handle errors individually, and then loop back to process the next object. Internally, that's what most cmdlets do, and it's a common and legitimate model.

    Batch processing is for when you don't care about individual failures (eg., non-terminating), usually, or when you perhaps only need to capture all errors but not necessarily relate them back to an object or deal with them independently.

  • #17640
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    There's also a bit of a balancing act between memory utilization and execution time. Using the pipeline allows you to stream objects and not have to store the entire result set in memory at any one time, but tends to be slower than the foreach loop in many cases. AD throws another wrench in the gears, as pipelines that take too long to finish executing can wind up "timing out." The actual error message you'd see is something like "Invalid enumeration context", and it's something to keep in mind if your environment is large.

  • #17643
    Profile photo of JohnRock Bilodeau
    JohnRock Bilodeau
    Participant

    Thanks for the tips guys. I think I'll be using the ForEach loop.

  • #17664
    Profile photo of JohnRock Bilodeau
    JohnRock Bilodeau
    Participant

    Hey guys,

    I have a follow up question. In the script that i writing to delete users from AD, i have my ForEach loop that loops through each of the users that are disabled longer than a specific retention period, (say 30 days) to be consistent with the first example i posted.

    In this loop I query i get the Home Directory variable and split it on the \ to determine the file server and name of the shared folder
    Next using WMI I query the server to find out the physical path of the share.
    I then delete the share and the user account.

    So to delete the home directory folders and it's files from the server i was planing on using Invoke-Command....I work for a school board, and some home directories are on our main file server, but others are on our school server to avoid on unnecessary network traffic. I figured that while going through my foreach loop i could create a new psobject with the server name and physical folder location and store it to an array...this way once i'm out of that foreach loop I can create a new foreach loop that would loop through a unique list of servername and run invoke command only once on each server, thus reducing the number of remote sessions that i create and improve on the efficiency.

    .....So with all that said, I'm wondering when deleting multiple directories from a remote server using invoke-command what is the best was to handle errors.

    Here is the code i'm currently working on.

        If($RunDeletes){
            Write-Verbose "Checking for users that need to be deleted`n"
            
            try {
                # Query Active Directory for a list of disabled used in the Disabled user OU where the last modified date is less than the retention period specified in the Configuration.xml file (ex 120 days)
                $UsersToDelete = Get-ADUser -Filter {Enabled -eq "False"} -Properties EmployeeID, Modified, HomeDirectory -SearchBase $DisableOU -SearchScope Subtree -Server $DC | Where Modified -LT (Get-Date).AddDays(-($Configuration.Configuration.DisabledAccountRetentionPeriod))
            
            } catch {
    
                $_.Exception.Message | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
                "Unable to successfully Query Active Directory to find list of users to delete." | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
    
            }
        
            $FoldersToDelete = @()
    
            ForEach ($user in $UsersToDelete){
    
                Write-Verbose "`tQuerying Active Directory user $($user.Name), for home directory path." 
    
                # Get the Home directory variable from AD
                if(-not $user.HomeDirectory){
                    "The following user does not have a Home Directory specified." | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
                    Continue
                }
    
                # Split the Home directory variable into its two parts. Server name and share name.
                $ServerName = $user.HomeDirectory.Replace('\\','').Split('\')[0]
                if(-not $ServerName){
                    "No home directory server name could be found based on the Active Directory Information." | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
                    Continue
                }
    
                $ShareName = $user.HomeDirectory.Replace('\\','').Split('\')[1]
                if(-not $ShareName){
                    "No share name could be found based on the Active Directory Information." | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
                    Continue
                }
    
                Write-Verbose "`tQuerying Server: $ServerName for share named: $ShareName"
                $Share = Get-WmiObject -Class Win32_Share -ComputerName $ServerName -Filter "Name='$ShareName'"
    
                if(-not $Share){
                    "No share named : $ShareName could be found on the specified server : $ServerName." | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
                    Continue
                }
    
                $Path = $Share.Path
    
                $FoldersToDelete += New-Object PSObject -Property @{
                    ServerName = $ServerName
                    Path = $Path
                }
    
                Write-Verbose "`tDeleting Home Drive Share for $($user.Name)"
                try
                {
                     $Share.Delete()
                }
                catch
                {
                     $_.Exception.Message | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
                    "Unable to delete share name : $ShareName from server $ServerName" | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
                }
                finally
                {
                    $Share.Dispose()
                }
    
                Write-Verbose "`tDeleting Active Directory accounts identified for deletion"
                
                try
                {
                    # Delete the User Account in Active Directory
                    $user | Remove-ADUser -Confirm:$false -WhatIf
                }
                catch
                {
                     $_.Exception.Message | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
                    "Unable to delete Active Directory User : $($user.Name) - EmployeeID: $()" | Tee-Object -FilePath $ErrorLogPath -Append | Write-Error
                }
            
       
            }
    
            # Loop through each file server where home directories need to be deleted
            ForEach ($server in ($FoldersToDelete | Select -ExpandProperty ServerName -Unique)){
    
                # Get a list of folders that need to be deleted for the current home directory
                $FolderPaths = $FoldersToDelete | Where -Property ServerName -EQ $ServerName | Select -ExpandProperty Path
            
                Write-Verbose "`tDeleting the following home directories from $server.`n"
                $FolderPaths | ForEach{ Write-Verbose "`t`t$_"}
    
                Write-Verbose "`tInvoking command on remote server to delete the specified home directories"
                # Make a call to the server to have it delete the specified folders recursively
                Invoke-Command -ComputerName $server -ScriptBlock {Remove-Item -LiteralPath $FolderPaths -Recurse -Force}
            }
    
        }
    
  • #17728
    Profile photo of Adnan Rashid
    Adnan Rashid
    Participant

    I may be wrong here so someone else can correct me if so

    Regarding your question about error handling in invoke-command i believe you would handle that within the Script block so for example

    Invoke-Command -ComputerName $server -ScriptBlock {

    try {
    Remove-Item -LiteralPath $FolderPaths -Recurse -Force
    }
    catch {
    some exception text
    }

    }

    Something else you could also do is something like this incase the try catch does not work.

    Remove-Item -LiteralPath $FolderPaths -Recurse -Force -ErrorAction SilentlyContinue -ErrorVariable Testing
    $testing | Out-File c:\temp\error.log -Append

    so you would then write the errors to a log file and then you can read those over afterwards.

    Thanks

You must be logged in to reply to this topic.