Rich Rumble
asked on
speed up group membership
For reference: https://www.experts-exchange.com/questions/24131449/Speed-up-query.html
That script looks up all computers in a domain, starts to ping them one by one, once they reply back, it connects and dumps the users in the local admin group, and if you use the very last script with the -r it will look up the users that make up the groups that are listed in the local admins. Omitting the -r will look up just the user names and group names of the local admins, and not recursively break down the members of the group names.
I'd like to speed the process up, as I have large domains with thousands of computers in each.
That script looks up all computers in a domain, starts to ping them one by one, once they reply back, it connects and dumps the users in the local admin group, and if you use the very last script with the -r it will look up the users that make up the groups that are listed in the local admins. Omitting the -r will look up just the user names and group names of the local admins, and not recursively break down the members of the group names.
I'd like to speed the process up, as I have large domains with thousands of computers in each.
Option Explicit
Const ADS_SCOPE_SUBTREE = 2
Sub GetMembers(strADSPath)
' Recursive subroutine to return group members
On Error Resume Next : Err.Clear
Dim objGroup : Set objGroup = GetObject(strADSPath)
If Err.Number = 0 Then
Dim objMember
For Each objMember in objGroup.Members
If Not objInfLoopPrevention.Exists(objMember.ADSPath) Then
objInfLoopPrevention.Add objMember.ADSPath, ""
If objMember.Class = "Group" And booEnableRecursion = True Then
GetMembers(objMember.ADSPath)
Else
WScript.Echo Replace(objMember.ADSPath, "WinNT://" , "")
End If
End If
Next
End If
On Error Goto 0
Set objGroup = Nothing
End Sub
Dim booEnableRecursion : booEnableRecursion = False
Dim objArgs : Set objArgs = WScript.Arguments
Dim strArg
For Each strArg in objArgs
If LCase(strArg) = "-r" Then
booEnableRecursion = True
End If
Next
Set objArgs = Nothing
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
objCommand.CommandText = _
"Select Name From 'LDAP://DC=some,DC=company,DC=com' WHERE objectClass='computer'"
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
Dim objRecordSet : Set objRecordSet = objCommand.Execute
WScript.Echo "Number of Computers: " & objRecordSet.RecordCount
Do Until objRecordSet.EOF
Dim strComputer : strComputer = objRecordSet.Fields("Name").Value
WScript.Echo strComputer
Dim objShell : Set objShell = CreateObject("WScript.Shell")
Dim strCommand : strCommand = "%comspec% /c ping -n 3 -w 1000 " & strComputer & ""
Dim objExecObject : Set objExecObject = objShell.Exec(strCommand)
Do While Not objExecObject.StdOut.AtEndOfStream
Dim strText : strText = objExecObject.StdOut.ReadAll()
If Instr(strText, "Reply") > 0 Then
' =====================================================================
' Insert your code here
' =====================================================================
On Error Resume Next : Err.Clear
Dim objWMIService : Set objWMIService = GetObject _
("winmgmts:\\" & strComputer & "\root\cimv2")
Dim colItems : Set colItems = objWMIService.ExecQuery _
("Select * From Win32_OperatingSystem")
If Err.Number <> 0 Then
WScript.Echo strComputer & ": Error connecting to WMI"
Else
Dim objItem
For Each objItem in ColItems
Wscript.Echo strComputer & ": " & objItem.Caption
Next
End If
On Error Goto 0
Dim objInfLoopPrevention : Set objInfLoopPrevention = CreateObject("Scripting.Dictionary")
WScript.Echo "Local Administrators:"
GetMembers("WinNT://" & strComputer & "/Administrators,group")
Set objInfLoopPrevention = Nothing
' =====================================================================
' End
' =====================================================================
Else
Wscript.Echo strComputer & " could not be reached."
End If
Loop
objRecordSet.MoveNext
Loop
I figure at this stage Rob has more of an interest in the threading in VbScript than I. Do you have code for it already Rob? I spend way too much time playing with PowerShell / C# these days.
> which look likes I wrote it...:-)
The bits that don't belong to MS are mine, but I always found your coding style to be close to my own ;)
Chris
ASKER
Right, that's what I thought too, and in addition, if I wanted to look up the members of groups that belong to the admin's, I thought if you did the recursion once of the entire domain, wrote it to a file, then you don't have to go back to AD each time you need to look it up.
I basically outlined all those same points in the last question Chris was good enough to assist with:
https://www.experts-exchange.com/questions/24131449/Speed-up-query.html
-rich
I basically outlined all those same points in the last question Chris was good enough to assist with:
https://www.experts-exchange.com/questions/24131449/Speed-up-query.html
-rich
Depending on the functional level and operating systems on the domain controllers you don't actually have to recurse at all. MS gave use an OID we can use to return all members of a given group, including all nesting.
That doesn't help us with the WinNT provider. But I'd still split it into stages.
1. Get local group members and compile a report (means you have to wait until the end to get the results.. but..)
2. For each unique domain group, get the members.
That does mean we have to search the domain for each group we find, but at least we can limit it to only those that are really necessary.
Chris
This will need a bit of modification, we'll need an instance of ADODB.Command if you're to enable Paging and therefore return more than 1000 results.
Chris
' This bit will need the LDAP distinguished name of the group, conversion
' from the WinNT ADSPath can be done with NameTranslate.
Dim strFilter : strFilter = "(memberOf:1.2.840.113556.1.4.1941:=" & strGroupDN & ")"
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
Dim objRecordSet : Set objRecordSet = objConnection.Execute( _
"<LDAP://" & objRootDSE.Get("defaultNamingContext") & ">;" & _
strFilter & ";" & "name;subtree")
Set objRootDSE = Nothing
Do Until objRecordSet.EOF
' Echo the name of each member
WScript.Echo objRecordSet.Fields("name").Value
objRecordSet.MoveNext
Loop
Hey Chris....you're right...on closer inspection, it's not my code, although we do have very similar coding styles....I probably learnt from you! LOL!
Anyway, I haven't even looked at your revisions Chris, but perhaps you could modify my new code to be a bit faster, if required....
So, what I've got is ScriptA.vbs, which calls ScriptB, with something like:
cscript <scriptB.vbs> /r:n /c:PC1;PC2;PC3
Here is script1.vbs, with these lines you can change:
Const intMaxComputers = 10
strScriptToRun = "\\server\share\GetLocalAd minGroupMe mbers.vbs"
' boolRecurse can be "y" or "n"
boolRecurse = "n"
Also note that currently this will only run against one "batch" of computers, with the amount of computers "per batch" defined by intMaxComputers
The reason it will only run against one batch is because of this line:
objRecordSet.MoveLast
at line 59.
When you want it to run against all computers, in batches, comment that line out.
What it should then do is concurrently run quite a few other scripts of ScriptB.vbs
Oh, and the child threads will open in command prompts that stay open. To change this so they close when finished, change the two
strCommand = "cmd /k cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs
lines to
strCommand = "cmd /c cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs
I guess, in ScriptA.vbs, you could restrict the LDAP query to a sub OU so that you control how many PCs it runs against...
Anyway, see below for ScriptA.vbs....ScriptB.vbs to follow....
Regards,
Rob.
Anyway, I haven't even looked at your revisions Chris, but perhaps you could modify my new code to be a bit faster, if required....
So, what I've got is ScriptA.vbs, which calls ScriptB, with something like:
cscript <scriptB.vbs> /r:n /c:PC1;PC2;PC3
Here is script1.vbs, with these lines you can change:
Const intMaxComputers = 10
strScriptToRun = "\\server\share\GetLocalAd
' boolRecurse can be "y" or "n"
boolRecurse = "n"
Also note that currently this will only run against one "batch" of computers, with the amount of computers "per batch" defined by intMaxComputers
The reason it will only run against one batch is because of this line:
objRecordSet.MoveLast
at line 59.
When you want it to run against all computers, in batches, comment that line out.
What it should then do is concurrently run quite a few other scripts of ScriptB.vbs
Oh, and the child threads will open in command prompts that stay open. To change this so they close when finished, change the two
strCommand = "cmd /k cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs
lines to
strCommand = "cmd /c cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs
I guess, in ScriptA.vbs, you could restrict the LDAP query to a sub OU so that you control how many PCs it runs against...
Anyway, see below for ScriptA.vbs....ScriptB.vbs
Regards,
Rob.
If LCase(Right(Wscript.FullName, 11)) = "wscript.exe" Then
strPath = Wscript.ScriptFullName
strCommand = "%comspec% /k cscript """ & strPath & """"
Set objShell = CreateObject("Wscript.Shell")
objShell.Run(strCommand), 1, True
Wscript.Quit
End If
Set objShell = CreateObject("WScript.Shell")
Dim strScriptToRun
Set objFSO = CreateObject("Scripting.FileSystemObject")
Const intMaxComputers = 10
strScriptToRun = "\\server\share\GetLocalAdminGroupMembers.vbs"
' boolRecurse can be "y" or "n"
boolRecurse = "n"
strScriptToRun = objFSO.GetFile(strScriptToRun).ShortPath
Const ADS_SCOPE_SUBTREE = 2
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
Dim objRootDSE, strDNSDomain
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")
objCommand.CommandText = _
"Select Name From 'LDAP://" & strDNSDomain & "' WHERE objectClass='computer'"
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
Dim objRecordSet : Set objRecordSet = objCommand.Execute
WScript.Echo "Number of Computers: " & objRecordSet.RecordCount
intCompCount = 0
strArgs = ""
While Not objRecordSet.EOF
If intCompCount < intMaxComputers Then
If strArgs = "" Then
strArgs = objRecordSet.Fields("Name").Value
Else
strArgs = strArgs & ";" & objRecordSet.Fields("Name").Value
End If
intCompCount = intCompCount + 1
Else
strCommand = "cmd /k cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs
'WScript.Echo "Running " & strCommand
objShell.Run strCommand, 1, False
strArgs = ""
intCompCount = 0
objRecordSet.MoveLast
End If
objRecordSet.MoveNext
Wend
If intCompCount > 0 Then
strCommand = "cmd /k cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs
'WScript.Echo "Running " & strCommand
objShell.Run strCommand, 1, False
End If
WScript.Echo VbCrLf & "Finished."
And here's the second "child" script....no modifications required....
Regards,
Rob.
Regards,
Rob.
Option Explicit
Dim booEnableRecursion : booEnableRecursion = False
If WScript.Arguments.Named("r") = "" Or (WScript.Arguments.Named("r") <> "n" And WScript.Arguments.Named("r") <> "y") Then
booEnableRecursion = False
ElseIf WScript.Arguments.Named("r") = "y" Then
booEnableRecursion = True
End If
If WScript.Arguments.Named("c") = "" Then
WScript.Echo "No computers were passed to the script."
Else
Dim arrComputers, strComputer
arrComputers = Split(WScript.Arguments.Named("c"), ";")
WScript.Echo "Number of Computers: " & UBound(arrComputers) + 1
For Each strComputer In arrComputers
WScript.Echo strComputer
Dim objShell : Set objShell = CreateObject("WScript.Shell")
If Ping(strComputer) = True Then
' =====================================================================
' Insert your code here
' =====================================================================
On Error Resume Next : Err.Clear
Dim objWMIService : Set objWMIService = GetObject _
("winmgmts:\\" & strComputer & "\root\cimv2")
Dim colItems : Set colItems = objWMIService.ExecQuery _
("Select * From Win32_OperatingSystem")
If Err.Number <> 0 Then
WScript.Echo strComputer & ": Error connecting to WMI"
Else
Dim objItem
For Each objItem in colItems
Wscript.Echo strComputer & ": " & objItem.Caption
Next
End If
On Error Goto 0
Dim objInfLoopPrevention : Set objInfLoopPrevention = CreateObject("Scripting.Dictionary")
WScript.Echo "Local Administrators:"
GetMembers("WinNT://" & strComputer & "/Administrators,group")
Set objInfLoopPrevention = Nothing
' =====================================================================
' End
' =====================================================================
Else
Wscript.Echo strComputer & " could not be reached."
End If
Next
End If
Sub GetMembers(strADSPath)
' Recursive subroutine to return group members
On Error Resume Next : Err.Clear
Dim objGroup : Set objGroup = GetObject(strADSPath)
If Err.Number = 0 Then
Dim objMember
For Each objMember in objGroup.Members
If Not objInfLoopPrevention.Exists(objMember.ADSPath) Then
objInfLoopPrevention.Add objMember.ADSPath, ""
If objMember.Class = "Group" And booEnableRecursion = True Then
GetMembers(objMember.ADSPath)
Else
WScript.Echo Replace(objMember.ADSPath, "WinNT://" , "")
End If
End If
Next
End If
On Error Goto 0
Set objGroup = Nothing
End Sub
Function Ping(strComputer)
Dim objShell, boolCode
Set objShell = CreateObject("WScript.Shell")
boolCode = objShell.Run("Ping -n 3 -w 1000 " & strComputer, 0, True)
If boolCode = 0 Then
Ping = True
Else
Ping = False
End If
End Function
I think we need a few more things... I'll have a think about them and post code...
1. A common source to write the responses back to.
At the moment we're going to be spawning windows all over the place and dropping output into each spawned window. Solutions to this include an SQL database, or a collection of flat-files that can be re-assembled by the master script after execution completes.
2. A thread limiter.
If each batch is 10 computers, a search for 1000 computers will spawn 100 VbScript threads at (almost) the same time.
I was thinking of a pool approach where 10 or so threads are available for processing. Potentially achievable if either we monitor Win32_Process or wait for a file to appear signalling the end of the script.
That does slow it down, but my last large domain consisted of 4500 computers in the domain (9000 in the forest). That would give us quite a scary number of threads trying to execute.
Chris
Almost time to play foosball ;) Figure I'll post what I have so far so you can have a look through then I'll come back to it later on.
This implementation of threading requires the child script to return a numbered file (e.g. 1.results) to signal the end of the child script. At the moment my child script consists of this, purely to test thread management (because the threads don't terminate sequentially):
Set objArgs = WScript.Arguments
strThreadNumber = objArgs(1)
Randomize()
i = Int(48 * Rnd())
WScript.Sleep (i * 1000)
Set objFileSystem = CreateObject("Scripting.Fi
Set objFile = objFileSystem.OpenTextFile
The main script passes a semi-colon delimited list of computers and a numeric thread identifier when starting the thread:
objShell.Run "ChildScript.vbs " & arrBatches(i) & " " & i, 0, False
I split up all the processing and batch generation, didn't like holding onto the connection to AD for so long (and it would be a very long time).
Thoughts are always welcome.
The "To Do" list includes:
* Compilation of return which will be held in the .results file (when I decide on a format for that)
* Returning membership for all unique domain groups found in the result set
Chris
Option Explicit
' Script Constants
' Maximum number of computers to add into each thread
Const MAX_PER_THREAD = 10
' Maximum number of threads to execute
Const MAX_THREADS = 10
' Functions
Function GetComputersFromAD(strPort, strBaseDN)
' Returns an array containing all computers in AD. Performed here so we do not have to maintain
' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
Const ADS_SCOPE_SUBTREE = 2
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
objCommand.CommandText = "SELECT name FROM '" & strPort & "://" & strBaseDN & _
"' WHERE objectClass='computer'"
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
Dim objRecordSet : Set objRecordSet = objCommand.Execute
Dim arrComputers()
Dim i : i = 0
Do Until objRecordSet.EOF
ReDim Preserve arrComputers(i)
arrComputers(i) = objRecordSet.Fields("name").Value
i = i + 1
objRecordSet.MoveNext
Loop
Set objRecordSet = Nothing
Set objCommand = Nothing
Set objConnection = Nothing
GetComputersFromAD = arrComputers
End Function
Function BatchGenerator(arrComputers)
Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
Dim intPerThread : intPerThread = MAX_PER_THREAD
' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
If (intTotalComputers / MAX_THREADS) < MAX_PER_THREAD Then
intPerThread = Round(intTotalComputers / MAX_THREADS)
End If
Dim intCurrentBatchCount : intCurrentBatchCount = 0
Dim intBatchNo : intBatchNo = 0
Dim arrBatches() : ReDim arrBatches(intBatchNo)
Dim strComputer
For Each strComputer in arrComputers
If intCurrentBatchCount = intPerThread Then
intCurrentBatchCount = 0
intBatchNo = intBatchNo + 1
ReDim Preserve arrBatches(intBatchNo)
End If
intCurrentBatchCount = intCurrentBatchCount + 1
arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
Next
BatchGenerator = arrBatches
End Function
Sub ThreadManager(arrBatches)
Dim objShell : Set objShell = CreateObject("WScript.Shell")
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objFolder, objFile
Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
For Each objFile in objFolder.Files
If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
objFile.Delete
End If
Next
Dim objThreadMonitor : Set objThreadMonitor = CreateObject("Scripting.Dictionary")
Dim i : i = 0
Do Until i = UBound(arrBatches)
' Start Threads if Threads can be started
If objThreadMonitor.Count < MAX_THREADS Then
' WScript.Echo "Starting " & i
objThreadMonitor.Add i, ""
objShell.Run "ChildScript.vbs " & arrBatches(i) & " " & i, 0, False
i = i + 1
Else
' WScript.Echo "Sleeping for 10 seconds"
WScript.Sleep 10000
End If
' Look for Finished Threads
Dim j
For Each j in objThreadMonitor
If objFileSystem.FileExists(j & ".results") Then
' WScript.Echo "Stopping " & j
objThreadMonitor.Remove j
End If
Next
Loop
' Wait for completion of threads
Do Until objThreadMonitor.Count = 0
' WScript.Echo "Batches complete. Waiting for Thread Shutdown"
WScript.Sleep 10000
For Each j in objThreadMonitor
If objFileSystem.FileExists(j & ".results") Then
' WScript.Echo "Stopping " & j
objThreadMonitor.Remove j
End If
Next
Loop
End Sub
'
' Main Code
'
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
' Alternatives include GC and rootDomainNamingContext
Dim arrComputers : arrComputers = GetComputersFromAD( _
"LDAP", _
objRootDSE.Get("defaultNamingContext"))
Set objRootDSE = Nothing
Dim arrBatches : arrBatches = BatchGenerator(arrComputers)
ThreadManager arrBatches
This is my version of the child script. It gives us text files that look something like this:
<ComputerName>|<OperatingS
Group is formatted like this:
GroupName#GroupADSPath
The latter for the "just in case" / "while I'm here" type reasoning. Members is formatted like this:
Member;Member;Member
Then Member itself into this:
Name#ADSPath#Class
Confused yet?
I removed recursion, instead it enumerates all local groups (harkens back to my original script). Doing this means we don't have to worry about identifying whether something is a Domain Group or not (at this stage).
I have a modification to the main script (above) that parses the file returned by this script. Using that we can filter through all the members that are Groups, and figure out if we need to grab membership for the domain (after we create a unique list of Domain groups from the local groups).
Chris
Option Explicit
Function GetMembers(strADSPath)
' Returns members as composite string
Dim objGroup : Set objGroup = GetObject(strADSPath)
Dim objMember
Dim strMembers : strMembers = ""
For Each objMember in objGroup.Members
Dim strMember : strMember = objMember.Name & "#" & objMember.ADSPath & "#" & objMember.Class
strMembers = strMembers & strMember & ";"
Next
Set objGroup = Nothing
GetMembers = strMembers
End Function
Function Ping(strComputer)
Dim objShell : Set objShell = CreateObject("WScript.Shell")
Dim booCode : booCode = objShell.Run("Ping -n 3 -w 1000 " & strComputer, 0, True)
If booCode = 0 Then
Ping = True
Else
Ping = False
End If
End Function
'
' Main Code
'
Dim objArgs : Set objArgs = WScript.Arguments
Dim strComputers : strComputers = objArgs(0)
Dim strThreadNumber : strThreadNumber = objArgs(1)
Dim arrComputers : arrComputers = Split(strComputers, ";")
Dim strOutput
Dim strComputer
For Each strComputer In arrComputers
If Ping(strComputer) = True Then
On Error Resume Next : Err.Clear
Dim objWMIService : Set objWMIService = GetObject("winmgmts://" & strComputer & "/root/cimv2")
Dim strOS : strOS = ""
If Err.Number = 0 Then
Dim colItems : Set colItems = objWMIService.ExecQuery _
("SELECT Caption FROM Win32_OperatingSystem")
Dim objItem
For Each objItem in colItems
strOS = objItem.Caption
Next
End If
On Error Goto 0
Dim objLocalSAM : Set objLocalSAM = GetObject("WinNT://" & strComputer)
objLocalSAM.Filter = Array("Group")
Dim objGroup
For Each objGroup in objLocalSAM
Dim strMembers : strMembers = GetMembers(objGroup.ADSPath)
strOutput = strOutput & strComputer & "|" & strOS & "|" & _
objGroup.Name & "#" & objGroup.ADSPath & "|" & strMembers & vbCrLf
Next
End If
Next
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objFile : Set objFile = objFileSystem.OpenTextFile(strThreadNumber & ".results", 2, True, 0)
objFile.Write strOutput
Set objFile = Nothing
Set objFileSystem = Nothing
Well... it's a bit of a monstrous script now, but I'm done.
It does this...
1. Gets a list of computers from AD (excluding Domain Controllers)
2. Splits those computers into batches for multi-threaded execution
3. Gets all local groups for all computers returned (if it can)
4. Pulls together all the results from the separate files
5. Extracts a list of Domain Groups from the results
6. Gets the full membership of the group in the domain
7. Writes the results to the command line, replacing Domain Groups with their members
I've tested most of it, but I haven't allowed it to do a full run on my network (I lock down access to WMI, and my PC does not have access to that, therefore every connection is slow as hell).
Naturally you should feel free to increase the thread pool size if you wish (MAX_THREADS).
Going to make myself a nice relaxing cup of tea now.
Chris
Option Explicit
' Script Constants
' Maximum number of computers to add into each thread
Const MAX_PER_THREAD = 10
' Maximum number of threads to execute
Const MAX_THREADS = 10
' Functions
Function GetComputersFromAD(strPort, strBaseDN)
' Returns an array containing all computers in AD. Performed here so we do not have to maintain
' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 1000
Dim strFilter : strFilter = "(&(objectClass=computer)(!frscomputerreferencebl=*))"
objCommand.CommandText = "<" & strPort & "://" & strBaseDN & ">;" & strFilter & ";" & _
"name;subtree"
Dim objRecordSet : Set objRecordSet = objCommand.Execute
Dim arrComputers()
Dim i : i = 0
Do Until objRecordSet.EOF
ReDim Preserve arrComputers(i)
arrComputers(i) = objRecordSet.Fields("name").Value
i = i + 1
objRecordSet.MoveNext
Loop
Set objRecordSet = Nothing
Set objCommand = Nothing
Set objConnection = Nothing
GetComputersFromAD = arrComputers
End Function
Function BatchGenerator(arrComputers)
Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
Dim intPerThread : intPerThread = MAX_PER_THREAD
' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
If (intTotalComputers / MAX_THREADS) < MAX_PER_THREAD Then
intPerThread = Round(intTotalComputers / MAX_THREADS)
End If
Dim intCurrentBatchCount : intCurrentBatchCount = 0
Dim intBatchNo : intBatchNo = 0
Dim arrBatches() : ReDim arrBatches(intBatchNo)
Dim strComputer
For Each strComputer in arrComputers
If intCurrentBatchCount = intPerThread Then
intCurrentBatchCount = 0
intBatchNo = intBatchNo + 1
ReDim Preserve arrBatches(intBatchNo)
End If
intCurrentBatchCount = intCurrentBatchCount + 1
arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
Next
BatchGenerator = arrBatches
End Function
Sub ThreadManager(arrBatches)
Dim objShell : Set objShell = CreateObject("WScript.Shell")
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objFolder, objFile
Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
For Each objFile in objFolder.Files
If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
objFile.Delete
End If
Next
Dim objThreadMonitor : Set objThreadMonitor = CreateObject("Scripting.Dictionary")
Dim i : i = 0
Do Until i = UBound(arrBatches)
' Start Threads if Threads can be started
If objThreadMonitor.Count < MAX_THREADS Then
' WScript.Echo "Starting " & i
objThreadMonitor.Add i, ""
objShell.Run "cscript.exe ChildScript.vbs " & arrBatches(i) & " " & i, 0, False
i = i + 1
Else
' WScript.Echo "Sleeping for 10 seconds"
WScript.Sleep 10000
End If
' Look for Finished Threads
Dim j
For Each j in objThreadMonitor
If objFileSystem.FileExists(j & ".results") Then
' WScript.Echo "Stopping " & j
objThreadMonitor.Remove j
End If
Next
Loop
' Wait for completion of remaining threads
Do Until objThreadMonitor.Count = 0
' WScript.Echo "Batches complete. Waiting for Thread Shutdown"
WScript.Sleep 10000
For Each j in objThreadMonitor
If objFileSystem.FileExists(j & ".results") Then
' WScript.Echo "Stopping " & j
objThreadMonitor.Remove j
End If
Next
Loop
Set objFileSystem = Nothing
Set objThreadMonitor = Nothing
Set objShell = Nothing
End Sub
Function CollateResultFiles
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objFolder : Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
Dim objLocalGroups : Set objLocalGroups = CreateObject("Scripting.Dictionary")
objLocalGroups.CompareMode = VbTextCompare
Dim objFile
For Each objFile in objFolder.Files
If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
Dim objStream : Set objStream = objFile.OpenAsTextStream(1, 0)
Do Until objStream.AtEndOfStream
Dim arrLine : arrLine = Split(objStream.ReadLine, "|")
Dim strComputer : strComputer = arrLine(0)
Dim strOS : strOS = arrLine(1)
Dim strGroup : strGroup = arrLine(2)
Dim strMembers : strMembers = arrLine(3)
objLocalGroups.Add strComputer & "/" & strGroup, Array(strOS, strMembers)
Loop
Set objStream = Nothing
End If
Next
CollateResultFiles = objLocalGroups
End Function
Function GetDomainGroups(objLocalGroups)
' Create a list of domain group names from the data returned
Dim objDomainGroups : Set objDomainGroups = CreateObject("Scripting.Dictionary")
Dim strLocalGroups
For Each strLocalGroup in objLocalGroups
Dim strMembers : strMembers = objLocalGroups(strLocalGroup)(1)
If strMembers <> "" Then
Dim arrMembers : arrMembers = Split(strMembers, ";")
Dim i
For i = 0 To (UBound(arrMembers) - 1)
Dim arrMember : arrMember = Split(arrMembers(i), "#")
If UBound(Split(arrMember(1), "/")) = 3 _
And InStr(arrMember(1), "NT AUTHORITY") = 0 _
And arrMember(2) = "Group" Then
Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
If Not objDomainGroups.Exists(strDomainGroup) Then
objDomainGroups.Add strDomainGroup, ""
End If
End If
Next
End If
Next
Set GetDomainGroups = objDomainGroups
End Function
Function ExpandDomainGroups(objDomainGroups, strBaseDN)
' This method requires Windows 2003 SP1 minimum
Dim strDomainGroup
For Each strDomainGroup in objDomainGroups
Dim objGroupDN : strGroupDN = GetObjectDN(strDomainGroup)
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 1000
Dim strFilter : strFilter = _
"(&(objectClass=user)(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=" & strGroupDN & "))"
objCommand.CommandText = "<LDAP://" & strBaseDN & ">;" & strFilter & ";" & _
"aDSPath,sAMAccountName;subtree"
Dim objRecordSet : Set objRecordSet = objCommand.Execute
Dim strMembers : strMembers = ""
Do Until objRecordSet.EOF
strMembers = strMembers & objRecordSet.Fields("sAMAccountName").Value & _
"#" & objRecordSet.Fields("aDSPAth") & "#User;"
objDomainGroups(strDomainGroup) = strMembers
objRecordSet.MoveNext
Loop
Set objRecordSet = Nothing
Set objCommand = Nothing
Set objConnection = Nothing
Next
Set ExpandDomainGroups = objDomainGroups
End Function
Function GetObjectDN(strObject)
' Return Type: String
'
' Returns a Distinguished Name for an Object from it's NT SAM ID.
' This will only function for valid object types within an NT Domain structure.
Const ADS_NAME_INITTYPE_GC = 3
Const ADS_NAME_TYPE_1779 = 1
Const ADS_NAME_TYPE_NT4 = 3
On Error Resume Next : Err.Clear
Dim objNameTranslate : Set objNameTranslate = CreateObject("NameTranslate")
objNameTranslate.Init ADS_NAME_INITTYPE_GC, ""
objNameTranslate.Set ADS_NAME_TYPE_NT4, strObject
Dim strObjectDN : strObjectDN = objNameTranslate.Get(ADS_NAME_TYPE_1779)
If Err.Number <> 0 Then ' Make the DN Blank for a Failed Search
strObjectDN = ""
End If
Set objNameTranslate = Nothing
On Error Goto 0
GetObjectDN = strObjectDN
End Function
Sub WriteResults(objLocalGroups, objDomainGroups)
Dim strLocalGroups
For Each strLocalGroup in objLocalGroups
WScript.Echo strLocalGroup & " - " & objLocalGroups(strLocalGroup)(0)
DisplayMembers strMembers, objDomainGroups
WScript.Echo
Next
End Sub
Sub DisplayMembers(strMembers, objDomainGroups)
If strMembers <> "" Then
Dim arrMembers : arrMembers = Split(strMembers, ";")
Dim i
For i = 0 To (UBound(arrMembers) - 1)
Dim strMember : strMember = arrMembers(i)
Dim arrMember : arrMember = Split(strMember, "#")
If UBound(Split(arrMember(1), "/")) = 3 _
And InStr(arrMember(1), "NT AUTHORITY") = 0 _
And arrMember(2) = "Group" Then
Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
If objDomainGroups.Exists(strDomainGroup) Then
strMembers = Replace(strMembers, strMember & ";", objDomainGroups(strDomainGroup))
End If
End If
Next
arrMembers = Split(strMembers, ";")
For i = 0 To (UBound(arrMembers) - 1)
arrMember = Split(arrMembers(i), "#")
WScript.Echo arrMember(0)
Next
End If
End Sub
'
' Main Code
'
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
Dim strDomainDN : strDomainDN = objRootDSE.Get("defaultNamingContext")
Set objRootDSE = Nothing
' Alternatives include GC and rootDomainNamingContext
Dim arrComputers : arrComputers = GetComputersFromAD("LDAP", strDomainDN)
Dim arrBatches : arrBatches = BatchGenerator(arrComputers)
ThreadManager arrBatches
Dim objLocalGroups : Set objLocalGroups = CollateResultFiles
Dim objDomainGroups : Set objDomainGroups = GetDomainGroups(objLocalGroups)
Set objDomainGroups = ExpandDomainGroups(objDomainGroups, strDomainDN)
WriteResults objLocalGroups, objDomainGroups
ASKER
I'm giving it a try now, I'll see if I can shoe-horn the ping script in... You've really outdone yourself... that's a lot of code!
-rich
-rich
Ping is in the child script already, it's designed to run with the one I posted above :)
Chris
ASKER
Ahh, I missed that. So ID: 23632695 and ID: 23633665 need to be in the same folder(location) on the hd... do they need to named a certain way or called a certain way
cscript /nologo scriptA.vbs ?? and scriptA will call scriptB.vbs on it's own?
I see ChildScript.vbs in the code.
-rich
cscript /nologo scriptA.vbs ?? and scriptA will call scriptB.vbs on it's own?
I see ChildScript.vbs in the code.
-rich
Yep, they will. The two must be in the same folder unless we go in and configure both scripts to use specific folders for the reports.
For the main script it doesn't matter what it's called.
The child only matters if it's named than in the main script (so either stick with ChildScript.vbs or modify that in the main (parent) script).
Chris
missing words... good sign, right? ;)
> named than
... named differently than ...
Chris
ASKER
The results files are building up, and so far they look good, can't wait until the loop finishes, I've got 10 spawned cscript's. I think these techniques could be utilized for a lot more big searches like this. It'd be neat to have a basic template, like the M$ ping script I was using, and a place inside one of the scripts to put ones own code. I realize with this one, there is recursion and other specialized portions, but I wonder if a generic version could be derived.
Nevermind... if I start thinking about scope creep, this question would never get done ;)
I think there are a few too many variables to consider, but if the goal is to do XYZ to all domain computers, then maybe a generic version of this could be done? Don't to it here unless you feel compelled, I''ll make yet another question if anyone was willing to participate. I can already tell this is going much faster. I have to audit share permissions pretty soon so I'd like to apply the same principals to that too :)
-rich
Nevermind... if I start thinking about scope creep, this question would never get done ;)
I think there are a few too many variables to consider, but if the goal is to do XYZ to all domain computers, then maybe a generic version of this could be done? Don't to it here unless you feel compelled, I''ll make yet another question if anyone was willing to participate. I can already tell this is going much faster. I have to audit share permissions pretty soon so I'd like to apply the same principals to that too :)
-rich
> can't wait until the loop finishes
I suspect you'll find the output format leaves a lot to be desired. I'm far from keen on it, too much data to display. Fortunately... we can always re-parse the .result files without re-running the script. In fact it's important not to re-run the script if you want to do that, because the first thing ThreadManager does is purge the existing result files.
> I think these techniques could be utilized for a lot more big searches like this
WMI perhaps. Anything that hooks into a large client base and must wait for a return in each instance.
If whatever we're doing is responding quickly there's no benefit. Ultimately it'll all bottleneck on the CPU on the local machine.
> and a place inside one of the scripts to put ones own code
Rip out the final functions that deal with collating the text files and formatting output and you can. Simply replace the Child Script with one that does what you need for the passed group of computers.
Still, I'd say this task, retrieving membership like this, is complex. As such it's very difficult to make generic scripts, at least in any way above the basic functions (many of which I use time and time again).
For the share permissions, it won't have anything like this scope will it? Or where you looking to grab those for every host on the domain as well?
Chris
ASKER
Right, every host on the domain for the shares. I have scanning tools that can do it, but I like "rolling" my own as it were. Not that I have done any work whatsoever. I'm about 1/2 way through, 150 results files and if my math holds up, 8-10 hosts in each file, I'm almost exactly 1/2 way.
I'm also fascinated with the whole process, the way programmers are, it's great, I'm glad guys like you are around. This share idea goes back to another one you knocked out:
https://www.experts-exchange.com/questions/21986624/Combine-these-two-WSH-scripts.html
As far as output, your right, I like that one could use those results files as they see fit, being able to parse them with whatever they choose. I could of sworn there was an easy way to output csv, xml,html with vbscript, without adding "," or other tags around each variable manually. The ScriptoMatic does it, but I've looked at that hta, and it's beyone me how they do it, and how they convert to other languages too.
-rich
I'm also fascinated with the whole process, the way programmers are, it's great, I'm glad guys like you are around. This share idea goes back to another one you knocked out:
https://www.experts-exchange.com/questions/21986624/Combine-these-two-WSH-scripts.html
As far as output, your right, I like that one could use those results files as they see fit, being able to parse them with whatever they choose. I could of sworn there was an easy way to output csv, xml,html with vbscript, without adding "," or other tags around each variable manually. The ScriptoMatic does it, but I've looked at that hta, and it's beyone me how they do it, and how they convert to other languages too.
-rich
Ahh that one. We can use most of the code we did above for that one. The changes will be in the child script, then it's again just a case of deciding how to present the results.
I've never found anything that'll automatically format for VbScript. Entirely possible I didn't look hard enough though, I tend to get dug into the bits I like (directory services mostly) and completely ignore anything outside of that unless I really need it.
I figure that's the advantage of being a sys admin / engineer rather than a programmer, I don't have to learn it properly, only the bits I like :)
Chris
ASKER
I think I found it...
http://msdn.microsoft.com/en-us/library/aa393065(VS.85).aspx#encode_an_object_using_vbscript
And if you look in scriptomaticV2.hta around line 959 is the method they use for xml. Again, fascinating :) If the output were XML it'd be easy to create a simple XSL sheet to style the ouput.
I'm sure powershell has more accessible output than vbs/wmi does. WMIC has it built in, and I could probably just reuse their xsl sheets for the most part.
-rich
http://msdn.microsoft.com/en-us/library/aa393065(VS.85).aspx#encode_an_object_using_vbscript
And if you look in scriptomaticV2.hta around line 959 is the method they use for xml. Again, fascinating :) If the output were XML it'd be easy to create a simple XSL sheet to style the ouput.
I'm sure powershell has more accessible output than vbs/wmi does. WMIC has it built in, and I could probably just reuse their xsl sheets for the most part.
-rich
Not sure we can use that here, but if XML is good we can parse the files and output XML instead?
Chris
ASKER
Yeah, sure... I'm just used to doing xml or csv so *hacky*, just echo'ing the tag's before and after or echoing the comma after the variable.. Like I did with XML here:
https://www.experts-exchange.com/questions/23545465/Vbscript-needs-to-accept-alternate-username-and-password.html?anchorAnswerId=21951146#a21951146
I like the way scriptomatic does it, so it may be good to emulate them and reuse the xsl files (C:\WINDOWS\system32\wbem) in the wbem dir.
We'll keep that for another day ;)
-rich
https://www.experts-exchange.com/questions/23545465/Vbscript-needs-to-accept-alternate-username-and-password.html?anchorAnswerId=21951146#a21951146
I like the way scriptomatic does it, so it may be good to emulate them and reuse the xsl files (C:\WINDOWS\system32\wbem)
We'll keep that for another day ;)
-rich
ASKER
doh
C:\admin.vbs(156, 3) Microsoft VBScript runtime error: Wrong number of arguments or invalid property assignment
...
CollateResultFiles = objLocalGroups
End Function
I swear i didn't do anything!
-rich
C:\admin.vbs(156, 3) Microsoft VBScript runtime error: Wrong number of arguments or invalid property assignment
...
CollateResultFiles = objLocalGroups
End Function
I swear i didn't do anything!
-rich
Quick check in before bed time and D'oh!
Missing a Set there (Set CollateResultFiles = objLocalGroups).
I've fixed that here and commented out enough so that it'll just continue from where it is (pulling it together and displaying the output). Should work with the first three functions commented out as below. Just make a copy of the report files in case ;)
Chris
Option Explicit
' Script Constants
' Maximum number of computers to add into each thread
Const MAX_PER_THREAD = 10
' Maximum number of threads to execute
Const MAX_THREADS = 10
' Functions
Function GetComputersFromAD(strPort, strBaseDN)
' Returns an array containing all computers in AD. Performed here so we do not have to maintain
' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 1000
Dim strFilter : strFilter = "(&(objectClass=computer)(!frscomputerreferencebl=*))"
objCommand.CommandText = "<" & strPort & "://" & strBaseDN & ">;" & strFilter & ";" & _
"name;subtree"
Dim objRecordSet : Set objRecordSet = objCommand.Execute
Dim arrComputers()
Dim i : i = 0
Do Until objRecordSet.EOF
ReDim Preserve arrComputers(i)
arrComputers(i) = objRecordSet.Fields("name").Value
i = i + 1
objRecordSet.MoveNext
Loop
Set objRecordSet = Nothing
Set objCommand = Nothing
Set objConnection = Nothing
GetComputersFromAD = arrComputers
End Function
Function BatchGenerator(arrComputers)
Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
Dim intPerThread : intPerThread = MAX_PER_THREAD
' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
If (intTotalComputers / MAX_THREADS) < MAX_PER_THREAD Then
intPerThread = Round(intTotalComputers / MAX_THREADS)
End If
Dim intCurrentBatchCount : intCurrentBatchCount = 0
Dim intBatchNo : intBatchNo = 0
Dim arrBatches() : ReDim arrBatches(intBatchNo)
Dim strComputer
For Each strComputer in arrComputers
If intCurrentBatchCount = intPerThread Then
intCurrentBatchCount = 0
intBatchNo = intBatchNo + 1
ReDim Preserve arrBatches(intBatchNo)
End If
intCurrentBatchCount = intCurrentBatchCount + 1
arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
Next
BatchGenerator = arrBatches
End Function
Sub ThreadManager(arrBatches)
Dim objShell : Set objShell = CreateObject("WScript.Shell")
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objFolder, objFile
Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
For Each objFile in objFolder.Files
If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
objFile.Delete
End If
Next
Dim objThreadMonitor : Set objThreadMonitor = CreateObject("Scripting.Dictionary")
Dim i : i = 0
Do Until i = UBound(arrBatches)
' Start Threads if Threads can be started
If objThreadMonitor.Count < MAX_THREADS Then
' WScript.Echo "Starting " & i
objThreadMonitor.Add i, ""
objShell.Run "cscript.exe ChildScript.vbs " & arrBatches(i) & " " & i, 0, False
i = i + 1
Else
' WScript.Echo "Sleeping for 10 seconds"
WScript.Sleep 10000
End If
' Look for Finished Threads
Dim j
For Each j in objThreadMonitor
If objFileSystem.FileExists(j & ".results") Then
' WScript.Echo "Stopping " & j
objThreadMonitor.Remove j
End If
Next
Loop
' Wait for completion of remaining threads
Do Until objThreadMonitor.Count = 0
' WScript.Echo "Batches complete. Waiting for Thread Shutdown"
WScript.Sleep 10000
For Each j in objThreadMonitor
If objFileSystem.FileExists(j & ".results") Then
' WScript.Echo "Stopping " & j
objThreadMonitor.Remove j
End If
Next
Loop
Set objFileSystem = Nothing
Set objThreadMonitor = Nothing
Set objShell = Nothing
End Sub
Function CollateResultFiles
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objFolder : Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
Dim objLocalGroups : Set objLocalGroups = CreateObject("Scripting.Dictionary")
objLocalGroups.CompareMode = VbTextCompare
Dim objFile
For Each objFile in objFolder.Files
If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
Dim objStream : Set objStream = objFile.OpenAsTextStream(1, 0)
Do Until objStream.AtEndOfStream
Dim arrLine : arrLine = Split(objStream.ReadLine, "|")
Dim strComputer : strComputer = arrLine(0)
Dim strOS : strOS = arrLine(1)
Dim strGroup : strGroup = arrLine(2)
Dim strMembers : strMembers = arrLine(3)
objLocalGroups.Add strComputer & "/" & strGroup, Array(strOS, strMembers)
Loop
Set objStream = Nothing
End If
Next
Set CollateResultFiles = objLocalGroups
End Function
Function GetDomainGroups(objLocalGroups)
' Create a list of domain group names from the data returned
Dim objDomainGroups : Set objDomainGroups = CreateObject("Scripting.Dictionary")
Dim strLocalGroups
For Each strLocalGroup in objLocalGroups
Dim strMembers : strMembers = objLocalGroups(strLocalGroup)(1)
If strMembers <> "" Then
Dim arrMembers : arrMembers = Split(strMembers, ";")
Dim i
For i = 0 To (UBound(arrMembers) - 1)
Dim arrMember : arrMember = Split(arrMembers(i), "#")
If UBound(Split(arrMember(1), "/")) = 3 _
And InStr(arrMember(1), "NT AUTHORITY") = 0 _
And arrMember(2) = "Group" Then
Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
If Not objDomainGroups.Exists(strDomainGroup) Then
objDomainGroups.Add strDomainGroup, ""
End If
End If
Next
End If
Next
Set GetDomainGroups = objDomainGroups
End Function
Function ExpandDomainGroups(objDomainGroups, strBaseDN)
' This method requires Windows 2003 SP1 minimum
Dim strDomainGroup
For Each strDomainGroup in objDomainGroups
Dim objGroupDN : strGroupDN = GetObjectDN(strDomainGroup)
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 1000
Dim strFilter : strFilter = _
"(&(objectClass=user)(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=" & strGroupDN & "))"
objCommand.CommandText = "<LDAP://" & strBaseDN & ">;" & strFilter & ";" & _
"aDSPath,sAMAccountName;subtree"
Dim objRecordSet : Set objRecordSet = objCommand.Execute
Dim strMembers : strMembers = ""
Do Until objRecordSet.EOF
strMembers = strMembers & objRecordSet.Fields("sAMAccountName").Value & _
"#" & objRecordSet.Fields("aDSPAth") & "#User;"
objDomainGroups(strDomainGroup) = strMembers
objRecordSet.MoveNext
Loop
Set objRecordSet = Nothing
Set objCommand = Nothing
Set objConnection = Nothing
Next
Set ExpandDomainGroups = objDomainGroups
End Function
Function GetObjectDN(strObject)
' Return Type: String
'
' Returns a Distinguished Name for an Object from it's NT SAM ID.
' This will only function for valid object types within an NT Domain structure.
Const ADS_NAME_INITTYPE_GC = 3
Const ADS_NAME_TYPE_1779 = 1
Const ADS_NAME_TYPE_NT4 = 3
On Error Resume Next : Err.Clear
Dim objNameTranslate : Set objNameTranslate = CreateObject("NameTranslate")
objNameTranslate.Init ADS_NAME_INITTYPE_GC, ""
objNameTranslate.Set ADS_NAME_TYPE_NT4, strObject
Dim strObjectDN : strObjectDN = objNameTranslate.Get(ADS_NAME_TYPE_1779)
If Err.Number <> 0 Then ' Make the DN Blank for a Failed Search
strObjectDN = ""
End If
Set objNameTranslate = Nothing
On Error Goto 0
GetObjectDN = strObjectDN
End Function
Sub WriteResults(objLocalGroups, objDomainGroups)
Dim strLocalGroups
For Each strLocalGroup in objLocalGroups
WScript.Echo strLocalGroup & " - " & objLocalGroups(strLocalGroup)(0)
DisplayMembers strMembers, objDomainGroups
WScript.Echo
Next
End Sub
Sub DisplayMembers(strMembers, objDomainGroups)
If strMembers <> "" Then
Dim arrMembers : arrMembers = Split(strMembers, ";")
Dim i
For i = 0 To (UBound(arrMembers) - 1)
Dim strMember : strMember = arrMembers(i)
Dim arrMember : arrMember = Split(strMember, "#")
If UBound(Split(arrMember(1), "/")) = 3 _
And InStr(arrMember(1), "NT AUTHORITY") = 0 _
And arrMember(2) = "Group" Then
Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
If objDomainGroups.Exists(strDomainGroup) Then
strMembers = Replace(strMembers, strMember & ";", objDomainGroups(strDomainGroup))
End If
End If
Next
arrMembers = Split(strMembers, ";")
For i = 0 To (UBound(arrMembers) - 1)
arrMember = Split(arrMembers(i), "#")
WScript.Echo arrMember(0)
Next
End If
End Sub
'
' Main Code
'
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
Dim strDomainDN : strDomainDN = objRootDSE.Get("defaultNamingContext")
Set objRootDSE = Nothing
' Alternatives include GC and rootDomainNamingContext
'Dim arrComputers : arrComputers = GetComputersFromAD("LDAP", strDomainDN)
'Dim arrBatches : arrBatches = BatchGenerator(arrComputers)
'ThreadManager arrBatches
Dim objLocalGroups : Set objLocalGroups = CollateResultFiles
Dim objDomainGroups : Set objDomainGroups = GetDomainGroups(objLocalGroups)
Set objDomainGroups = ExpandDomainGroups(objDomainGroups, strDomainDN)
WriteResults objLocalGroups, objDomainGroups
ASKER
ziped them up, get some sleep, work on this error tomorrow :)
C:\admin.vbs(165, 3) Microsoft VBScript runtime error: Variable is
undefined: 'strLocalGroup'
-rich
C:\admin.vbs(165, 3) Microsoft VBScript runtime error: Variable is
undefined: 'strLocalGroup'
-rich
I must have forgotten to copy these parts back from my test script. Sorry about this.
Line 165 is currently "Dim strLocalGroups". If just needs the "s" removing from the end. Probably caused by keyboard mashing Alt, F, S to save the file ;)
Chris
ASKER
C:\admin.vbs(195, 22) Microsoft VBScript runtime error: Variable i
s undefined: 'strGroupDN'
Just in case I messed it up I've attached the code I'm using.,.
-rich
s undefined: 'strGroupDN'
Just in case I messed it up I've attached the code I'm using.,.
-rich
Option Explicit
' Script Constants
' Maximum number of computers to add into each thread
Const MAX_PER_THREAD = 10
' Maximum number of threads to execute
Const MAX_THREADS = 10
' Functions
Function GetComputersFromAD(strPort, strBaseDN)
' Returns an array containing all computers in AD. Performed here so we do not have to maintain
' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 1000
Dim strFilter : strFilter = "(&(objectClass=computer)(!frscomputerreferencebl=*))"
objCommand.CommandText = "<" & strPort & "://" & strBaseDN & ">;" & strFilter & ";" & _
"name;subtree"
Dim objRecordSet : Set objRecordSet = objCommand.Execute
Dim arrComputers()
Dim i : i = 0
Do Until objRecordSet.EOF
ReDim Preserve arrComputers(i)
arrComputers(i) = objRecordSet.Fields("name").Value
i = i + 1
objRecordSet.MoveNext
Loop
Set objRecordSet = Nothing
Set objCommand = Nothing
Set objConnection = Nothing
GetComputersFromAD = arrComputers
End Function
Function BatchGenerator(arrComputers)
Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
Dim intPerThread : intPerThread = MAX_PER_THREAD
' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
If (intTotalComputers / MAX_THREADS) < MAX_PER_THREAD Then
intPerThread = Round(intTotalComputers / MAX_THREADS)
End If
Dim intCurrentBatchCount : intCurrentBatchCount = 0
Dim intBatchNo : intBatchNo = 0
Dim arrBatches() : ReDim arrBatches(intBatchNo)
Dim strComputer
For Each strComputer in arrComputers
If intCurrentBatchCount = intPerThread Then
intCurrentBatchCount = 0
intBatchNo = intBatchNo + 1
ReDim Preserve arrBatches(intBatchNo)
End If
intCurrentBatchCount = intCurrentBatchCount + 1
arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
Next
BatchGenerator = arrBatches
End Function
Sub ThreadManager(arrBatches)
Dim objShell : Set objShell = CreateObject("WScript.Shell")
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objFolder, objFile
Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
For Each objFile in objFolder.Files
If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
objFile.Delete
End If
Next
Dim objThreadMonitor : Set objThreadMonitor = CreateObject("Scripting.Dictionary")
Dim i : i = 0
Do Until i = UBound(arrBatches)
' Start Threads if Threads can be started
If objThreadMonitor.Count < MAX_THREADS Then
' WScript.Echo "Starting " & i
objThreadMonitor.Add i, ""
objShell.Run "cscript.exe ChildScript.vbs " & arrBatches(i) & " " & i, 0, False
i = i + 1
Else
' WScript.Echo "Sleeping for 10 seconds"
WScript.Sleep 10000
End If
' Look for Finished Threads
Dim j
For Each j in objThreadMonitor
If objFileSystem.FileExists(j & ".results") Then
' WScript.Echo "Stopping " & j
objThreadMonitor.Remove j
End If
Next
Loop
' Wait for completion of remaining threads
Do Until objThreadMonitor.Count = 0
' WScript.Echo "Batches complete. Waiting for Thread Shutdown"
WScript.Sleep 10000
For Each j in objThreadMonitor
If objFileSystem.FileExists(j & ".results") Then
' WScript.Echo "Stopping " & j
objThreadMonitor.Remove j
End If
Next
Loop
Set objFileSystem = Nothing
Set objThreadMonitor = Nothing
Set objShell = Nothing
End Sub
Function CollateResultFiles
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objFolder : Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
Dim objLocalGroups : Set objLocalGroups = CreateObject("Scripting.Dictionary")
objLocalGroups.CompareMode = VbTextCompare
Dim objFile
For Each objFile in objFolder.Files
If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
Dim objStream : Set objStream = objFile.OpenAsTextStream(1, 0)
Do Until objStream.AtEndOfStream
Dim arrLine : arrLine = Split(objStream.ReadLine, "|")
Dim strComputer : strComputer = arrLine(0)
Dim strOS : strOS = arrLine(1)
Dim strGroup : strGroup = arrLine(2)
Dim strMembers : strMembers = arrLine(3)
objLocalGroups.Add strComputer & "/" & strGroup, Array(strOS, strMembers)
Loop
Set objStream = Nothing
End If
Next
Set CollateResultFiles = objLocalGroups
End Function
Function GetDomainGroups(objLocalGroups)
' Create a list of domain group names from the data returned
Dim objDomainGroups : Set objDomainGroups = CreateObject("Scripting.Dictionary")
Dim strLocalGroup
For Each strLocalGroup in objLocalGroups
Dim strMembers : strMembers = objLocalGroups(strLocalGroup)(1)
If strMembers <> "" Then
Dim arrMembers : arrMembers = Split(strMembers, ";")
Dim i
For i = 0 To (UBound(arrMembers) - 1)
Dim arrMember : arrMember = Split(arrMembers(i), "#")
If UBound(Split(arrMember(1), "/")) = 3 _
And InStr(arrMember(1), "NT AUTHORITY") = 0 _
And arrMember(2) = "Group" Then
Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
If Not objDomainGroups.Exists(strDomainGroup) Then
objDomainGroups.Add strDomainGroup, ""
End If
End If
Next
End If
Next
Set GetDomainGroups = objDomainGroups
End Function
Function ExpandDomainGroups(objDomainGroups, strBaseDN)
' This method requires Windows 2003 SP1 minimum
Dim strDomainGroup
For Each strDomainGroup in objDomainGroups
Dim objGroupDN : strGroupDN = GetObjectDN(strDomainGroup)
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 1000
Dim strFilter : strFilter = _
"(&(objectClass=user)(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=" & strGroupDN & "))"
objCommand.CommandText = "<LDAP://" & strBaseDN & ">;" & strFilter & ";" & _
"aDSPath,sAMAccountName;subtree"
Dim objRecordSet : Set objRecordSet = objCommand.Execute
Dim strMembers : strMembers = ""
Do Until objRecordSet.EOF
strMembers = strMembers & objRecordSet.Fields("sAMAccountName").Value & _
"#" & objRecordSet.Fields("aDSPAth") & "#User;"
objDomainGroups(strDomainGroup) = strMembers
objRecordSet.MoveNext
Loop
Set objRecordSet = Nothing
Set objCommand = Nothing
Set objConnection = Nothing
Next
Set ExpandDomainGroups = objDomainGroups
End Function
Function GetObjectDN(strObject)
' Return Type: String
'
' Returns a Distinguished Name for an Object from it's NT SAM ID.
' This will only function for valid object types within an NT Domain structure.
Const ADS_NAME_INITTYPE_GC = 3
Const ADS_NAME_TYPE_1779 = 1
Const ADS_NAME_TYPE_NT4 = 3
On Error Resume Next : Err.Clear
Dim objNameTranslate : Set objNameTranslate = CreateObject("NameTranslate")
objNameTranslate.Init ADS_NAME_INITTYPE_GC, ""
objNameTranslate.Set ADS_NAME_TYPE_NT4, strObject
Dim strObjectDN : strObjectDN = objNameTranslate.Get(ADS_NAME_TYPE_1779)
If Err.Number <> 0 Then ' Make the DN Blank for a Failed Search
strObjectDN = ""
End If
Set objNameTranslate = Nothing
On Error Goto 0
GetObjectDN = strObjectDN
End Function
Sub WriteResults(objLocalGroups, objDomainGroups)
Dim strLocalGroups
For Each strLocalGroup in objLocalGroups
WScript.Echo strLocalGroup & " - " & objLocalGroups(strLocalGroup)(0)
DisplayMembers strMembers, objDomainGroups
WScript.Echo
Next
End Sub
Sub DisplayMembers(strMembers, objDomainGroups)
If strMembers <> "" Then
Dim arrMembers : arrMembers = Split(strMembers, ";")
Dim i
For i = 0 To (UBound(arrMembers) - 1)
Dim strMember : strMember = arrMembers(i)
Dim arrMember : arrMember = Split(strMember, "#")
If UBound(Split(arrMember(1), "/")) = 3 _
And InStr(arrMember(1), "NT AUTHORITY") = 0 _
And arrMember(2) = "Group" Then
Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
If objDomainGroups.Exists(strDomainGroup) Then
strMembers = Replace(strMembers, strMember & ";", objDomainGroups(strDomainGroup))
End If
End If
Next
arrMembers = Split(strMembers, ";")
For i = 0 To (UBound(arrMembers) - 1)
arrMember = Split(arrMembers(i), "#")
WScript.Echo arrMember(0)
Next
End If
End Sub
'
' Main Code
'
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
Dim strDomainDN : strDomainDN = objRootDSE.Get("defaultNamingContext")
Set objRootDSE = Nothing
' Alternatives include GC and rootDomainNamingContext
'Dim arrComputers : arrComputers = GetComputersFromAD("LDAP", strDomainDN)
'Dim arrBatches : arrBatches = BatchGenerator(arrComputers)
'ThreadManager arrBatches
Dim objLocalGroups : Set objLocalGroups = CollateResultFiles
Dim objDomainGroups : Set objDomainGroups = GetDomainGroups(objLocalGroups)
Set objDomainGroups = ExpandDomainGroups(objDomainGroups, strDomainDN)
WriteResults objLocalGroups, objDomainGroups
Definitely failed to copy back the code from my test script.
> Dim objGroupDN : strGroupDN = GetObjectDN(strDomainGroup
Should be:
Dim strGroupDN : strGroupDN = GetObjectDN(strDomainGroup
Chris
ASKER
C:\admin.vbs(259, 3) Microsoft VBScript runtime error: Variable is undefined: 'strLocalGroup'
-rich
-rich
Same again, plural instead of singular. On that line:
> Dim strLocalGroups
Should be:
Dim strLocalGroup
Sorry for the slow responses, I try not to post too much / check mail over the weekends.
Chris
ASKER
Np, didn't expect anything til monday or after ;)
C:\-admin.vbs(261, 5) Microsoft VBScript runtime error: Variable is undefined: 'strMembers'
added Dim strMembers, seems to be working... The OS seems to repeat with each group...
-rich
C:\-admin.vbs(261, 5) Microsoft VBScript runtime error: Variable is undefined: 'strMembers'
added Dim strMembers, seems to be working... The OS seems to repeat with each group...
-rich
Good morning :)
Recoding the output into XML. I'll post an amendment with a proper round of bug-checking first.
Chris
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
ASKER
Jesus... you've out done yourself... running it now...
One thing I noticed, and I'm sure it's the nature of the beast and probably can't be helped, but killing the main script still allows the spawned scripts to continue. I know they are their own process, and I'm pretty sure there isn't much that can be done about it, again you all have exceeded all expectations!
-rich
One thing I noticed, and I'm sure it's the nature of the beast and probably can't be helped, but killing the main script still allows the spawned scripts to continue. I know they are their own process, and I'm pretty sure there isn't much that can be done about it, again you all have exceeded all expectations!
-rich
Yeah, they'll continue until they complete. The only way to kill it off completely is if we wrote another script to terminate any processes running CScript.exe (a bit tricky because it'll have to identify its own process).
Chris
ASKER
Looks like this is working well! I'm going to open another question in a few, related to the same type of multi-threaded scanning and shares.
-rich
-rich
ASKER
Again, above and beyond!
Before leaving this one.
I rewrote the thread management part (again). Splitting it into it's own moderately generic script. It accepts the name of the child script as an argument (along with a few optional arguments).
I figure we can just shift the part that deals with concatenating the results into the script executed by the FinalCommand parameter. Thread management relies on the return from Win32_Process now.
Hopefully that means all we have to do for the other tasks is concoct the child script, the concatenate the return in the final script.
Chris
Option Explicit
' Searches the domain for computers, executes a child script under a number of seperate processes for each
' batch of computers. Passes computer list to child as a semi-colon delimited list.
'
' Uses Win32_Process to monitor threads.
'
' Author: Chris Dent
' Last Modified: 17/02/2009
Sub UsageText
Dim strUsage : strUsage = "Usage:" & vbCrLf & vbCrLf
strUsage = strUsage & WScript.ScriptName & " /ChildScript:<ScriptName> [/FinalScript:<ScriptName>]" & vbCrLf
strUsage = strUsage & " [/NumComputers:<Integer>] [/NumThreads:<Integer>]" & vbCrLf
strUsage = strUsage & " [/Server:<Name>] [/BaseDN:<Distinguished Name>] [/LDAPFilter:<Filter String>]" & vbCrLf
strUsage = strUsage & " [/GlobalCatalog] [/OneLevel]" & vbCrLf & vbCrLf
strUsage = strUsage & "Required arguments:" & vbCrLf & vbCrlf
strUsage = strUsage & " ChildScript Script to execute as batch job" & vbCrLf & vbCrLf
strUsage = strUsage & "Optional arguments: " & vbCrlf & vbCrLf
strUsage = strUsage & " FinalCommand Command to execute after all processing is complete." & vbCrLf
strUsage = strUsage & " NumComputers Maximum number of computer objects to assign to each batch. (Default 10)" & vbCrLf
strUsage = strUsage & " NumThreads Maximum number of threads to execute simultaneously. (Default 10)" & vbCrLf
strUsage = strUsage & " Server LDAP server for query. (Default logon server)" & vbCrLf
strUsage = strUsage & " BaseDN Base distinguished name or OU used for search. (Default current domain)" & vbCrLf
strUsage = strUsage & " LDAPFilter LDAP Filter used when performing search. (Default objectClass=computer)" & vbCrLf
strUsage = strUsage & " GlobalCatalog Execute the search against a Global Catalog." & vbCrLf
strUsage = strUsage & " OneLevel Execute the search for this level only (Default subtree)." & vbCrLf
WScript.Echo strUsage
WScript.Quit
End Sub
Function GetArgs
Dim objArgs : Set objArgs = CreateObject("Scripting.Dictionary")
Dim strChildScript : strChildScript = WScript.Arguments.Named("ChildScript")
If strChildScript = "" Then
UsageText
End If
Dim objFSO : Set objFSO = CreateObject("Scripting.FileSystemObject")
If Not objFSO.FileExists(strChildScript) Then
WScript.Echo "ERROR: Child script not found" & vbCrLf
UsageText
End If
objArgs.Add "ChildScript", strChildScript
Set objFSO = Nothing
If WScript.Arguments.Named("FinalCommand") <> "" Then
objArgs.Add "FinalCommand", WScript.Arguments.Named("FinalCommand")
End If
Dim strNumComputers : strNumComputers = WScript.Arguments.Named("NumComputers")
If strNumComputers <> "" Then
If IsNumeric(strNumComputers) Then
objArgs.Add "NumComputers", CInt(strNumComputers)
Else
WScript.Echo "ERROR: NumComputers must be an integer value" & vbCrLf
End If
Else
objArgs.Add "NumComputers", 10
End If
Dim strNumThreads : strNumThreads = WScript.Arguments.Named("NumThreads")
If strNumThreads <> "" Then
If IsNumeric(strNumThreads) Then
objArgs.Add "NumThreads", CInt(strNumThreads)
Else
WScript.Echo "ERROR: NumThreads must be an integer value" & vbCrLf
End If
Else
objArgs.Add "NumThreads", 10
End If
objArgs.Add "Server", ""
If WScript.Arguments.Named("Server") <> "" Then
objArgs("Server") = WScript.Arguments.Named("Server") & "/"
End If
If WScript.Arguments.Named("BaseDN") <> "" Then
objArgs.Add "BaseDN", WScript.Arguments.Named("BaseDN")
End If
If Not objArgs.Exists("BaseDN") Then
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://" & objArgs("Server") & "RootDSE")
objArgs.Add "BaseDN", objRootDSE.Get("defaultNamingContext")
Set objRootDSE = Nothing
End If
objArgs.Add "LDAPFilter", "(objectClass=computer)"
If WScript.Arguments.Named("LDAPFilter") <> "" Then
objArgs("LDAPFilter") = WScript.Arguments.Named("LDAPFilter")
End If
objArgs.Add "Port", "LDAP"
objArgs.Add "Scope", "subtree"
Dim strArg
For Each strArg in WScript.Arguments
If LCase(strArg) = "/globalcatalog" Then
objArgs("Port") = "GC"
ElseIf LCase(strArg) = "/onelevel" Then
objArgs("Scope") = "onelevel"
End If
Next
Set GetArgs = objArgs
End Function
Function GetComputersFromAD(objArgs)
' Returns an array containing all computers in AD. Performed here so we do not have to maintain
' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 1000
objCommand.CommandText = "<" & objArgs("Port") & "://" & objArgs("Server") & objArgs("BaseDN") & _
">;" & objArgs("LDAPFilter") & ";" & "name;" & objArgs("Scope")
Dim objRecordSet : Set objRecordSet = objCommand.Execute
Dim arrComputers()
Dim i : i = 0
Do Until objRecordSet.EOF
ReDim Preserve arrComputers(i)
arrComputers(i) = objRecordSet.Fields("name").Value
i = i + 1
objRecordSet.MoveNext
Loop
Set objRecordSet = Nothing
Set objCommand = Nothing
Set objConnection = Nothing
GetComputersFromAD = arrComputers
End Function
Function BatchGenerator(arrComputers, objArgs)
Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
Dim intPerThread : intPerThread = objArgs("NumComputers")
' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
If (intTotalComputers / objArgs("NumThreads")) < objArgs("NumComputers") Then
intPerThread = Round(intTotalComputers / objArgs("NumThreads"))
End If
Dim intCurrentBatchCount : intCurrentBatchCount = 0
Dim intBatchNo : intBatchNo = 0
Dim arrBatches() : ReDim arrBatches(intBatchNo)
Dim strComputer
For Each strComputer in arrComputers
If intCurrentBatchCount = intPerThread Then
intCurrentBatchCount = 0
intBatchNo = intBatchNo + 1
ReDim Preserve arrBatches(intBatchNo)
End If
intCurrentBatchCount = intCurrentBatchCount + 1
arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
Next
For intBatchNo = 0 to UBound(arrBatches)
arrBatches(intBatchNo) = Left(arrBatches(intBatchNo), Len(arrBatches(intBatchNo)) - 1)
Next
BatchGenerator = arrBatches
End Function
Sub ThreadManager(arrBatches, objArgs)
' Starts multiple script processes and waits for completion
Dim objShell : Set objShell = CreateObject("WScript.Shell")
' Execute thread for each batch
Dim i : i = 0
Do Until i = (UBound(arrBatches) + 1)
If GetProcessCount(objArgs("ChildScript")) < objArgs("NumThreads") Then
WScript.Echo "Starting Batch " & (i + 1) & " of " & (UBound(arrBatches) + 1)
objShell.Run "cscript.exe " & objArgs("ChildScript") & " " & arrBatches(i), 0, False
i = i + 1
Else
WScript.Sleep 10000
End If
Loop
' Wait for completion of remaining threads
Do Until GetProcessCount(objArgs("ChildScript")) = 0
WScript.Sleep 10000
Loop
Set objShell = Nothing
End Sub
Function GetProcessCount(strChildScript)
' Returns the number of cscript or wscript processes executing ChildScript.
Dim objWMI : Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
Dim colItems : Set colItems = _
objWMI.ExecQuery("SELECT CommandLine, Name, ProcessId " & _
"FROM Win32_Process WHERE Name LIKE '_script.exe' AND CommandLine LIKE '% " & _
strChildScript & " %'")
GetProcessCount = colItems.Count
Set colItems = Nothing
Set objWMI = Nothing
End Function
Sub ExecuteFinal(objArgs)
If objArgs.Exists("FinalCommand") Then
Dim objShell : Set objShell = CreateObject("WScript.Shell")
objShell.Run objArgs("FinalCommand"), 0, True
Set objShell = Nothing
End If
End Sub
'
' Main code
'
Dim objArgs : Set objArgs = GetArgs
Dim arrComputers : arrComputers = GetComputersFromAD(objArgs)
Dim arrBatches : arrBatches = BatchGenerator(arrComputers, objArgs)
ThreadManager arrBatches, objArgs
ExecuteFinal(objArgs)
I think the only way to speed up such a sequential script like this (which look likes I wrote it...:-)), is to change it to accept a bunch of computer names at one time, so that it runs sequentially against say, 10 machines. Then, you'd create a "parent" script that would enumerate AD, (lines 38 to 51), and call the "child" script with each 10 PCs or so.....
This way, you run multiple processes of the same script at the same time, each querying 10 different PCs.
Do you need assistance with creating a script to do that?
Regards,
Rob.