Jump to content
Sign in to follow this  
niftyapple

Performance Enhancements

Recommended Posts

niftyapple

Hello all!

First time poster here. Hope you all can assist. I dont really have an issue, more of a request for suggestions.

I have been able to use the AD UDF to search my companies AD servers for the list of all AD servers, and check an ID if it is locked on each server. My question for you all is, the script takes almost 4 minutes to run against all 76 of the AD servers through out the company. Is there any way to speed this up?

We have other utils that complete this task, but they are really annoying to use, and I decided to make my own.

The below code is operational and does work great! It reports number of servers the user was locked on, how many servers were checked, time it took to complete. My only complaint is the time. Thoughts?

#include <AD.au3>
#include <array.au3>

Global $aDC, $i, $IDExists, $user, $begin, $dif, $ServerErrors, $Lines, $Time, $UnlockedOn

;Ask for the network login of the user
Func AskUser()
   $user = InputBox("Username", "Enter the Network Login of the person that needs unlocked.")
EndFunc

;call function to prompt for ID, make sure the ID given actually exists
_AD_Open()
While $IDExists = 0
   AskUser()
   If @error Then Exit
   $IDExists = _AD_ObjectExists($user)
WEnd

;Time this scripts actions
$begin = TimerInit()

; Create a connection to the AD servers and request the list of all Domain Controllers
If @error Then MsgBox(16, "AD Error", "_AD_Open failed - Error = " & @error & ", @extended = " & @extended)
$aDC = _AD_ListDomainControllers()
If @error Then Exit MsgBox(16, "AD DC Error", "Error = " & @error)
_AD_Close()

;Count the number of domain controllers available, used in For Loop calculation
$Lines = Ubound($aDC)

;Set the number of servers unlocked on value to 0, being we haven't done any work yet.
$UnlockedOn = 0
$ServerErrors = 0

Func IDCheck()
If _AD_IsObjectLocked($user) = 1 Then
   $UnlockedOn = $UnlockedOn + 1
   _AD_UnlockObject($user)
EndIf
EndFunc

;Assume no errors on number of servers before beginning loop
$ServerErrors = 0

;For loop that opens a connection to each server and checks if the account is locked. If so, unlocks it.
For $i = 1 to $Lines - 1
   _AD_Open("", "", "", $aDC[$i][2])
   ;if server failed to connect, report it in a tally
   If @error Then $ServerErrors = $ServerErrors + 1
   IDCheck()
   _AD_Close()
Next

;Stop the timer, check, format and see how long this took
$dif = TimerDiff($begin)
Number($dif)
$dif = $dif / 1000

If $dif > 60 Then 
   $Time = $dif / 60 
   $Time = Round($Time, 2)
   $Time = $Time & " minutes"
Else
   $Time = Round($Time, 2)
   $Time = $Time & " seconds"
EndIf

MsgBox(16, "Total Unlocked servers", "The user " & $user & " has now been unlocked." & @CRLF & @CRLF & $UnlockedOn & "/" & $Lines - 1 & " total servers unocked on."& @CRLF & @CRLF & "These actions were completed in about " & $Time & @CRLF & @CRLF & "Number of servers that caused errors: " & $ServerErrors)

Share this post


Link to post
Share on other sites
water

Are the DCs all in the same domain?

Anything special in your domain about replication?


My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (NEW 2018-06-01 - Version 1.4.9.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX (2018-01-27 - Version 1.3.3.1) - Download - General Help & Support - Example Scripts - Wiki
ExcelChart (2015-04-01 - Version 0.4.0.0) - Download - General Help & Support - Example Scripts
Excel - Example Scripts - Wiki
Word - Wiki
PowerPoint (2015-06-06 - Version 0.0.5.0) - Download - General Help & Support

Tutorials:
ADO - Wiki

 

Share this post


Link to post
Share on other sites
niftyapple

Are the DCs all in the same domain?

Anything special in your domain about replication?

All are in the same domain

No specialities on replication (that I am aware of) other than when changing in one server it could take a few hours for it to reach them all. Hence the need for this script.

Share this post


Link to post
Share on other sites
JLogan3o13

Are they spread out geographically? A change to a DC in the same location should be almost instantaneous; remote sites should be as fast as your bandwidth (and the needs of the site) allow. Even a remote site that only serves as a hub I would expect no more than 60 minutes. I would be curious if it is design that is causing the delay in your case, or something else.


√-1 2^3 ∑ π, and it was delicious!

Share this post


Link to post
Share on other sites
water

A locked user is immediately propagated to the Global Catalog PDC (so the user can't log on by connecting to another DC).

Hence it is sufficient to query the GC PDC for the locked user. How to connect to the GC can be found in the Wiki entry for the AD UDF (link can be found in my signature).

How to connect to the PDC:

  • Do a simple _AD_Open to the domain
  • Run _AD_ListRoleOwners to retrieve a list of all roles in the domain (element 1 of the returned array is the PDC)
  • Do _AD_Close
  • Do _AD_Open and connect to the PDC
  • Run _AD_IsObjectLocked

Untested. So please be careful and test carefully.

Edit: It's not the GC it's the PDC according to this MS document (section "Urgent Replication of Account Lockout Changes")

Edited by water

My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (NEW 2018-06-01 - Version 1.4.9.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX (2018-01-27 - Version 1.3.3.1) - Download - General Help & Support - Example Scripts - Wiki
ExcelChart (2015-04-01 - Version 0.4.0.0) - Download - General Help & Support - Example Scripts
Excel - Example Scripts - Wiki
Word - Wiki
PowerPoint (2015-06-06 - Version 0.0.5.0) - Download - General Help & Support

Tutorials:
ADO - Wiki

 

Share this post


Link to post
Share on other sites
niftyapple

Is it possible to put each IDCheck() function call on its own thread? Seen some posts over multithreading, and that its possible to an extent. Would that help? Is this a scenarios that this could actually be beneficial? Havent tried that kind of method, not sure I am getting the basics behind it quite yet.

I know where the long completion time is coming from, having to deal with latency between client and AD server in each location across the world. If this was threaded, we could make all the requests near simultaneously and wait for them all to respond?

@MVPs I have tried this method, it still does take 10-15 minutes to replicate IF the user is locked on this server, not always will be locked there first. If the person is located in a different region, they would be communicating with the local AD server, thus the need to check each individual box.

Share this post


Link to post
Share on other sites
water

The link that I posted says:

"An account lockout is urgently replicated to the PDC emulator"

and

"In addition, when authentication fails at a domain controller other than the PDC emulator, the authentication is retried at the PDC emulator. For this reason, the PDC emulator locks the account before the domain controller that handled the failed-password attempt if the bad-password-attempt threshold is reached."

So it should be sufficient to query the PDC as I described above.


My UDFs and Tutorials:

Spoiler

UDFs:
Active Directory (NEW 2018-06-01 - Version 1.4.9.0) - Download - General Help & Support - Example Scripts - Wiki
OutlookEX (2018-01-27 - Version 1.3.3.1) - Download - General Help & Support - Example Scripts - Wiki
ExcelChart (2015-04-01 - Version 0.4.0.0) - Download - General Help & Support - Example Scripts
Excel - Example Scripts - Wiki
Word - Wiki
PowerPoint (2015-06-06 - Version 0.0.5.0) - Download - General Help & Support

Tutorials:
ADO - Wiki

 

Share this post


Link to post
Share on other sites
niftyapple

OK, after a little while of racking my brain over the reason why this was taking so long, I have implemented a "server blacklist" persay as a fix. Heres a little more info.

I was unsure what was causing the long drag, so I started to document each servers response times using timerdiff() in a consolewrite() fashion. When executing, I found multiple servers that were spitting out a special set of errors. Those special errors, leads me to the default settings of windows.

It appears that windows will wait a full 30 seconds to connect to a domain controller, this put in place for super high latency environments. Being some of the controllers in my org are IP listed to communicate with only other AD servers for performance, this causes an issue with my local machine tries to connect from the AD server list it pulls all AD server names, and waits for the full 30 seconds for each one of these servers which refuse to communicate. 5 servers, 30 seconds, thats 2.5 minutes wait time right there.  

To remedy that, I implemented a blacklist check loop. Over the 70+ servers in operation, 5 on a hardcoded blacklist, the processing time is now < 1 minute top to bottom. :)

Final note, just havent done it yet, but will add in a status update ticker, and a txt file based blacklist and even more errorlogging to it later, but for now, works like a beaut!

@Mods, you can close this thread, at 75% reduction in processing time, I am satisfied with this topics closing.

Here is the code for anyone to use if they like.

#include <AD.au3>
#include <array.au3>

Global $aDC, $i, $IDExists, $user, $begin, $dif, $ServerErrors, $Lines, $Time, $UnlockedOn, $lockedstatus, $performunlock, $ServersUnlocked, $TimeToUnlockBegin, $TimeToUnlockEnd, $TimeToUnlockTotal, $stringCompare, $b, $s, $f, $q, $skiplines

; Creates an array, this array used for skipping servers known to cause problems to this process.
Local $skip[7] = [6, "server.com", "anotherserver.com", "againaserver.com", "notanotherserver.com", "whatamigoingtodowithalltheseservers.com", "theend.com"]

;Ask for the network login of the user
Func AskUser()
   $user = InputBox("Username", "Enter the Network Login of the person that needs unlocked.")
   If @error Then Exit
EndFunc

;call function to prompt for ID, make sure the ID given actually exists
_AD_Open()
; Create a connection to the AD servers and request the list of all Domain Controllers
If @error Then MsgBox(16, "AD Error", "_AD_Open failed - Error = " & @error & ", @extended = " & @extended)

; set an array of all returned AD controllers in the Domain
$aDC = _AD_ListDomainControllers()
If @error Then Exit MsgBox(16, "AD DC Error", "Error = " & @error)

; Prompts the user for input of the users ID and will continue to prompt until a valid ID is entered.
While $IDExists = 0
   AskUser()
   If @error Then Exit
   $IDExists = _AD_ObjectExists($user)
WEnd

;Time this scripts actions
$begin = TimerInit()

; no longer needing connection to the current server, close it. Will be directly connected to later in the unlock process
_AD_Close()

;Count the number of domain controllers available, used in For Loop calculation
$Lines = Ubound($aDC)

;Set the value of variables to 0 to begin calculations
$UnlockedOn = 0
$ServerErrors = 0

; Function for checking if the server is on the "blacklist"
Func checkserver($s)
   $f = Ubound($skip) - 1
   For $b = 1 to $f
      $c = StringCompare($s, $skip[$b], 2)
      if $c = 0 Then 
         Return 1
         $b = $f
      EndIf
   Next
EndFunc

; Checking reveals the user is indeed locked, this completes the unlock.
Func UnlockAccount()
   $UnlockedOn = $UnlockedOn + 1
   $performunlock = _AD_UnlockObject($user)
   If @error=1 Then MsgBox(16, "user", "User " & $user & " does not exist on " & $aDC[$i][2] )
   If @error="x" Then MsgBox(16, "user", "You do not have permission to set " & $user & " to an unlocked status on " & $aDC[$i][2] )
EndFunc

; used for error logging a server status check when running from the editor console
Func Status()
   ConsoleWrite("Servers unlocked on: " & $ServersUnlocked & " - Server completed: " & $aDC[$i][2] & @CRLF)
   ; future plan to add a gui as well
EndFunc

Func timeelapsed()
   ;Check the timer, format and output how long this took
   $dif = TimerDiff($begin)
   Number($dif)
   $dif = $dif / 1000

   If $dif > 60 Then 
      $Time = $dif / 60 
      $Time = Round($Time, 2)
      $Time = $Time & " minutes"
   Else
      $Time = $dif
      $Time = Round($Time, 2)
      $Time = $Time & " seconds"
   EndIf
EndFunc

;Assume no errors on number of servers before beginning loop
$ServerErrors = 0

;For loop that opens a connection to each server, and checks if the account is locked. If so, unlocks it.
For $i = 1 to $Lines - 1
      ;Timer to see how fast a series of commands reacts
      $TimeToUnlockBegin = TimerInit()
      ; check if the connection server is on the "blacklist", skip if it is
      ConsoleWrite("Checking Server " & $aDC[$i][2] & @CRLF )
      $q = checkserver($aDC[$i][2])
      ; complete the work behind the connection, and if the account is locked.
      if $q = 0 Then
         ;Connect to the server, if server failed to connect, report it in a tally
         _AD_Open("", "", "", $aDC[$i][2])
         If @error Then 
            $ServerErrors = $ServerErrors + 1
            ConsoleWrite("Server " & $aDC[$i][2] & " appears to be having some issues." & @CRLF)
         Else
            ; check if the user is locked, if it is, unlock it 
            $lockedstatus = _AD_IsObjectLocked($user)
            if $lockedstatus = 1 Then 
              UnlockAccount()
              $ServersUnlocked = $ServersUnlocked + 1
              Status()
            Else
              if $lockedstatus = 0 then 
                 $ServersUnlocked = $ServersUnlocked + 1
                 Status()
              EndIf
            EndIf
         EndIf
         _AD_Close()
      EndIf
      $TimeToUnlockTotal = TimerDiff($TimeToUnlockBegin)
      $TimeToUnlockTotal = $TimeToUnlockTotal / 1000
      $TimeToUnlockTotal = Number($TimeToUnlockTotal)
      $TimeToUnlockTotal = Round($TimeToUnlockTotal, 2)
      ; more logging for the console
      ConsoleWrite($TimeToUnlockTotal & " seconds to complete " & $aDC[$i][2] & @CRLF)
      timeelapsed()
      ConsoleWrite("Elapsed Time: " & $Time & @CRLF)
Next

; give a simple status report for the scripts processing. Later this will become obsolete when a GUI is implemented
MsgBox(16, "Total Unlocked servers", "The user " & $user & " has now been unlocked." & @CRLF & @CRLF & $UnlockedOn & "/" & ($Lines - 1) - $f & " total servers unocked on."& @CRLF & @CRLF & "These actions were completed in about " & $Time & @CRLF & @CRLF & "Number of servers that caused errors: " & $ServerErrors)

Share this post


Link to post
Share on other sites
niftyapple

Slight update: File based blacklist, and misc other small code tweaks

; Created by niftyapple
; Version 1.1 - Stable

#include <AD.au3>
#include <array.au3>
#include <File.au3>

Global $aDC, $i, $IDExists, $user, $begin, $dif, $ServerErrors, $Lines, $Time, $UnlockedOn, $lockedstatus, $performunlock, $ServersUnlocked, $TimeToUnlockBegin, $TimeToUnlockEnd, $TimeToUnlockTotal, $stringCompare, $b, $s, $f, $q, $skiplines, $BL_FolderName, $skip

;------------------------------
; Blacklist:
$BL = "Unlock\"
$BL_File = "blacklist.txt"
$BL_FolderName = @AppDataDir & "\" & $BL

; check for blacklist file if exists
if FileExists( $BL_FolderName ) <> 1 then 
   DirCreate ( $BL_FolderName )
   NotifyUserFileCreate()
Elseif FileExists( $BL_FolderName & $BL_File ) <> 1 then 
   NotifyUserFileCreate()
EndIf

; Informs user that a blank file for blacklist had to be created and requests input. Used in first time use, or when the config has been deleted.
Func NotifyUserFileCreate()
   _FileCreate ( $BL_FolderName & $BL_File )
   MsgBox (1, "Blacklist Warning", "The blacklist file was not present and had to be created." & @CRLF & @CRLF & "See: " & $BL_FolderName & $BL_File & @CRLF & @CRLF & "Please enter in the blacklist servers now. NOTE: 1 per line.", 60 )
   Run("Explorer.exe " & $BL_FolderName )
   if @error then 
      Run("Explorer.exe " & $BL_FolderName )
      Exit
   EndIf
   Exit
EndFunc

; get contents of blacklist file, 
_FileReadToArray( $BL_FolderName & $BL_File, $skip )

if @error then MsgBox(0, "Warning", "Blacklist servers file could not be read.")

; in the event that the blacklist file is blank, create a dull array so the application wont error from a calculation of 1 ÷ 0
if $skip = 0 then 
   Local $skip[2]
   $skip[0] = 1
   $skip[1] = "no domains available"
EndIf

;Ask for the network login of the user
Func AskUser()
   $user = InputBox("Username", "Enter the Network Login of the person that needs unlocked.")
   If @error Then Exit
EndFunc

;call function to prompt for ID, make sure the ID given actually exists
_AD_Open()
; Create a connection to the AD servers and request the list of all Domain Controllers
If @error Then MsgBox(16, "AD Error", "_AD_Open failed - Error = " & @error & ", @extended = " & @extended)

; set an array of all returned AD controllers in the Domain
$aDC = _AD_ListDomainControllers()
If @error Then Exit MsgBox(16, "AD DC Error", "Error = " & @error)

; Prompts the user for input of the users ID and will continue to prompt until a valid ID is entered.
While $IDExists = 0
   AskUser()
   If @error Then Exit
   $IDExists = _AD_ObjectExists($user)
WEnd

;Time this scripts actions
$begin = TimerInit()

; no longer needing connection to the current server, close it. Will be directly connected to later in the unlock process
_AD_Close()

;Count the number of domain controllers available, used in For Loop calculation
$Lines = $aDC[0][0]

;Set the value of variables to 0 to begin calculations
$UnlockedOn = 0
$ServerErrors = 0

; Function for checking if the server is on the "blacklist"
Func checkserver($s)
   $f = $skip[0]
   For $b = 1 to $f
      $c = StringCompare($s, $skip[$b], 2)
      if $c = 0 Then 
         Return 1
         $b = $f
      EndIf
   Next
EndFunc

; Checking reveals the user is indeed locked, this completes the unlock.
Func UnlockAccount()
   $UnlockedOn = $UnlockedOn + 1
   $performunlock = _AD_UnlockObject($user)
   If @error=1 Then MsgBox(16, "user", "User " & $user & " does not exist on " & $aDC[$i][2] )
   If @error="x" Then MsgBox(16, "user", "You do not have permission to set " & $user & " to an unlocked status on " & $aDC[$i][2] )
EndFunc

; used for error logging a server status check when running from the editor console
Func Status()
   ConsoleWrite("Servers unlocked on: " & $ServersUnlocked & " - Server completed: " & $aDC[$i][2] & @CRLF)
   ; future plan to add a gui as well
EndFunc

Func timeelapsed()
   ;Check the timer, format and output how long this took
   $dif = TimerDiff($begin)
   Number($dif)
   $dif = $dif / 1000

   If $dif > 60 Then 
      $Time = $dif / 60 
      $Time = Round($Time, 2)
      $Time = $Time & " minutes"
   Else
      $Time = $dif
      $Time = Round($Time, 2)
      $Time = $Time & " seconds"
   EndIf
EndFunc

;Assume no errors on number of servers before beginning loop
$ServerErrors = 0

;For loop that opens a connection to each server, and checks if the account is locked. If so, unlocks it.
For $i = 1 to $Lines
      ;Timer to see how fast a series of commands reacts
      $TimeToUnlockBegin = TimerInit()
      ; check if the connection server is on the "blacklist", skip if it is
      ConsoleWrite("Checking Server " & $aDC[$i][2] & @CRLF )
      $q = checkserver($aDC[$i][2])
      ; complete the work behind the connection, and if the account is locked.
      if $q = 0 Then
         ;Connect to the server, if server failed to connect, report it in a tally
         _AD_Open("", "", "", $aDC[$i][2])
         If @error Then 
            $ServerErrors = $ServerErrors + 1
            ConsoleWrite("Server " & $aDC[$i][2] & " appears to be having some issues." & @CRLF)
         Else
            ; check if the user is locked, if it is, unlock it 
            $lockedstatus = _AD_IsObjectLocked($user)
            if $lockedstatus = 1 Then 
              UnlockAccount()
              $ServersUnlocked = $ServersUnlocked + 1
              Status()
            Elseif $lockedstatus = 0 then 
                 $ServersUnlocked = $ServersUnlocked + 1
                 Status()
            EndIf
         EndIf
         _AD_Close()
      EndIf
      $TimeToUnlockTotal = TimerDiff($TimeToUnlockBegin)
      $TimeToUnlockTotal = $TimeToUnlockTotal / 1000
      $TimeToUnlockTotal = Number($TimeToUnlockTotal)
      $TimeToUnlockTotal = Round($TimeToUnlockTotal, 2)
      ; more logging for the console
      ConsoleWrite($TimeToUnlockTotal & " seconds to complete " & $aDC[$i][2] & @CRLF)
      timeelapsed()
      ConsoleWrite("Elapsed Time: " & $Time & @CRLF)
Next

; give a simple status report for the scripts processing. Later this will become obsolete when a GUI is implemented
MsgBox(16, "Total Unlocked servers", "The user " & $user & " has now been unlocked." & @CRLF & @CRLF & $UnlockedOn & "/" & ($Lines - 1) - $f & " total servers unocked on."& @CRLF & @CRLF & "These actions were completed in about " & $Time & @CRLF & @CRLF & "Number of servers that caused errors: " & $ServerErrors)

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×