Posts RSS Comments RSS 253 Posts and 411 Comments till now

blog: Discovery options with R2 AD Cmdlets

Last week I talked about how to "discover" information using the built in .NET classes for ActiveDirectory. This week I would like to show how you can do similar things with the ActiveDirectory cmdlets that ship with Win7 and R2.

The first task we discussed was getting Forest information like Domains, Sites, ForestMode, RootDomain, and Forest masters.

With .NET we do this
  1. $Forest = [DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
With the cmdlets we do this
  1. $Forest = Get-ADForest

Next we discussed getting Domain information like Domain Controllers, DomainMode, Domain Masters, and Forest Root.
  1. $Domain = [DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
With the cmdlets we do this
  1. $Domain = Get-ADDomain

Now the object we get back is slightly different so lets take a look

First lets look at what $Forest has to offer
  1. PS C:UsersAdministrator> $Forest
  2.  
  3. ApplicationPartitions : {}
  4. CrossForestReferences : {}
  5. DomainNamingMaster : Win2K8R2DC1.R2.Dev.Lab
  6. Domains : {R2.Dev.Lab}
  7. ForestMode : Windows2008R2Forest
  8. GlobalCatalogs : {Win2K8R2DC1.R2.Dev.Lab}
  9. Name : R2.Dev.Lab
  10. PartitionsContainer : CN=Partitions,CN=Configuration,DC=R2,DC=Dev,DC=Lab
  11. PSShowComputerName : {}
  12. RootDomain : R2.Dev.Lab
  13. SchemaMaster : Win2K8R2DC1.R2.Dev.Lab
  14. Sites : {Default-First-Site-Name}
  15. SPNSuffixes : {}
  16. UPNSuffixes : {}
  17. WriteErrorStream : {}

Finally, Lets look at $Domain
  1. PS C:UsersAdministrator> $domain
  2.  
  3. AllowedDNSSuffixes : {}
  4. ChildDomains : {}
  5. ComputersContainer : CN=Computers,DC=R2,DC=Dev,DC=Lab
  6. DeletedObjectsContainer : CN=Deleted Objects,DC=R2,DC=Dev,DC=Lab
  7. DistinguishedName : DC=R2,DC=Dev,DC=Lab
  8. DNSRoot : R2.Dev.Lab
  9. DomainControllersContainer : OU=Domain Controllers,DC=R2,DC=Dev,DC=Lab
  10. DomainMode : Windows2008R2Domain
  11. DomainSID : S-1-5-21-4244231903-4101880959-1987002231
  12. ForeignSecurityPrincipalsContainer : CN=ForeignSecurityPrincipals,DC=R2,DC=Dev,DC=Lab
  13. Forest : R2.Dev.Lab
  14. InfrastructureMaster : Win2K8R2DC1.R2.Dev.Lab
  15. LastLogonReplicationInterval :
  16. LinkedGroupPolicyObjects : {CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=R2,DC=Dev,DC=Lab}
  17. LostAndFoundContainer : CN=LostAndFound,DC=R2,DC=Dev,DC=Lab
  18. ManagedBy :
  19. Name : R2
  20. NetBIOSName : R2
  21. ObjectClass : domainDNS
  22. ObjectGUID : c2d8e67d-2a49-4352-a795-de2b6508b1dc
  23. ParentDomain :
  24. PDCEmulator : Win2K8R2DC1.R2.Dev.Lab
  25. QuotasContainer : CN=NTDS Quotas,DC=R2,DC=Dev,DC=Lab
  26. ReadOnlyReplicaDirectoryServers : {}
  27. ReplicaDirectoryServers : {Win2K8R2DC1.R2.Dev.Lab}
  28. RIDMaster : Win2K8R2DC1.R2.Dev.Lab
  29. SubordinateReferences : {CN=Configuration,DC=R2,DC=Dev,DC=Lab}
  30. SystemsContainer : CN=System,DC=R2,DC=Dev,DC=Lab
  31. UsersContainer : CN=Users,DC=R2,DC=Dev,DC=Lab

Here are some more specific examples on how to use these variables:

To see the forest roles
  1. $forest | select SchemaMaster,DomainNamingMaster
To see the domain roles
  1. $domain | select PDCEmulator,RIDMaster,InfrastructureMaster
To see what application partitions your forest has
  1. $forest.ApplicationPartitions

NOTE: you can use this command to see all the AD Cmdlets have to offer
  1. get-command -Module ActiveDirectory

Breaking down DCDiag.exe to an object

Using some regex magic and some custom object mojo I threw together this DCDiag objectifier.

These are very early bits (only spent a few minutes on it) and may not go any further as I think this would be better as a cmdlet, but it was fun to play with and show some of the POWER you have at your finger tips.

The Code

$DCDiag = Get-Content c:\temp\dcdiag.txt

# Creating the DCDiag object
$DCDiagObject = "" | Select-Object Server,Advertising,SPNs,KnownRoles,Tests
$DCDiagObject.Server = ""
$DCDiagObject.Advertising = @()
$DCDiagObject.KnownRoles = @()
$DCDiagObject.SPNs = @()
$DCDiagObject.Tests = @()

# Setting up RegEXs here so they are easier to consume for the viewer
$DCNameRegex = "^\s*\* Connecting to directory service on server (?<DCName>\w+)."
$Advertising = "^\s+The\sDC\s\w*\sis\sadvertising\sas\s(a|an|having a)\s(?<Type>.*)"
$KnownRolesRegex = "^.*Role\s(?<Role>.*)\sOwner = CN=NTDS Settings,CN=(?<Holder>\w*),"
$SPNsRegex = "^\s+\* SPN found \:(?<SPN>.*)"
$StartRegex = "^\s*\.+\s(?<Target>\w+)\s(?<Result>\w+)\stest\s(?<Test>\w+)"

# Getting stuff done
switch -regex ($DCDiag)
{
    $DCNameRegex        {$DCDiagObject.Server = $matches.DCName}
    $Advertising        {$DCDiagObject.Advertising += $matches.Type}
    $KnownRolesRegex    {$DCDiagObject.KnownRoles += $matches.Role}
    $SPNsRegex          {$DCDiagObject.SPNs += $matches.SPN}
    $StartRegex         {
                            $myobj = "" | Select-Object Target,Test,Result
                            $myobj.Target = $matches.Target
                            $myobj.Test = $matches.Test
                            $myobj.Result = $matches.Result
                            $myobj | Add-Member -MemberType ScriptMethod -name ToString -value {$this.Test} -force
                            $DCDiagObject.Tests += $myobj
                        }
}

# outputting object
$DCDiagObject

Getting users group membership (tokengroups)

Around the same time as I wrote my Linked-Value Attribute script I also came up with this little gem. It also uses a constructed attribute provided with Windows 2003 called “tokengroups.” (Did I mention this includes recursive groups?)

Effectively it gets the attribute which returns an array of SIDs (in byte array form) for each group. I then use a function I posted about eariler called ConvertTo-Name to convert that BYTE array into a friendly group name we humans like.

Parameters:

  • – Account: Can be User samAccountName or DN of the user
  • – Verbose: Enables verbose output

More Info:
You may notice the GetInfoEx call I make on the user object. This is because tokengroups is not an actual attribute and does not get “populated” until you specifically request it. The GetInfoEx does exactly that.

Links:
MSDN: GetInfoEx
MSDN: tokenGroups

Get-TokenGroups.ps1

Param($Account,[switch]$Verbose)

if($verbose){$verbosepreference="Continue"}

Write-Host
Write-Verbose " – Account: $Account"
Write-Verbose " – Verbose: $Verbose"

function ConvertTo-Name($sid,[switch]$FromByte) {
    if($FromByte)
    {
        $ID = New-Object System.Security.Principal.SecurityIdentifier($sid,0)
    }
    else
    {
        $ID = New-Object System.Security.Principal.SecurityIdentifier($sid)
    }
    if($ID)
    {
        $User = $ID.Translate([System.Security.Principal.NTAccount])
        $User.Value
    }
}
function GetDNfromName{
    Param($name)
    $root = [ADSI]""
    $filter = "(sAMAccountName=$name)"
    $props = @("distinguishedName")
    $Searcher = new-Object System.DirectoryServices.DirectorySearcher($root,$filter,$props)
    $Searcher.FindOne().properties.distinguishedname
}

if($Account -notmatch "CN=(.*),((OU|DC)=\w*)*")
{
    Write-Verbose " + Getting User DN for [$Account]"
    $Account = GetDNfromName $Account
    Write-Verbose "   – GetDNfromName returned [$Account]"
}

Write-Verbose " – Getting User Object"
$UserAccount = [ADSI]"LDAP://$Account"

Write-Verbose " – Calling GetInfoEx"
$UserAccount.GetInfoEx(@("tokengroups"),0)

Write-Verbose " – Getting tokengroups"
$groups = $UserAccount.Get("tokengroups")

Write-Verbose " + Processing Groups"
foreach($group in $groups)
{
    $GroupName = ConvertTo-Name $group -FromByte
    Write-Verbose "   – Found group [$GroupName]"
    $GroupName
}

Write-Host

Setting the WriteProperty on Members (ManagedBy Check box)

This has come up three times in the last week which triggers my auto blog response 🙂

Below is a function called New-ADAce. This function creates an Access Control Entry that can be applied to an AD Object (in this case the member property of an AD object.)

Basically what the code below does is:

  • Gets the ID object of the Manager
  • Creates ACE that gives the Manager Write access to the member property
  • Gets the Object to be managed
  • Gets the existing ACL
  • Addes the ACE to the ACL
  • Sets the ManagedBy Property to the DN of the Manager
  • Commits the changes
Param(
   $myGuid = "bf9679c0-0de6-11d0-a285-00aa003049e2", #GUID for the Members property
   $DN = "cn=jloser,cn=users,dc=corp,dc=lab",
   $domain = $env:UserDomain,
   $manager = "jmanager",
   $MangedByDN = ""cn=jmanager,cn=users,dc=corp,dc=lab""
)

function New-ADACE {
   Param([System.Security.Principal.IdentityReference]$identity,
   [System.DirectoryServices.ActiveDirectoryRights]$adRights,
   [System.Security.AccessControl.AccessControlType]$type,
   $Guid)

   $help = @"
   $identity
      System.Security.Principal.IdentityReference
      http://msdn.microsoft.com/en-us/library/system.security.principal.ntaccount.aspx
   $adRights
      System.DirectoryServices.ActiveDirectoryRights
      http://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectoryrights.aspx
   $type
      System.Security.AccessControl.AccessControlType
      http://msdn.microsoft.com/en-us/library/w4ds5h86(VS.80).aspx
   $Guid
      Object Type of the property
      The schema GUID of the object to which the access rule applies.
"
@
   $ACE = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($identity,$adRights,$type,$guid)
   $ACE
}

# Some example code on how to use the New-ADACE function

# Create ACE to add to object
$ID = New-Object System.Security.Principal.NTAccount($domain,$manager)
$newAce = New-ADACE $ID "WriteProperty" "Allow" $myGuid

# Get Object
$ADObject = [ADSI]"LDAP://$DN"

# Set Access Entry on Object
$ADObject.psbase.ObjectSecurity.SetAccessRule($newAce)

# Set the manageBy property
$ADObject.Put("managedBy",$MangedByDN)

# Commit changes to the backend
$ADObject.psbase.commitchanges()

Setting lDAPAdminLimits via Powershell

I was having a conversation with a friend the other day and he brought up a question about updating the AD property lDAPAdminLimits.

The Problem
The property is stored as an array of string values (at least as far as Powershell is concerned.) The initial reaction was do try something like this $queryPolicies.lDAPAdminLimits.MaxNotificationPerConn = 30, but this assumed that MaxNotficationPerConn was a property of lDAPAdminLimits and not the actual value (or at least part of the value.)

The Solution
Use the ADSI method PutEX to modify the value. PutEx uses ADS_PROPERTY_OPERATION_ENUM to make selective changes to an existing property. In the script below, we add the new value using the Update operation and then use the Delete operation to remove the old value.

Some Examples of Use

D:\Scripts\Set-ldapAdminPolicy.ps1 MaxNotificationPerConn 45
D:\Scripts\Set-ldapAdminPolicy.ps1 MaxQueryDuration 360
D:\Scripts\Set-ldapAdminPolicy.ps1 MaxPageSize 500
D:\Scripts\Set-ldapAdminPolicy.ps1 MaxPoolThreads 8

Here is a link on how to view/set via NTDSUtil.exe
How to view and set LDAP policy in Active Directory by using Ntdsutil.exe

The Code

Param($policy=$(throw ‘$policy is required’),$count=30)
$rootDSE = [ADSI]"LDAP://rootDSE"
$config = $rootDSE.configurationNamingContext
$queryPolicies = [adsi]"LDAP://CN=Default Query Policy,CN=Query-Policies,cn=Directory Service,cn=Windows NT,CN=Services,$config"
$oldvalue = $queryPolicies.lDAPAdminLimits | ?{$_ -match $policy}
$queryPolicies.PutEx(3,"lDAPAdminLimits",@("$policy=$count"))
$queryPolicies.Setinfo()
$queryPolicies.PutEx(4,"lDAPAdminLimits",@("$oldvalue"))
$queryPolicies.Setinfo()

Bulk Imports using CSV (cross post from TurboChargeAD.org)

As some of you may know I am doing some guest blogging over at TurboChargeAD.org and below is a link to the second such article.

Here is the Link: Bulking Importing User from CSV file using Quest cmdlets

Here is the code

#Import-ADUser.ps1
#requires -pssnapin Quest.ActiveRoles.ADManagement
Param($file,$OU,[switch]$whatif,[switch]$verbose)

if($verbose){$VerbosePreference = "Continue"}

if(!(test-path $file))
{
   throw " File NOT found!"
   return
}

if(!$ou)
{
    # Setting the OU to the Users container
    $ou = "CN=Users,{0}" -f ([ADSI]"").distinguishedName[0]
}

Write-Verbose " Using File: $file"
Write-Verbose " Using OU: $ou"

foreach($user in (Import-Csv $file))
{
    $props = @{}
    # Getting a list property names.
    $propNames = $user | Get-Member -MemberType properties | %{$_.name}
    # Foreach of the property names add an entry in the hash table with
    # the key being the property name and value being the value from the object
    foreach($prop in $propNames)
    {
        # This removes quotes if they exist
    $value = $user.$prop -replace "’|`"",""
        $props += @{$prop=$value}
    }
    # Create User using the displayname as the CN
    $MyUser = new-qaduser -name $user.displayName `
                          -ObjectAttributes $props `
                          -parent $OU `
                          -whatif:$whatif `
                          -verbose:$verbose
    $MyUser
}

What Domain Controllers were IFM’d and from whom.

This is to provide Context to joe’s post and a Powershell script I used to get the values to determine if our DC’s were Installed from Media.

I manage a very large Active Directory (~380k users) which causes a predictably large DIT (click here to Learn about DITs.) We recently encountered one specific DC exhibiting odd behavior: (LSASS) was churning an unusually large amount of disk IO.  This problem was quickly resolved and is of little interest but resulted in another good-to-know fact.  As we begun our investigation, one of the first things we looked at was how large the DIT was; roughly 4.8GB for this particular DC.  Dean (who consults for us full-time) and I first tried to determine if the churn was a product of our environment combined with DIT Bloat [1] or some kind of other whacky effect/limitation.  Dean was told that it was a newly-born DC and initially discounted DIT bloat since AD does NOT replicate white space.  Just a few minutes later, I recollected and piped up that I had promoted this particular DC via IFM [2] – this made a huge difference to our earlier and incorrect (albeit brief) conclusion: during an IFM, the DIT of the originating DC (white-space and all) is literally copied bit for bit and serves as the template from which the new database is built (necessary modifications acknowledged.)

This brings us to the moral of my story: how do we determine if a DC was promoted via IFM – a great question and an even better answer.  joe’s® post (HERE) was the result of a lengthy and possibly pointless [3] IM between him and Dean.  The short answer is there are some things that DCs create locally during replication causing the metadata to indicate that the local DC was the originating writer of certain attributes.  As a result, it is likely that a DC that lists itself as the originator of the RDN attribute for an object that existed prior to this DC’s promotion, (RDN attribute equating to CN, OU or DC / CN for say the Users container) was either the first DC to host this partition (or, typically, the DC that created this domain) or was promoted via replication (either way, not IFM’d.)  If, however, the attribute’s metadata indicates that another DC originated the RDN attribute, we can assume (though not yet without question since none of us have sufficient data to confirm that) that this DC was IFM’d and that the metadata indicates from whom the IFM’s backup-data was originally sourced (this is not necessarily the same DC from which this particular IFM was taken since it too may have been promoted from yet another IFM.)  HOW COOL IS THAT?

Here is a little [4] Script I used to get me the info:[5]

# Get the Current Domain
$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()
# Get the User Container for the Domain. We use this for the Metadata
$ConfigContainer = "CN=Configuration,{0}" -f ([adsi]"").distinguishedName[0]
# The final line enums each DC and check the MetaData for the CN of the Users Container. If IFM’d this should be a remote DC. If local it was replicated.
$domain.DomainControllers | Select Name,@{n="USN";e={$_.GetReplicationMetadata($ConfigContainer).cn | select name,LocalChangeUsn,OriginatingChangeUsn,OriginatingServer}} | ft -auto

Here is what the output of the script looks

Name                          USN
====                         ===
HomeDC1.corp.lab     @{Name=cn; LocalChangeUsn=4100; OriginatingChangeUsn=4100; OriginatingServer=HomeDC1.corp.lab}
HomeDC2.corp.lab     @{Name=cn; LocalChangeUsn=5552; OriginatingChangeUsn=5552; OriginatingServer=HomeDC2.corp.lab}
Lab0DC2.corp.lab       @{Name=cn; LocalChangeUsn=5552; OriginatingChangeUsn=5552; OriginatingServer=Lab0DC2.corp.lab}
Lab0DC1.corp.lab       @{Name=cn; LocalChangeUsn=5552; OriginatingChangeUsn=5552; OriginatingServer=Lab0DC1.corp.lab}
Home1dc1.corp.lab  @{Name=cn; LocalChangeUsn=5691; OriginatingChangeUsn=5691; OriginatingServer=Home1dc1.corp.lab}
Home1dc2.corp.lab    @{Name=cn; LocalChangeUsn=5691; OriginatingChangeUsn=5691; OriginatingServer=Home1dc1.corp.lab}
Lab1dc1.corp.lab       @{Name=cn; LocalChangeUsn=5691; OriginatingChangeUsn=5691; OriginatingServer=Home1dc1.corp.lab}
Lab1dc2.corp.lab       @{Name=cn; LocalChangeUsn=5691; OriginatingChangeUsn=5691; OriginatingServer=Home1dc1.corp.lab}

[1] Here is one kind of DIT bloat scenario. (HERE.)
[2] More info on IFM (Install From Media) (HERE)
[3] Comment from Dean
[4] joe posted a “one liner” perl script, I could have easily posted one as well, but I wanted my script to be clear in intent. You could simply do this:

([System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()).DomainControllers | Select Name,@{n="USN";e={$_.GetReplicationMetadata("CN=Configuration,{0}" -f ([adsi]"").distinguishedName[0]).cn | select name,LocalChangeUsn,OriginatingChangeUsn,OriginatingServer}} | ft -auto

[5] I used the configuration container for this. This script assumes you are in the forest root.

Build Lab (v1 w/out Quest Tools)

This script worked for me… just took a few days 🙂

To Recap. This does the following. In my final revision I am removing the last two steps… it TAKES FOREVER!!! and its not the useful.

# A TestOU OU
# A TestComputers OU
# A TestUsers OU
# A TestGroups OU
# 10K OU’s Under TestOU
## Each of the 10k OUs will have 4 Child OUs
### Each OU should have 5 users Accounts and 5 Machines Accounts
# Create 500 Group Policies.
# Link 100 policies on the 10k Base OUs
# Create 2000 Users in the TestUser OU
# Create 2000 Computers in the TestComputer OU
# Find all the Users
# Create 2K Groups
## Add Even Numbered Users to Even Groups
## Add Odd Numbered Users to Odd Groups

function New-ADOU{
    Param($Name,$OU,$DC)
    # Get Root Path for OU
    if($dc -and $ou){$root = "LDAP://$dc/$ou"}
    if($dc -and !$ou){$root = "LDAP://{0}" -f $dc,(([ADSI]"LDAP://$dc/rootDSE").DefaultNamingContext)}
    if(!$dc -and $ou){$root = "LDAP://$OU"}
    if(!$dc -and !$ou){$root = "LDAP://{0}" -f (([ADSI]"LDAP://rootDSE").DefaultNamingContext)}

    #Write-Host ("Creating OU [{0}] Using Path [{1}]" -f $Name,$Root)

    # Creating Account in OU
    $BaseOU = [ADSI]"$root"
    $NewOU = $BaseOU.Create("organizationalUnit","OU=$Name")
    $NewOU.Setinfo()
    $NewOU.distinguishedName
}
function New-ADUSer{
    Param($user,$password="P@ssw0rd",$dc,$ou)

    # Get Root Path for OU
    if($dc -and $ou){$root = "LDAP://$dc/$ou"}
    if($dc -and !$ou)
    {$root = "LDAP://{0}/CN=Users,{1}" -f $dc,(([ADSI]"LDAP://$dc/rootDSE").DefaultNamingContext)}
    if(!$dc -and $ou)
    {$root = "LDAP://$OU"}
    if(!$dc -and !$ou)
    {$root = "LDAP://CN=Users,{0}" -f (([ADSI]"LDAP://rootDSE").DefaultNamingContext)}

    #Write-Host ("Creating user [{0}] Using Path [{1}]" -f $user,$Root)

    # Creating Account in OU
    $UserOU = [ADSI]"$root"
    $userObj = $UserOU.Create("User","CN=$user")

    # Set samAccountName
    $userObj.put("samAccountName","$user")
    $userObj.Setinfo()

    # Set Password
    $userObj.psbase.invoke("setpassword",$password)
    $userObj.Setinfo()

    # Enable Account
    $userObj.psbase.invokeset(‘accountdisabled’, $false)
    $userObj.Setinfo()
    $userObj.distinguishedName
}
function New-ADComputer{
    Param($Name,$OU,$DC)
    # Get Root Path for OU
    if($dc -and $ou){$root = "LDAP://$dc/$ou"}
    if($dc -and !$ou)
    {$root = "LDAP://{0}/CN=Users,{1}" -f $dc,(([ADSI]"LDAP://$dc/rootDSE").DefaultNamingContext)}
    if(!$dc -and $ou)
    {$root = "LDAP://$OU"}
    if(!$dc -and !$ou)
    {$root = "LDAP://CN=Computers,{0}" -f (([ADSI]"LDAP://rootDSE").DefaultNamingContext)}

    #Write-Host ("Creating user [{0}] Using Path [{1}]" -f $user,$Root)

    # Creating Account in OU
    $CompOU = [ADSI]"$root"
    $CompObj = $CompOU.Create("Computer","CN=$Name")

    # Set samAccountName
    $CompObj.put("samAccountName","$Name`$")
    $CompObj.Setinfo()

    # Enable Account
    $CompObj.psbase.invokeset(‘accountdisabled’, $false)
    $CompObj.Setinfo()
    $CompObj.distinguishedName
}
function New-ADGroup{
    Param($OU,$Grp,$dc)
    Write-Host " + Creating Group [$Grp] in OU [$OU]"

    # Get Root Path of OU
    if($dc){$GroupOU  = [ADSI]"LDAP://$dc/$ou"}
    else{$GroupOU  = [ADSI]"LDAP://$ou"}

    # Create Group
    $GroupObj = $GroupOU.Create("Group","CN=$Grp")
    $Groupobj.SetInfo()
    $Groupobj.distinguishedName
}
function Add-UsertoGroup{
    Param($User,$Grp,$DC)
    if($DC){$myGroup = [ADSI]"LDAP://$DC/$Grp"}
    else{$myGroup = [ADSI]"LDAP://$Grp"}
    #Write-Host "     – Processing User [$User] in Group [$Grp]"
    $myGroup.Add("LDAP://$user")
    $myGroup.SetInfo()
}

#A TestOU OU
Write-Host " + Creating TestOU"
$TestOU = New-ADOU -name TestOU

#A TestComputers OU
Write-Host " + Creating TestComputers OU"
$TestComp = New-ADOU -name TestComputers

#A TestUsers OU
Write-Host " + Creating TestUsers OU"
$TestUsers = New-ADOU -name TestUsers

#A TestGroups OU
Write-Host " + Creating TestGroups OU"
$TestGroups = New-ADOU -name TestGroups

#10K OU’s Under TestOU
foreach($n in 1..10000)
{
    Write-Host " + Creating Level1 OU [Level1OU$N]"
    $Level1 = New-ADOU -name "Level1OU$N" -ou $TestOU
    # Each of the 10k OUs will have 4 Child OUs
    foreach($i in 1..4)
    {
        Write-Host "   + Creating Level2 OU [Level2OU$i]"
        $Level2 = New-ADOU -name "Level2OU$i" -ou $Level1
        #Each OU should have 5 users Accounts and 5 Machines Accounts
        foreach($x in 1..5)
        {
            Write-Host "     – Creating User [Lvl2User$n$i$x] in [$Level2]"
            New-ADUSer -user "Lvl2User$n$i$x" -OU $Level2 | out-Null
            Write-Host "     – Creating Computer [Lvl2Comp$n$i$x] in [$Level2]"
            New-ADComputer -name "Lvl2Comp$n$i$x" -OU $Level2 | out-Null
        }
    }
}

#Create 500 Group Policies.
1..500 | %{New-SDMgpo "TestGPO$_"}

#Link 100 policies on the 10k Base OUs
1..100 | %{Add-SDMgplink -name "TestGPO$_" -Scope "OU=Level1OU$_,$TestOU" -Location -1}

#Create 2000 Users in the TestUser OU
1..2000 | %{New-ADUSer -user "TestUser$_" -OU $TestUsers}

#Create 2000 Computers in the TestComputer OU
1..2000 | %{New-ADComputer -user "TestComputer$_" -OU $TestComputers}

# Find all the Users
$props = @("sAMAccountName","distinguishedName")
$ds = new-Object System.DirectoryServices.DirectorySearcher([ADSI]"","(objectcategory=user)",$props)
$ds.pagesize = 100
$users = $ds.Findall()
$eUsers = $users | ?{ $_.properties.item("sAMAccountName") -match ‘(2|4|6|8|0)$’ } | `
              select-Object @{n="Name";e={$_.properties.item("distinguishedName")}}
$oUsers = $users | ?{ $_.properties.item("sAMAccountName") -match ‘(1|3|5|7|9)$’ } | `
              Select-Object @{n="Name";e={$_.properties.item("distinguishedName")}}

#Create 2K Groups
foreach($i in 1..2000)
{
    $NewGrp = New-ADGroup -Grp "TestGrp$i" -OU $TestGroups
    if($i%2 -eq 0)
    {
        Write-Host "   + Adding Even Users to Group [$NewGrp]"
        $eUsers | Select-Object | %{ Add-UsertoGroup -user $_.name -Grp $NewGrp }
    }
    else
    {
        Write-Host "   + Adding Odd Users to Group [$NewGrp]"
        $oUsers | Select-Object |%{ Add-UsertoGroup -user $_.name -Grp $NewGrp }
    }
}