Posts RSS Comments RSS 253 Posts and 411 Comments till now

Blog Archives

Multiple Paths to the same End (Citrix)

Today I was asked this question

“Is there a more efficient way to list each published app and list out servers that are associated with it? In hope to give me an output that will display the ‘AppName’ objects once and display all the servers.”

He was referring to script I have posted somewhere (/shrug.)

I knew exactly what he wanted and thought “HEY! This would be a good time to blog about MFCom object Nesting.”

What do I mean by “MFCom object Nesting?” I simply mean that MFCom object model is nested in such a way that you can attain the same information from various code paths. For example, Farm objects contain Application objects which contain Server objects, but Farm objects also have Server objects which contain Application objects.

Farm -> Applications -> Servers
Or
Farm -> Servers -> Applications

Why is this important or why would Citrix do this? IMO, it is to provide a shorter code path for the information you want.

Here are some examples

In the case of the question I received, what he had required getting all the applications from each server and returning the information like this:

$mfarm.Servers | Select ServerName -expandproperty Applications | Select ServerName,
                                                                          AppName,
                                                                          DistinguishedName,
                                                                          @{n="Groups";e={$_.Groups | %{$_.GroupName}}}

What he wanted to get was all the applications and output the information directly like:

$mfarm.Applications | %{$_.LoadData(1);$_} | Select-Object AppName,
                                                           DistinguishedName,
                                                           @{n=‘Groups’;e={$_.Groups | %{$_.GroupName}}},
                                                           @{n=‘Servers’;e={$_.Servers | %{$_.ServerName}}}

 
Perhaps a more direct example would be collecting Sessions. If I want all the Sessions for a Server I could do one of the following:

$farm.Sessions | ?{$_.ServerNamematch "Server1"}

this works, but has to touch all my sessions… a better way would be like:

Get-CitrixServer  "Server1" | %{$_.Sessions}

 
More Info on LoadData()
If you are not familiar with the LoadData() method on the Application object it is critical that you get familiar, and get familiar quick.

LoadData() was introduced in MPS 4.0 (I believe) to allow the farm (or directly instantiated Application objects) to be returned with just a small bit of information. This allows you to quickly get basic application information without having to collect the entire data set, saving both time and network traffic. The problem is that most people do not know this and it can get quiet messy.

Why is it messy? If you get an application and set a property (like adding a user) and then commit the data back to the server using the SaveData() method, what do you think will happen? One could expect it would only update that which has change, but one would be wrong. When you call SaveData() it actually commits whatever is in local memory to the farm (yup, you got it… HOSED!) You just committed an effectively empty application and set it back to the Farm wiping out any existing information.

Moral of the story? USE LoadData().

Using rootDSE mods to transfer role ownership

A while back I had conversation with Richard siddaway about a blog entry he made HERE.

The basic gist is there seems to be a bug in the TransferRoleOwnership method of .NET Class DomainController. There is clearly a problem, but I say “seems” because strictly speaking the DomainController class does not include Windows 2008.

Thus we fall back on our old trusty rootDSE mods and for those of you that have never heard of rootDSE mods (you may have heard them referred to as Operational Attributes) I definately recommend reading up on them HERE.

Effectively what we do is get the rootDSE for the DC we want to transfer the role to and set one of the following mods to 1:

  • becomeInfrastructureMaster
  • becomePDC
  • becomeRidMaster
  • becomeSchemaMaster
  • becomeDomainMaster
  • You, of course, need to have the valid rights to perform the operation.

    Here is the Script I use

    Param($Server = "127.0.0.1",$role)
    if(!$role){return "Please enter a valid Role: IM,PDC,RID,Schema,DM"}
    $rootDSE = [ADSI]"LDAP://$Server/rootDSE"
    Write-Host
    Write-Host " Moving FSMO Role"
    Write-Host " – Using Server: [$Server]"
    Write-Host " – Using Role:   [$Role]"
    switch -exact ($role)
    {
        "IM"        {$myrole = ‘becomeInfrastructureMaster’}
        "PDC"       {$myrole = ‘becomePDC’}
        "RID"       {$myrole = ‘becomeRidMaster’}
        "Schema"    {$myrole = ‘becomeSchemaMaster’}
        "DM"        {$myrole = ‘becomeDomainMaster’}
        Default     {return "Please provide Valid Role: IM,PDC,RID,Schema,DM"}
    }
    Write-Host " – Performing $MyRole on $Server"
    $rootDSE.put($myRole,1)
    $rootDSE.SetInfo()
    Write-Host

    Getting the UpToDateness Vector (UTDV) in Powershell

    A year or so ago I needed to get the UTDV Table (defined in detail HERE by Laura Hunter of ShutUpLaura.) At the time, the only way I knew how to get this information was to get the replUpToDateVector attribute from the NC on the target DC. I could have used Repadmin shown below, but that wouldn’t have been any fun. I did however end up with the massive script at the bottom of this post. It required decoding the UTDV table and resolving the Invocation ID to a name or “DeletedDSA.”

    Fast forward to the present.

    During the course of a long conversation on an ActiveDir thread (“Domain Controller Version”,) joe (aka joeware) and I got into side conversation about getting the UTDV table (ironically due to a statement/question in the thread by the same Laura, mentioned earlier.)

    This led me to a wonderful little .NET method (GetReplicationCursors) on DirectoryServices.ActiveDirectory.DomainController class. This was so much easier I wanted to kick myself for not finding it sooner.

    In any case, it was a great learning experience so I wanted to share it you. Enjoy my pain and victory.

    Using Repadmin

    repadmin /showutdvec [dc]  "[ DN for Naming Context ]"

    The easy way.
    Using System.DirectoryServices.ActiveDirectory.DomainController

    Write-Host
    $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
    $NC = "DC={0}" -f ($domain.Name -replace "\.",",DC=")
    foreach($dc in $domain.DomainControllers)
    {
        Write-Host "$($DC.Name)"
        Write-Host "================"
        $UDTV =  $dc.GetReplicationCursors($NC)
        $GUID = @{n=‘DSA’;e={if($_.SourceServer){$_.SourceServer}else{$_.SourceInvocationId}}}
        $UDTV | Select-Object $GUID,
                              UpToDatenessUsn,
                              LastSuccessfulSyncTime | Sort DSA -desc | Format-Table -auto
        Write-Host
    }

    The Hard way.
    I initially did all the work myself using this function. This gets the Up To Dateness vector and decodes it. It also translates the Invocation ID (if possible.)

    param([string]$server)

    Begin{
        function Get-InvocationIDFromBytes{
            Param([byte[]]$GuidArray)
            $GUIDSection1 = @(1..4)
            $GUIDSection2 = @(1..2)
            $GUIDSection3 = @(1..2)
            $GUIDSection4 = @(1..2)
            $GUIDSection5 = @(1..6)
       
            $start = 0
           
            $InvocationIDString = ""
           
            foreach($byte in $GuidArray)
            {
                $InvocationIDString = $InvocationIDString + "\" + ([string]::Format("{0:X2}",$byte))
            }
           
            [system.Array]::Copy($GuidArray,$start,$GUIDSection1,0,4)
            [system.Array]::Copy($GuidArray,($start+4),$GUIDSection2,0,2)
            [system.Array]::Copy($GuidArray,($start+6),$GUIDSection3,0,2)
            [system.Array]::Copy($GuidArray,($start+8),$GUIDSection4,0,2)
            [system.Array]::Copy($GuidArray,($start+10),$GUIDSection5,0,6)
            [system.Array]::Reverse($GUIDSection1)
            [system.Array]::Reverse($GUIDSection2)
            [system.Array]::Reverse($GUIDSection3)
           
            [string]$GuidString1 = $GUIDSection1 | %{[string]::Format("{0:X2}",$_)}
            [string]$GuidString2 = $GUIDSection2 | %{[string]::Format("{0:X2}",$_)}
            [string]$GuidString3 = $GUIDSection3 | %{[string]::Format("{0:X2}",$_)}
            [string]$GuidString4 = $GUIDSection4 | %{[string]::Format("{0:X2}",$_)}
            [string]$GuidString5 = $GUIDSection5 | %{[string]::Format("{0:X2}",$_)}
           
            $GuidString1 = $GuidString1.replace(" ","")
            $GuidString2 = $GuidString2.replace(" ","")
            $GuidString3 = $GuidString3.replace(" ","")
            $GuidString4 = $GuidString4.replace(" ","")
            $GuidString5 = $GuidString5.replace(" ","")
           
            $InvocationGUID = "{0}-{1}-{2}-{3}-{4}" -f $GuidString1,$GuidString2,$GuidString3,$GuidString4,$GuidString5
           
            $name = Get-NameFromInvocationID $InvocationIDString
            if(!$DCInvocationID.$InvocationGUID)
            {
                if($name)
                {
                    $DCInvocationID.add($InvocationGUID,($InvocationIDString,$name))
                }
                else
                {
                    $DCInvocationID.add($InvocationGUID,$InvocationIDString)
                }
            }
            return $InvocationGUID
        }
        function Get-USNFromBytes{
            Param([byte[]]$USNArray)
            [system.Array]::Reverse($USNArray)
            [string]$USN = $USNArray | %{[string]::Format("{0:X2}",$_)}
            $usn = $usn.replace(" ","")
            $usn = [int]"0x$usn"
            $usn.PadRight(12)
        }
        function Get-DateFromBytes{
            Param([byte[]]$dateArray)
            [system.Array]::Reverse($dateArray)
            $temp = @(1..5)
            [system.Array]::Copy($dateArray,3,$temp,0,5)
            [string]$date = $temp | %{[string]::Format("{0:X2}",$_)}
            $date = $date.replace(" ","")
            $date = [int64]"0x$date"
            $date = $date + "0000000"
            [system.DateTime]::FromFileTime($date)
        }
        function Get-NameFromInvocationID{
            Param($InvocationID)
            [string]$config = ([adsi]"LDAP://RootDSE").ConfigurationNamingContext
            $de = new-Object System.DirectoryServices.DirectoryEntry("LDAP://$Config")
            $filter = "(&(invocationID=$InvocationID)(objectcategory=ntdsdsa))"
            $ds = new-Object System.DirectoryServices.DirectorySearcher($de,$filter)
            $hresult = $ds.findone()
            if($hresult.Path)
            {
                $ResultDE = $hresult.GetDirectoryEntry()
                $name = ($ResultDE.psbase.parent).DNSHostName
            }
            $name
        }
        function Decode-UpToDateVectorTable{
            Param([byte[]]$table)
           
            $guid = @(1..16)
            $USN = @(1..8)
            $Date = @(1..8)
            $UTDTable = @()
            $name = $null
           
            for($i=16;$i -lt $table.count;$i+=32)
            {
                [system.Array]::Copy($table,$i,$Guid,0,16)
                [system.Array]::Copy($table,$i+16,$USN,0,8)
                [system.Array]::Copy($table,$i+16+8,$Date,0,8)
               
                $GUIDString = Get-InvocationIDFromBytes $GUID
                $USNString  = Get-USNFromBytes $usn
                $dateString = Get-DateFromBytes $Date
                $UTDEntry = "" | Select-Object Name,USN,Date
                [system.Array]$dcname = $DCInvocationID.$GUIDString
                if($dcname[1])
                {
                    $UTDEntry.Name = $dcname[1]
                    $UTDEntry.USN =  $USNString
                    $UTDEntry.Date = $DateString
                    $UTDTable += $UTDEntry
                }
                else
                {
                    $UTDEntry.Name = $GUIDString
                    $UTDEntry.USN =  $USNString
                    $UTDEntry.Date = $DateString
                    $UTDTable += $UTDEntry
                }
            }  
            $UTDTable
        }
        $DCInvocationID = @{}
        $utdTables = @()
        $process = @()
    }
    Process{
        if($_)
        {
            $process += $_
        }
    }
    End{
        if($server){$process += $server}
        foreach($srv in $process)
        {
            $table = ([adsi]"LDAP://$srv").replUpToDateVector[0]
            if($process.count -gt 1)
            {
                $utdTableEntry = "" | Select-Object Name,UpToDatenessVector
                $utdTableEntry.Name = $srv
                $utdTableEntry.UpToDatenessVector = Decode-UpToDateVectorTable $table | Sort-Object Name -des
                $utdTables += $utdTableEntry
            }
            else
            {
                Decode-UpToDateVectorTable $table | Sort-Object Name -des
            }
        }
        if($utdTables){return $utdTables}
    }

    userAccountControl and “User cannot change password”

    Someone asked me a question about setting the “User cannot change password” check box in ADUC. They were creating the user account and setting PASSWD_CANT_CHANGE along with other settings (see my post about HERE about userAccountControl and values) and they couldn’t figure out why the check box wasn’t being applied.

    I thought about this, and my first impression was they had the wrong bit value. So, I posted the “correct” one. They came back and said “That didn’t work.”

    Hmmm, that’s curious. I turned to Dean and asked him if I was missing something obvious (I had a nagging feeling I had been here before) and he informed me that it has to be set with an “Extended Right” (control access right) via an ACE.

    DOH! Now the whole scenario I had THAT feeling about came back to me. I recall having this discussion with someone and providing them a Script that would set the ACE. I searched for the script and I couldn’t find it. This happens to me a lot so I decided a while ago… when I run across this again, BLOG IT!

    Technical Info:

    In the past, permissions on the ‘userAccountControl’ attribute could be edited oftentimes making the effective password policy moot, i.e. you could end up with accounts that don’t comply with the domain’s password policy.

    In Windows 2000, you can’t easily prevent this except by using third party front-end/provisioning tools to manage user objects. In Windows 2003 and later, you can use three newly-added extended rights (Control Access Rights) to prevent these bits from being edited even when the caller has permission to do so. The three (new) ‘Extended Rights’ are –

    • Update password not required bit [controls ‘password not required’ and maps to ACE in footnote below]
    • Enable per user reversible encryption [controls whether password is stored reversibly encrypted or not]
    • Unexpire password [controls ‘password never expires’]

    Each of these extended rights MUST be configured on the domain head and scoped as “This Object only” with ALLOW or DENY for the security principals you designate. By default, ‘Authenticated Users’ is granted an ALLOW ACE for each of the three extended rights. This doesn’t mean any old authenticated user can alter the password related bits in the ‘userAccountControl’ attribute; they still require the permission to modify ‘userAccountControl’.

    USAGE SCENARIO – create a single group representing ALL three extended rights (or perhaps ONE group for EACH extended right). Then ACL the group(s) accordingly on the domain head with a DENY ACE. Finally, place the account-administrator users and groups that have management permissions to user objects (i.e. they have write permissions to the ‘userAccountControl’ property) in the group(s) you just created thereby preventing those account-administrators from altering the password related bits on the ‘userAccountControl’ attribute resulting in an enforced password policy.

    IMPORTANT NOTE [Observed Behavior] – When viewing or changing a user’s ability to change their own password (User Cannot Change Password) through the GUI, it no longer appears to touch ‘userAccountControl’s bit 0x40 (64) — rather, it simply grant’s the ‘SELF’ security principal ‘ALLOW’ or ‘DENY’ to ‘Change Password’ — this can be easily verified by viewing the DACL.

    Links:
    Modifying User Cannot Change Password (LDAP Provider)

    So… Here it is (Set-UserCannotChangePassword.ps1)
    Parameters
    User: The sAMAccountName of the User
    CheckBox: If passed it checks the box
    Default: Remove Check box.

    Param($User = $(throw ‘$User is Required’,[switch]$CheckBox)
    Write-Host

    $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]"","(&(objectcategory=User)(sAMAccountName=$user))")
    $MyUser = $Searcher.FindOne().GetDirectoryEntry()

    if(!$?){" !! Failed to Get User !!";Return}

    if($CheckBox)
    {
        Write-Host " – Checking Box for User [$($MyUser.distinguishedName)]"
        $self = [System.Security.Principal.SecurityIdentifier]‘S-1-5-10’
        $ExtendedRight = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight
        $deny = [System.Security.AccessControl.AccessControlType]::Deny
        $selfDeny = new-object System.DirectoryServices.ActiveDirectoryAccessRule($self,$ExtendedRight,$deny,‘ab721a53-1e2f-11d0-9819-00aa0040529b’)
        $MyUser.psbase.get_ObjectSecurity().AddAccessRule($selfDeny)
        $MyUser.psbase.CommitChanges()
    }
    else
    {
        Write-Host " – Removing Check Box for User [$($MyUser.distinguishedName)]"
        $ACL = $MyUser.psbase.get_ObjectSecurity().GetAccessRules($true,$false, [System.Security.Principal.NTAccount])
        $ACEs = $ACL | ?{($_.ObjectType -eq ‘ab721a53-1e2f-11d0-9819-00aa0040529b’) -and ($_.AccessControlType -eq ‘Deny’)}
        foreach($ACE in $ACEs){if($ACE){[void]$MyUser.psbase.get_ObjectSecurity().RemoveAccessRule($ACE)}}
        $MyUser.psbase.CommitChanges()
    }

    Write-Host

    Using Get-Credential to Store Passwords “securely” in a file.

    Lee Holmes of PowerShell Cookbook fame has a post here Importing and Exporting Credentials in PowerShell. After fielding a few questions on EE regarding this post.. I wrote these two scripts to make it a little simpler.

    Set-myCredential: This will prompt you for credentials and store them in the file specified.

    #####################
    #Set-myCredential.ps1
    Param($File)
    $Credential = Get-Credential
    $credential.Password | ConvertFrom-SecureString | Set-Content $File
    #####################

    Get-myCredential: This will get you credentials from a file specified. It require you know the user name.

    #####################
    #Get-myCredential.ps1
    Param($User,$File)
    $password = Get-Content $File | ConvertTo-SecureString
    $credential = New-Object System.Management.Automation.PsCredential($user,$password)
    $credential
    #####################

    With these two script you can do something like this (using VMware Toolkit for example.)

    c:\scripts\Set-myCredential.ps1 c:\tools\mp.txt
    $creds = c:\scripts\Get-myCredential.ps1 MyUserName c:\tools\mp.txt
    Get-ViServer MyVirtualCenter -cred $creds

    What Are “Parameterized Properties?”

    I recently had someone ask me a question about property types. They kept referring to Parameterized Properties and while I knew they were confused, I only had vague understanding of this concept and I knew I definitely could not define it.

    I had tried google and all I came up with was this ETS Parameterized Properties. I shot this description over to a friend at MS and he said that was a bad definition and one didn’t really exist. I asked him to define it the best he can and here is the result. I would consider this source pretty authoritative 🙂

    Note: These are very common with COM Objects

    Parameterized Property

    Properties are members that are accessed like fields but are actually methods. There is a setter method for setting the value of the property and a getter method for getting the current value of the property. Normally, the setter method takes only one parameter (the value to set) and the getter does not take any.

    However, there are situations where additional parameters are needed. Consider the case where you want to use the property to get and set a member of a collection. To do this, the setter method requires 2 parameters—the index (location) in the collection and the value to set the member to; the getter method requires 1 parameter—the index to retrieve the value from. When a property requires additional parameters to operate, it is called a parameterized property or an indexer.

    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.

    Scripting/SysAdmin Meme

    Was called out by GPOGuy

    What was your first machine?
    Atari 800 or a TRS 80. Can’t remember.

    What was the first real script you wrote?
    Wow… The one that forced me to learn vbscript. I always knew I should, but when I got stuck in an environment with 2800 DCs in 2700+ sites. That was a lot of clicking… I had to script. I wrote a script that created the AD replication topology (scripted KCC if you like.)

    What scripting languages have you used?
    Used extensively: DOS batch, VBScript, PIC, PowerShell. dabbled with PHP, Perl, and Python.

    What was your first professional sysadmin gig?
    My first “computer” job was in high school working at a local technology repair firm. The first “sysadmin” job was for a small collection agency in Pcola Fl.

    If you knew then what you know now, would have started in IT?
    Absolutely. If I didnt like it I would leave. I think the most important thing about work is enjoying it.

    If there is one thing you learned along the way that you would tell new sysadmins, what would it be?
    Google, Google, GOOGLE! Think out of the box. Always ask why, the REAL problem isnt always clear.

    What’s the most fun you’ve ever had scripting?
    Decoding the UpToDateness Vector Table or my first 1000 line vbscript.

    Who am I calling out?
    No one… the buck stops here 🙂

    Working with LDAP Stats Control in Powershell

    What: The stats control is a LDAP control that you can pass that will tell the server to return its internal stats on a query.

    Why: The stats control is a great way to see what the Domain Controller does with your filter. Like what indexes it hits, how many entries it had to visit, how much time the DC spent, and entries visited. It is very useful in creating the most efficient LDAP Query possible.

    How: I Used System.DirectoryServices.Protocols.DirectoryControl to pass the LDAP control to the Server and I used System.DirectoryServices.Protocols.BERConverter along with the protocol spec here: LDAP_SERVER_GET_STATS_OID: 1.2.840.113556.1.4.970 to decode the Byte Array that was returned.

    Here is what is Returned:
    For 2000
    threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
    coreTime: The time in milliseconds which core logic in the DC spent processing the request.
    callTime: The overall time in milliseconds that the DC spent processing the request.
    searchSubOperations: The number of individual operations which the DC performed in processing the request.

    For 2003/2008
    threadCount: Number of threads that were processing LDAP requests on the DC at the time the search operation was performed.
    callTime: The overall time in milliseconds that the DC spent processing the request
    entriesReturned: The number of objects returned in the search result.
    entriesVisited: The number of objects that the DC considered for inclusion in the search result.
    filter: String which represents the optimized form of the search filter used by the DC to perform a search. This very well may be different than the filter that was passed.
    index: String which indicates which database indexes were used by the DC to perform the search.

    For 2008 Only
    pagesReferenced: The number of database pages referenced by the DC in processing the search.
    pagesRead: The number of database pages read from disk.
    pagesPreread: The number of database pages preread from disk by the DC in processing the search.
    pagesDirtied: The number of clean database pages modified by the DC in processing the search.
    pagesRedirtied: The number of previously modified database pages that were modified by the DC in processing the search.
    logRecordCount: The number of database log records generated by the DC in processing the search.
    logRecordBytes: The size in bytes of database log records generated by the DC in processing the search.

    Note:
    – Must have SE_DEBUG_PRIVILEGE
    – I did NOT implement SO_EXTENDED_FMT flag.
    – I did NOT test 2000.
    – The functions that decodes Byte Array actually return objects, but for this test I just outputed the test to mimic ADFind.exe
    – Special thanks to Robin Caron, joe Richards, and Dmitri Gavrilov for help with the decoding.
    – Here is GREAT Doc on the Controls (and everything else AD) [MS-ADTS]: Active Directory Technical Specification

    Code:

    Param(
            $filter = "(objectclass=*)",
            $base,
            $Server,
            [int]$pageSize = 1000,
            [string[]]$props = @("1.1"),
            [switch]$StatsOnly,
            [switch]$Verbose
        )
    function CreateStatsObject2008{
        Param($StatsArray)
        $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaiaiiiiiiiiiiiiii}",$StatsArray) # Win2008
        $myStatsObject = New-Object System.Object
        $myStatsObject | Add-Member -Name "ThreadCount"     -Value $DecodedArray[1]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "CallTime"        -Value $DecodedArray[3]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "EntriesVisited"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "Filter"          -Value $DecodedArray[9]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "Index"           -Value $DecodedArray[11] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesReferenced" -Value $DecodedArray[13] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesRead"       -Value $DecodedArray[15] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesPreread"    -Value $DecodedArray[17] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesDirtied"    -Value $DecodedArray[19] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "PagesRedirtied"  -Value $DecodedArray[21] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "LogRecordCount"  -Value $DecodedArray[23] -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "LogRecordBytes"  -Value $DecodedArray[25] -MemberType "NoteProperty"
        $myStatsObject
    }
    function CreateStatsObject2003{
        Param($StatsArray)
        $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiiiiaia}",$StatsArray) # Win2003
        $myStatsObject = New-Object System.Object
        $myStatsObject | Add-Member -Name "ThreadCount"     -Value $DecodedArray[1]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "CallTime"        -Value $DecodedArray[3]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "EntriesReturned" -Value $DecodedArray[5]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "EntriesVisited"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "Filter"          -Value $DecodedArray[9]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "Index"           -Value $DecodedArray[11] -MemberType "NoteProperty"
        $myStatsObject
    }
    function CreateStatsObject2000{
        Param($StatsArray)
        $DecodedArray = [System.DirectoryServices.Protocols.BerConverter]::Decode("{iiiiiiii}",$StatsArray) # Win2000
        $myStatsObject = New-Object System.Object
        $myStatsObject | Add-Member -Name "ThreadCount"          -Value $DecodedArray[1]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "CoreTime"             -Value $DecodedArray[3]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "CallTime"             -Value $DecodedArray[5]  -MemberType "NoteProperty"
        $myStatsObject | Add-Member -Name "searchSubOperations"  -Value $DecodedArray[7]  -MemberType "NoteProperty"
        $myStatsObject
    }

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

    Write-Verbose " – Loading System.DirectoryServices.Protocols"
    [VOID][System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols")
       
    [int]$pageCount = 0
    [int]$objcount = 0

    if(!$Server)
    {
        $rootDSE = [ADSI]"LDAP://rootDSE"
        $Server = $rootDSE.dnsHostName
        if(!$base){$base = $rootDSE.defaultNamingContext}
        switch ($rootDSE.domainControllerFunctionality)
        {
            0 {$expression = ‘CreateStatsObject2000 $stats’}
            2 {$expression = ‘CreateStatsObject2003 $stats’}
            3 {$expression = ‘CreateStatsObject2008 $stats’}
        }
    }

    Write-Verbose " – Using Server:  [$Server]"
    Write-Verbose " – Using Base:    [$base]"
    Write-Verbose " – Using Filter:  [$filter]"
    Write-Verbose " – Page Size:     [$PageSize]"
    Write-Verbose " – Returning:     [$props]"
    Write-Verbose " – CSV:           [$csv]"
    Write-Verbose " – NoHeaders:     [$noHeader]"
    Write-Verbose " – Count:         [$Count]"
    Write-Verbose " – StatsOnly:     [$StatsOnly]"
    Write-Verbose " – Expression:    [$expression]"

    Write-Verbose " – Creating LDAP connection Object"
    $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($Server)  
    $Subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"

    Write-Verbose " + Creating SearchRequest Object"
    $SearchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($base,$filter,$Subtree,$props)

    Write-Verbose "   – Creating System.DirectoryServices.Protocols.PageResultRequestControl Object"
    $PagedRequest  = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)

    Write-Verbose "   – Creating System.DirectoryServices.Protocols.SearchOptionsControl Object"
    $SearchOptions = New-Object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)

    Write-Verbose "   – Creating System.DirectoryServices.Protocols.DirectoryControl Control for OID: [1.2.840.113556.1.4.970]"
    $oid = "1.2.840.113556.1.4.970"
    $StatsControl = New-Object System.DirectoryServices.Protocols.DirectoryControl($oid,$null,$false,$true)

    Write-Verbose "   – Adding Controls"
    [void]$SearchRequest.Controls.add($pagedRequest)
    [void]$SearchRequest.Controls.Add($searchOptions)
    [void]$SearchRequest.Controls.Add($StatsControl)

    $start = Get-Date
    while ($True)
    {
        # Increment the pageCount by 1
        $pageCount++

        # Cast the directory response into a SearchResponse object
        Write-Verbose " – Cast the directory response into a SearchResponse object"
        $searchResponse = $connection.SendRequest($searchRequest)

        # Display the retrieved page number and the number of directory entries in the retrieved page
        Write-Verbose (" – Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count)

        Write-Verbose " – Returning Stats for Page:$PageCount"
        $stats = $searchResponse.Controls[0].GetValue()
        $ResultStats = invoke-Expression $expression
        if($pageCount -eq 1)
        {
            $StatsFilter = $ResultStats.Filter
            $StatsIndex = $ResultStats.Index
            Write-Verbose "   + Setting Filter to [$StatsFilter]"
            Write-Verbose "   + Setting Index  to [$StatsIndex]"
        }
       
        # If Cookie Length is 0, there are no more pages to request"
        if ($searchResponse.Controls[1].Cookie.Length -eq 0)
        {
            if($count){$objcount}
            "`nStatistics"
            "================================="
            "Elapsed Time: {0} (ms)" -f ((Get-Date).Subtract($start).TotalMilliseconds)
            "Returned {0} entries of {1} visited – ({2})`n" -f $ResultStats.EntriesReturned,$ResultStats.EntriesVisited,($ResultStats.EntriesReturned/$ResultStats.EntriesVisited).ToString(‘p’)
            "Used Filter:"
            "- {0}`n" -f $StatsFilter
            "Used Indices:"
            "- {0}`n" -f $StatsIndex
            break
        }

        # Set the cookie of the pageRequest equal to the cookie of the pageResponse to request the next
        # page of data in the send request and cast the directory control into a PageResultResponseControl object
        Write-Verbose " – Setting Cookie on SearchResponse to the PageReQuest"
        $pagedRequest.Cookie = $searchResponse.Controls[1].Cookie
    }

    Phase 3 of S.DS.P “CSV Output” (as much as 21 sec faster!)

    In the next phase I wanted to actually get some data back and display it. Because I just wanted to test performance I kept the test as close as possible. ADFind has the built in ability to output a csv file so I mimicked his output. I also added two properties to return name and sAMAccountName. For those interested, my next step will be to output objects instead of text (really the whole point.)

    My expectation for this test was that ADFind would pull ahead due to the processing of each record, but I was pleasantly surprised when the script was actually faster in all environments except 700k Environment. I will let joe explain why his is slower.

    Testing
    – I ran each 10x in a Row (as joe Suggested) in its own environement
    – ADFind I tested using CMD.exe and used ptime.exe for time measurements.
    – Get-DSPObject I used Measure-Command and outputed only TotalSeconds

    UPDATED!: I had several people suggest that I should have a consolidated view of the results. So here it is. I took the Average from each test and put into and Excel spreadsheet and then plotted on a chart.

    Here is the screenshot (lower is better)
    DSP vs ADFind Chart

    400k objects in a Prod Quality VM running Win2008 RTM 64bit (local)
    Average: ADFind = 90.45
    Average: DSP = 68.79
    Winner: DSP 21.66 secs faster

    Get-DSPObject -prop “name”,”sAMAccountName” -csv
    Execution time: 70.455s
    Execution time: 69.551s
    Execution time: 68.466s
    Execution time: 68.085s
    Execution time: 68.611s
    Execution time: 68.179s
    Execution time: 68.837s
    Execution time: 68.670s
    Execution time: 69.105s
    Execution time: 67.994s
    adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
    Execution time: 100.538 s
    Execution time: 109.511 s
    Execution time: 92.499 s
    Execution time: 96.063 s
    Execution time: 91.601 s
    Execution time: 80.693 s
    Execution time: 81.551 s
    Execution time: 81.044 s
    Execution time: 90.945 s
    Execution time: 80.143 s

    400k objects in a Prod Quality VM running Win2008 RTM 64bit (remote)
    Average: ADFind = 77.18
    Average: DSP = 64.23
    Winner: DSP 13 secs faster

    Get-DSPObject -prop “name”,”sAMAccountName” -csv
    Execution time: 67.065 s
    Execution time: 63.871 s
    Execution time: 63.330 s
    Execution time: 63.027 s
    Execution time: 62.630 s
    Execution time: 64.692 s
    Execution time: 64.451 s
    Execution time: 64.450 s
    Execution time: 64.594 s
    Execution time: 64.219 s
    adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
    Execution time: 77.280 s
    Execution time: 77.616 s
    Execution time: 77.304 s
    Execution time: 76.899 s
    Execution time: 77.426 s
    Execution time: 76.773 s
    Execution time: 76.699 s
    Execution time: 77.445 s
    Execution time: 77.463 s
    Execution time: 76.912 s

    700k objects on a Physical machine Win2k3 x86
    Average: ADFind = 101.383
    Average: DSP = 109.600
    Winner: ADFind 8.21 secs faster

    Get-DSPObject -prop “name”,”sAMAccountName” -csv
    Execution time: 111.088s
    Execution time: 109.962s
    Execution time: 110.112s
    Execution time: 110.832s
    Execution time: 110.167s
    Execution time: 109.853s
    Execution time: 109.757s
    Execution time: 110.387s
    Execution time: 109.070s
    Execution time: 109.065s
    adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
    Execution time: 101.571 s
    Execution time: 101.313 s
    Execution time: 101.386 s
    Execution time: 101.593 s
    Execution time: 101.539 s
    Execution time: 100.917 s
    Execution time: 101.020 s
    Execution time: 101.286 s
    Execution time: 101.544 s
    Execution time: 101.667 s

    300k Objects on Physical Win2k3 x86 (Remote)
    Average: ADFind = 49.90
    Average: DSP = 44.44
    Winner: DSP 5 secs Faster

    Get-DSPObject -prop “name”,”sAMAccountName” -csv
    Execution time: 44.782 s
    Execution time: 44.316 s
    Execution time: 44.473 s
    Execution time: 44.322 s
    Execution time: 44.380 s
    Execution time: 44.440 s
    Execution time: 44.394 s
    Execution time: 44.544 s
    Execution time: 44.428 s
    Execution time: 44.247 s
    adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
    Execution time: 51.983 s
    Execution time: 51.937 s
    Execution time: 51.810 s
    Execution time: 49.142 s
    Execution time: 48.913 s
    Execution time: 49.073 s
    Execution time: 48.765 s
    Execution time: 49.125 s
    Execution time: 49.110 s
    Execution time: 49.105 s

    200k Objects on my Server at Home (Win2k8 x64)
    Average: ADFind = 35.50
    Average: DSP = 31.74
    Winner: DSP 4 secs Faster

    Get-DSPObject -prop “name”,”sAMAccountName” -csv
    Execution time: 31.887 s
    Execution time: 31.888 s
    Execution time: 31.044 s
    Execution time: 30.746 s
    Execution time: 31.549 s
    Execution time: 31.601 s
    Execution time: 31.168 s
    Execution time: 31.528 s
    Execution time: 31.481 s
    Execution time: 31.269 s
    adfind -b “your dn here” -f “(objectclass=user)” name samaccountname -csv
    Execution time: 37.305 s
    Execution time: 34.815 s
    Execution time: 34.275 s
    Execution time: 34.193 s
    Execution time: 39.116 s
    Execution time: 34.634 s
    Execution time: 39.265 s
    Execution time: 33.686 s
    Execution time: 33.793 s
    Execution time: 33.917 s

    Here is the script I used for the DSP Tests

    function Get-DSPObject {
        Param(
                $filter = "(objectclass=user)",
                $base = ([ADSI]"").distinguishedName,
                $Server,
                [int]$pageSize = 1000,
                [string[]]$props = @("1.1"),
                [switch]$noHeader,
                [switch]$csv,
                [switch]$count
            )
           
        [VOID][System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols")
           
        [int]$pageCount = 0
        [int]$objcount = 0
       
        if(!$server){$server = ([ADSI]"").distinguishedName -replace  ",","." -replace "dc=","" }
       
        $connection = New-Object System.DirectoryServices.Protocols.LdapConnection($Server)  
        $subtree = [System.DirectoryServices.Protocols.SearchScope]"Subtree"
       
        $searchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest($base,$filter,$subtree,$props)  
        $pagedRequest = New-Object System.DirectoryServices.Protocols.PageResultRequestControl($pageSize)
        $searchOptions = New-Object System.DirectoryServices.Protocols.SearchOptionsControl([System.DirectoryServices.Protocols.SearchOption]::DomainScope)
        $searchRequest.Controls.add($pagedRequest) | out-null
        $searchRequest.Controls.Add($searchOptions) | out-null
       
        # Output Prep
        if($props -notcontains "1.1")
        {
            $MyProps = @()
            foreach($prop in $props){$MyProps += $prop.ToLower()}
            if($csv)
            {
                if(!$noHeader)
                {
                    $header = "distinguishedName"
                    foreach($prop in $props){$header += ",$prop"}
                    $header
                }
            }
            else
            {
                $MyUserObj = New-Object System.Object
                $MyUserObj | Add-Member -name distinguishedName -MemberType "NoteProperty" -value $null
                foreach($prop in $props){$MyUserObj | Add-Member -name $prop -MemberType "NoteProperty" -value $null}
            }
        }
       
        # Process Pages
        while ($True)
        {
            # Increment the pageCount by 1
            $pageCount++
       
            # Cast the directory response into a SearchResponse object
            $searchResponse = $connection.SendRequest($searchRequest)
       
            # Display the retrieved page number and the number of directory entries in the retrieved page
            # "Page:{0} Contains {1} response entries" -f $pageCount,$searchResponse.entries.count
            $objcount += ($searchResponse.entries).count
           
            # Display the entries within this page
            if(!$count)
            {
                if($props -notcontains "1.1")
                {
                    foreach($entry in $searchResponse.Entries)
                    {
                        if($csv)
                        {
                            $results = "`"{0}`"" -f $entry.distinguishedName
                            foreach($prop in $MyProps)
                            {
                                $results += ",`"{0}`"" -f ($entry.Attributes[$prop][0])
                            }
                            $results
                        }
                        else
                        {
                            $MyUserObj.distinguishedName = $entry.distinguishedName
                            foreach($prop in $MyProps)
                            {
                                $MyUserObj."$prop" = $null
                                $MyUserObj."$prop" = $entry.Attributes[$prop][0]
                            }
                            $MyUserObj
                        }
                    }
                }
                else{$searchResponse.Entries | select distinguishedName}
            }
           
            # if this is true, there are no more pages to request
            if ($searchResponse.Controls[0].Cookie.Length -eq 0){if($count){$objcount};break}
       
            # Set the cookie of the pageRequest equal to the cookie of the pageResponse to request the next
            # page of data in the send request and cast the directory control into a PageResultResponseControl object
            $pagedRequest.Cookie = $searchResponse.Controls[0].Cookie
        }
    }

    foreach($i in (1..10))
    {
        $time = Measure-Command { Get-DSPObject -prop "name","sAMAccountName" -csv }
        "Execution time: {0:n3} s" -f $time.TotalSeconds
    }

    « Prev - Next »