Powershell and FSMO Roles

November 4th, 2009 Mark A. Weaver No comments
Rating 3.50 out of 5

Okay, this will be a quick and dirty post due to lack of time right now.
This one is kind of a tip, rather than a full-blown script or topic. Basically I was looking to grab which system was the PDC Emulator for my current domain (or NOT my current domain) and so I did some google-ing and finally ended up with these little functions.

All I need to do is pass in the DomainName and it spits out the info. For the FSMO roles, it will return an object and for the DomainMode, just the text is returned.

Hopefully you will find them useful.
That’s it for now…
Happy Scripting..
– Mark

Function get-PDCERole ($DomainName)
  {
   ## Return the PDC Emulator Role Owner for the specified Domain
   $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain", $DomainName)
   $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext)
   $PDCE = $Domain.PDCRoleOwner
 
   Return $PDCE  
  }
 
Function get-RIDMasterRole ($DomainName)
  {
   ## Return the RID Master Role Owner for the specified Domain
   $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain", $DomainName)
   $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext)
   $RIDMaster = $Domain.RIDRoleOwner
 
   Return $RIDMaster
  } 
 
Function Get-InfMasterRole ($DomainName)
  {
   ## Return the Infrastucture Master role owner for the specified Domain
   $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain", $DomainName)
   $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext)
   $InfMaster = $Domain.InfrastructureRoleOwner
 
   Return $InfMaster
 }
 
Function Get-DomainMode ($DomainName)
  {
   ## Return the Domain Mode for the specified Domain
   $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain", $DomainName)
   $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext)
   $DomainMode = $Domain.DomainMode
 
   Return $DomainMode
 
  }

Powershell and Unknown User SIDs

October 9th, 2009 Mark A. Weaver No comments
Rating 3.00 out of 5

Once again, my apologies for my lack of posting..

Anyway…

An interesting thing came up at work the other day when one of my fellow administrators asked me if I could resolve an unknown SID he was seeing in some logs to see what the heck it belonged to.

Since I had been telling him that Powershell could do ANYTHING (slight exaggeration, I know)… that it should be able to do this.

Well, it certainly is an interesting notion.

I know that I have run into this in the past where you have file system ACLs set and there are a bunch of SIDs sitting in there that nobody seems to know who they belong to.

While it isn’t THAT important to resolve them since the user account is most likely no longer around, it IS an interesting thought exercise.

After perusing the web looking for others who have done something similar, I feel I had enough to throw something together..

Basically when an Active Directory object (like a user) is “deleted”, it is really just Tombstoned for a period of time and is moved to the hidden container “Deleted Objects” and then removed after like 90 days or so.

Here is my solution to “finding” those objects.

First you will have to know what Domain you want to look at for this object AND you have know the SID you are looking for.

function Resolve-DeletedUserSID($Domain, $UserSID)
{
	## This is kind of a mashup of a few different scripts I found online in some forums.
	## Unfortunately I don't remember who did them.  If it was you, point me to your post and I will
	## give you the credit for your piece.
	## 
	## Returns User information for deleted account with the specified SID and User Domain
 
	$DomainRoot = "LDAP://" + $Domain.trim()
	$DomainDN = ([adsi] ( $DomainRoot )).DistinguishedName
	$adspath = "LDAP://" + $DomainDN
	$root = [system.directoryservices.Directoryentry] $Adspath
	$root.psbase.AuthenticationType = [system.directoryservices.authenticationtypes]::Fastbind
	## We will be looking in the "Deleted Objects" container which is normally hidden, etc.
	## You will need to execute this with an account that has DomainAdmin rights to the domain you are
	## querying.
	$root.psbase.path = "LDAP://cn=Deleted Objects," + $DomainDN
	$search = [system.directoryservices.directorysearcher] $root
	$search.filter = "(&(isDeleted=TRUE)(!(objectClass=computer))(objectclass=user))"
	$search.Tombstone = $true
 
	# If you have more than 1000 users, you must NOT define SizeLimit (we haven't)
	# and PageSize must be less than the default value (of 1000). 
	# I found this a bit strange...but as long as we understand it, I guess it is okay
	$Search.PageSize = 500
 
	# Only look in the top level of the Deleted Objects container.
	$search.SearchScope = [system.directoryservices.searchscope]::OneLevel
	$result = $search.FindAll()
 
	# If the SID isn't found, you will get nothing returned.
	$result | Select-Object @{ Name = "Name" ; Expression = { $_.Properties.Item("Name")[0].split("`n")[0] } }, `
	@{ Name = "SAMAccountName" ; Expression = { $_.Properties.Item("SAMAccountName") } }, `
	@{ Name = "SID" ; Expression = { New-Object System.Security.Principal.SecurityIdentifier($_.Properties.Item("ObjectSID")[0], 0) } }, `
	@{ Name = "WhenChanged" ; Expression = { $_.Properties.Item("WhenChanged") } } `
	| where-Object { $_.SID -ieq $UserSid.Trim() }
}

As always…

Happy Scripting!!!! and let me know if you have questions or problems.

– Mark

Powershell – Recursive Group Membership

August 16th, 2009 Mark A. Weaver 7 comments
Rating 3.50 out of 5

Well, I am back for yet another Powershell script. This is one that I found pretty useful actually.
As one of the people really pushing automation in my group at work, I was tasked with getting a list of all users in the “Domain Admins” group for all domains in our Active Directory forest.

One of the challenges in doing this is that you may have a bunch of nested groups and we needed to dump users from all nested groups, etc.  I do realize that there are probably tools and what-not that would this for me, but what fun is that and why spend the bucks if you can script it out.  I like this approach, too, because I can force the output to be whatever I want and in whichever format is best for what I am trying to accomplish.

From my days in college as a computer science kinda guy, I figured we could use recursion to help walk us through all nested groups.

So for those of you unfamilar with this idea of recursion, I will summarize:
It is basically a function that calls itself until a certain condition is met. At this point the function exits. I know all you CS types may take exception to such a simplified definition, so please google for it or hit up Wikipedia for more detailed info on recursion.

How can this possible help us in our quest to enumeration group memberships?  Well let’s break it down a little.

  1. I start with a group I care about. Let’s say it is “Domain Admins” for domain “office1.contoso.com”.
  2. I have a function (“get-groupmembers”) that I use to enumerate the members of this group and do something with them (output, write to file, etc)
  3. As I am enumerating them, I find a member that is of type “group” called something like “Corp Admins”
  4. I now call my function (“get-groupmembers”) with this nested group (“Corp Admins”) to enumerate the members
  5. As I am enumerating them, I find ANOTHER group called “Help Desk On-Call” inside of “Corp Admins”
  6. I can now call my funciton (“get-groupmembers”) ANOTHER time and keep going until I only have users and have walked all of the nested groups

I know this may sound a little weird “that I am calling myself” over and over again, but it is actually pretty efficient.

Let’s jump into some code now.

For starters, I need to have some functions that take a Fully Qualified Domain Name (for an Active Directory Domain) and convert it into an LDAP-ish format.  For example, I needed “office1.contoso.com” to be transformed into “DC=office1,DC=contoso,DC=com”.  I know this isn’t rocket-surgery, but I just threw some stuff together for it.

function Convert-DNStoDN ($DNSName)
{
   #  Create an array of each item in the string separated by "."
   $DNSArray = $DNSName.Split(".")
  # Let's go through our new array and do something with each item
   for ($x = 0; $x -lt $DNSArray.Length ; $x++)
      {
        #I don't want a comma after my last item, so check to see if I am on my last one and set
        # $Separator equal to nothing.
        # Remember that we need to go to Length-1 because arrays are "0 based indexes"
         if ($x -eq ($DNSArray.Length - 1)){$Separator = ""}else{$Separator =","}
         [string]$DN += "DC=" + $DNSArray[$x] + $Separator
      }
   return $DN
}

We will also need to be able to split the FQDN of the DOMAIN out from the DN of a group or user. So, I have something like “CN=Me,OU=User1,DC=office1,DC=contoso,DC=com” and want to get the FQDN of this domain. For this example this would output “office1.contoso.com”.

function Convert-DNtoDNS ($DN)
{
    $DNArray = $DN.Split(",")
     # Let's go through our new array and do something with each item
   for ($x = 0; $x -lt $DNArray.Length ; $x++)
      {
        #I don't want a period after my last item, so check to see if I am on my last one and set
        # $Separator equal to nothing.
        # Remember that we need to go to Length-1 because arrays are "0 based indexes"
         if ($x -eq ($DNArray.Length - 1)){$Separator = ""}else{$Separator ="."}
        # Now we have to see if we look like "DC=". If it does, we will
        # start to construct our DNS name.
        if ($DNArray[$x].Split("=")[0] -ilike "DC")
          {
               # Let's grab the "contoso" side of the "DC=contoso"
              [string]$DNS += $DNArray[$x].Split("=")[1] + $Separator
           }
      }
   return $DNS
}

Now that we have those little “cameo” functions, we will move on to the more of the meat-and-potatoes of the script.
The next function will be to actually enumerate a group in Active Directory without using the Quest Tools for Active Directory (if you don’t have those yet, you need to them).

We are actually going to use some .NET calls to get the directory objects. My colleague Mike Hays actually did a lot of this part of the code.

function get-groupmember($domain, $groupName)
{
   # I have passed in the FQDN and Groupname I am interested in
   # I just need to convert my FQDN into an LDAP style name using my previous function
   $DN = convert-DNStoDN($Domain)
   $domainLDAPUrl = "LDAP://" + $DN
   # Setup my directory connection using .NET call
   $ent = [System.DirectoryServices.DirectoryEntry] ( $domainLDAPUrl )
 
   # Define my "searcher" object to query the directory
   $srch = [System.DirectoryServices.DirectorySearcher] ( $ent )
 
   # Setup my search criteria.. looking for all Groups with CN=GroupName
   $groupNameFilter = "(&(objectClass=group)(CN=" + $groupName + "))"
   $srch.Filter = $groupNameFilter
 
   # Now go execute my query to and put the results in $coll
   $coll = [System.DirectoryServices.SearchResultCollection]      $srch.FindAll()
 
   foreach ($rs in $coll)
     {
       # Now get a collection of properties for that object
       $resultPropColl = [System.DirectoryServices.ResultPropertyCollection] $rs.Properties
 
       # Cycle through all group members
       foreach ($memberColl in $resultPropColl["member"])
         {
           # Build my membership array
           [array]$gpMemberEntry += [System.DirectoryServices.DirectoryEntry] ( "LDAP://" + $memberColl )
          }
   }
  # Send back my group members.
  return $gpMemberEntry
}

Okay.. now that we have THAT setup let’s talk about the next bits of code.
This is where we will have our recursive function “Get-AllMembers”. In it, you will a call to itself. One of the biggest concerns is that you can end up in an unending or infinite cycle. I don’t really do any checking in this little scripty-do-dad, so that may be something for later.

function get-allmembers($objectName, $OF, $GN)
{
    # Split out my domain name  (should be FQDN) and the group name
    $domainName = $objectname.split("\")[0]
    $ObjectName = $objectname.split("\")[1]
 
    $members = get-groupmember "$DomainName" "$ObjectName"
    if ($members -ne $NULL)
     {
        foreach ($member in $members)
         {
            #  Grab the domain DNS name out of the object DN
            $ObjDomain = convert-DNtoDNS $Member.DistinguishedName
            if ($member.objectclass -contains "group")
              {
                 #If my group member is, itself, a group We get to do some recursion
                  $out = $objDomain + "\" + $member.name
                  Write-Host $out
                  # Call myself with the nested group name
                  get-allmembers -ObjectName $out -OF $of -GN $GN
               }
            else
               {
                  # If I get back a user, then see if the user is disabled or not
                  $userAndDomain = $objDomain + "\" + $member.name
                  # The  UserAccountControl property contains several "flags"
                  # that we can interrogate.  By doing a Binary AND we are seeing if the 2nd flag is set.
                  [bool]$accountIsDisabled = [int]$member.userAccountControl.ToSTring() -band 2
 
                  # Setup our output (I am choosing to construct a comma-delimited type of output
                  $OutText = "'" + $GN + "','" + $objDomain + "','" + $Member.Samaccountname + "','" + $Member.displayName + "','" + $member.distinguishedName + "','" + $objectname + "','" + $accountIsDisabled + "'"
 
                  Out-File -FilePath $OF -inputobject $Outtext -append -Encoding "ASCII"
 
                  Write-Host $OutText
                }
         }
    }
    else
     {
        Write-Host "No Members or no Group:" $ObjectName "in Domain:" $DomainName -Foreground RED
     }
}
 
###########################
## Main
###########################
$GroupName = "Domain Admins"
$Today = Get-Date -format "yyyyMMddhh"
$OutputFolder = "C:\Temp\"
 
if ((Test-Path $outputFolder) -eq $False)
 {
    New-Item -Path $OutputFolder -Type Directory > $NULL
  }
 
# Grab my forest info
$forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
 
#Setup my output file
$OutHeader = "'Group','UserDomain','SAMAccount','DisplayName','DN','MemberofGroup','IsDisabled'"
 
# Define output file name
$of = $OutputFolder + $Today +"_"+ $GroupName + "-AuditReport.txt"
 
Out-File -FilePath $OF -inputobject $OutHeader -Encoding "ASCII"
 
# Cycle through all the child domains in the forest root to query for Group.
foreach ($domain in $forest.Domains)
 {
    $FullGroupName = $domain.name + "\" + $GroupName
    get-allmembers -ObjectName $FullGroupName -OF $of -GN $FullGroupName
}

Just take all of the script blocks from above and paste them into your script. I am trying to keep these posts a bit shorter, so you may see upcoming posts broken out into parts.

Well, I think I am done here with this one. Please let me know if you have questions, concerns, or comments.

Please keep in mind that this script will attempt to enumerate the Group in ALL child domains in your current AD Forest. If you have a large Forest with lots of child domains…..this could take a while.

I will be happy to help out with requested changes if they seem like they would be beneficial overall, but I am also a STRONG advocate of doing-it-yourself.
Every bit of Powershell and scripting I have learned by grabbing it and going with it.

Anyway, as always…thanks for stopping by and happy scripting!!!

– Mark

Binary Search and Powershell

July 22nd, 2009 Mark A. Weaver No comments
Rating 3.00 out of 5

Time for another Powershell Post.
So, what is a binary search and why should you care about it?

Well, it provides us a mechanism to very quickly search through an ordered list of “things” (like event log entries). It is especially useful when looking through LARGE amounts of data.

I know, I know.. There are all sorts of ways to look through/filter/search event logs and everyone has probably written about this already, but I guess I feel compelled to put in my 2 cents.

We ran into the need to be able to pull event log entries from a specific period of time on several servers.
There aren’t really any “native” Powershell methods to query Eventlogs from REMOTE servers (unless you are on CTP and hitting Vista/2K8 systems).

So a little info about a binary search:

Lets say we have a list of things as follows:

Index

Name

DOB

0

Adam 01/20/80

1

Cheryl 09/16/77

2

Frank 04/01/82

3

Ivan 10/26/70

4

Suzy 04/10/90

5

Tim 11/21/62

6

Wendy 02/02/71

And we are wanting to find which index “Suzy” is in the list.

One way would be to start at the top of the list and see if the Name matches. On a short list this may make sense since it is really only 5 compares before we find her.

But if I was looking for “Suzy” in a list the size of a phone book this can take a long time and a lot of horsepower to find her and may take 300,000 compares or more.

A binary search algorithm helps us by breaking the list down by using something we already know about the list: that it is sorted by name.

With a single compare, though, we can cut the list of viable options in half.

All we need to do is take the index of our smallest value [0] and we add it to the index of our largest element [6] and divide by 2, we get the index of our middle element [3]. Now lets compare what we are looking for (“Suzy”) to the value of element [3].

LowerBound Index = 0
UpperBound Index = 6
Mid Index = (6+0)\2 = 3

Lets take a look at this:

Index

Name

DOB

0

Adam 01/20/80

1

Cheryl 09/16/77

2

Frank 04/01/82

3

Ivan 10/26/70

4

Suzy 04/10/90

5

Tim 11/21/62

6

Wendy 02/02/71

Obviously “Suzy” is greater than our value at index [3] “Ivan”. Since we know this, we can throw out all indexes that are [3] and lower.

Our list of viable options now becomes this:

4

Suzy 04/10/90

5

Tim 11/21/62

6

Wendy 02/02/71

Let’s repeat this exercise on our new list:

Lowest index = 4

Largest index = 6

Middle index = (6+4) /2 = 5

 

4

Suzy 04/10/90

5

Tim 11/21/62

6

Wendy 02/02/71

 So now we compare what we are looking for “Suzy” to our element [5] (“Tim”). Well, “Suzy is less than “Tim”, so that means we can throw out indexes that are [5] and higher.

 

4

Suzy 04/10/90

 Let’s go one more time…. oh wait.. .there is only one thing left….it’s “Suzy”. We have found her.

 

There ARE .NET methods (of [System.Array]) that enable us to do binary searches of Arrays for Strings. The only problem is that it will be looking for EXACT matches for the string.

 

This poses a problem for us if we are looking for something that is a little less exact.

 

Mark’s Modified Binary Search

Okay, leave it to me to mess with a good thing.

My problem is that I want to grab all events from the System event log that occurred between 8pm and 9pm last night.

This is a little more “fuzzy” than exact, so let me show you what I did….heh

 I know you all are getting a little antsy for some code, but we will get there soon.. Stay with me.

function Get-DatedLogEntries([string]$ServerName, [string]$EventLogName, [datetime]$OldestTime, [datetime]$NewestTime)
{
 
	#Grabbing my Eventlog Entries
	$EventLog = New-Object System.Diagnostics.EventLog($EventlogName)
	$EventLog.MachineName = $ServerName
	$Entries = $Eventlog.Entries

There are some cmdlets designed to return eventlog entries, but they are only really effective if you are querying the local server or if you are leveraging the “new” Powershell Remoting, but you have to be running against Vista or Server 2008.

Fortunately, there is a way to grab remote event log entries via .NET (as shown in the code above)

When done, $Entries will contain all of the events in the logfile sorted by Time.

Next, we will setup our “bound”ing values for our array of entries and the times we are looking for.

#Defining my starting boundaries of my array
$Ubound = $Entries.count - 1
$Lbound = 0
$Mid = 0
 
#Setting up my dates
$StartTime = $OldestTime
$EndTime = $NewestTime

Now we are ready for the real work. Because I want to grab a range of events, I will run through the binary search two times. The first time will tell me what the UpperBound of my list is, and the second will tell me what the LowerBound of my list is as array indexes.

Before we start checking anything, we can make sure our search isn’t for naught.

Many systems have event logs that can roll pretty quickly, so we can just compare the oldest event to our start time. If the oldest event is newer than my start time, then my logs have rolled and I won’t have any data.

if ($Entries[0].TimeGenerated -lt $StartTime)
{
while (($Ubound - $Lbound) -gt 1)
{
   $Mid = [int] ( ($Ubound + $Lbound) / 2 ) #Calculate my midpoint
  #Compare my midpoint to my StartTime
  if ($Entries[$Mid].TimeGenerated -lt $StartTime)
     {
  #If my midpoint is less than my Start time, then throw out all events
  #below and including my Midpoint
        $LBound = $Mid + 1
     }
   elseif ($Entries[$Mid].TimeGenerated -gt $StartTime)
     {
     #If my midpoint is greater than my Start time, then throw out all events
      #above and including my Midpoint
      $Ubound = $Mid-1
     }
    else
     {
     #If my midpoint is equal to my Start time, then I got lucky and found my time.
     #I just realized that I may need to do something else with this. May tackle that
     # later though...
      $Ubound = $Mid
      $LBound = $Mid
     }
  }
 # Now I know that the array index of our oldest item is here
 $Oldest = $LBound

Now we will repeat the process to find out LowerBound. I won’t document it too much since it is nearly identical code to above. If you have questions, please let me know.

$Ubound = $Entries.count - 1
$Lbound = 0
$Mid = 0
 
while (($Ubound - $Lbound) -gt 1)
   {
   $Mid = [int] ( ($Ubound + $Lbound) / 2 )
   if ($Entries[$Mid].TimeGenerated -lt $EndTime)
     {
      $LBound = $Mid + 1
     }
    elseif ($Entries[$Mid].TimeGenerated -gt $EndTime)
     {
      $Ubound = $Mid-1
     }
    else
    {
     $Ubound = $Mid
     $LBound = $Mid
    }
 }
$Newest = $LBound

So now we have the LowerBound index for the range I am looking for.
Now to finish it up…

# Sometimes we can end up with endpoints that don't meet our time range
# We fix that by going through each side and adjusting them until they
# are correct
 while ($Entries[$Newest].TimeGenerated -gt $Endtime)
  {
   $Newest--
  }
 
 while ($Entries[$Oldest].TimeGenerated -lt $StartTime)
  {
   $Oldest++
  }	
 
if (($Entries[$Newest].TimeGenerated -lt $StartTime) -and ($Entries[$Oldest].TimeGenerated -gt $EndTime))
 {
    #Insert Code here if you want to do something when
    #no events found during the period requested..
    $EntriesByDate = $NULL
 }
else
 {
    #Create a new array and assign it the $Entries indexes ranging from our 'oldest' to our 'newest'
    $EntriesByDate = $Entries[$Oldest..$Newest]
  }
}
else
{
	#Insert Code here if you want to do something when
	# Logs have rolled... and none were in the specified range.
	$EntriesByDate = $NULL
 }
return $EntriesByDate
}

I hope I was able to articulate this effectively. As always, let me know if you have questions, comments, or suggestions.
Thanks for Reading and happy shelling.
So, that is basically it. Here is the code in one big block:

## Get-DatedLogEntries Function
## Written by: Mark A. Weaver
## Website: www.vmweaver.com
## Version: 1.0
## Date: 7/23/2009
## Purpose: This Function will get event log entries from the specified server using currently logged in
##          credentials and return an array of Events that occurred between the 2 times.
##          Not much error checking or validation is done, so you please edit to your liking.
##
##        Input:
##				-ServerName "ServerName"
##				-EventLogName "EventLogName"
##          -OldestTime [DateTime]OldestTime
##				-NewestTime [Datetime]NewestTime
##
##        Output:
##				Array of Event log entries or Null if none found
#############################
## Updates:
##
##
##
######################################################################
######################################################################
 
function Get-DatedLogEntries([string]$ServerName, [string]$EventLogName, [datetime]$OldestTime, [datetime]$NewestTime)
{
 
	#Grabbing my Eventlog Entries
	$EventLog = New-Object System.Diagnostics.EventLog($EventlogName)
	$EventLog.MachineName = $ServerName
	$Entries = $Eventlog.Entries
 
	#Defining my starting boundaries of my array
	$Ubound = $Entries.count - 1
	$Lbound = 0
	$Mid = 0
 
	#Setting up my dates
	$StartTime = $OldestTime
	$EndTime = $NewestTime 
 
	if ($Entries[0].TimeGenerated -lt $StartTime)
	{
		while (($Ubound - $Lbound) -gt 1)
		{
			$Mid = [int] ( ($Ubound + $Lbound) / 2 ) #Calculate my midpoint
			#Compare my midpoint to my StartTime
			if ($Entries[$Mid].TimeGenerated -lt $StartTime)
			{
				#If my midpoint is less than my Start time, then throw out all events
				#below and including my Midpoint
				$LBound = $Mid + 1
			}
			elseif ($Entries[$Mid].TimeGenerated -gt $StartTime)
			{
				#If my midpoint is greater than my Start time, then throw out all events
				#above and including my Midpoint
				$Ubound = $Mid-1
			}
			else
			{
				#If my midpoint is equal to my Start time, then I got lucky and found my time.
				#I just realized that I may need to do something else with this. May tackle that
				# later though...
				$Ubound = $Mid
				$LBound = $Mid
			}
		}
 
		# Now I know that the array index of our oldest item is here
		$Oldest = $LBound 
 
		$Ubound = $Entries.count - 1
		$Lbound = 0
		$Mid = 0
 
		while (($Ubound - $Lbound) -gt 1)
		{
			$Mid = [int] ( ($Ubound + $Lbound) / 2 )
			if ($Entries[$Mid].TimeGenerated -lt $EndTime)
			{
				$LBound = $Mid + 1
			}
			elseif ($Entries[$Mid].TimeGenerated -gt $EndTime)
			{
				$Ubound = $Mid-1
			}
			else
			{
				$Ubound = $Mid
				$LBound = $Mid
			}
		}
		$Newest = $LBound		
 
		while ($Entries[$Newest].TimeGenerated -gt $Endtime)
		{
			$Newest--
		}
 
		while ($Entries[$Oldest].TimeGenerated -lt $StartTime)
		{
			$Oldest++
		}	
 
		if (($Entries[$Newest].TimeGenerated -lt $StartTime) -and ($Entries[$Oldest].TimeGenerated -gt $EndTime))
		{
			#No events found during the period requested..
			$EntriesByDate = $NULL
		}
		else
		{
			#Create a new array and assign it the $Entries indexes ranging from our 'oldest' to our 'newest'
			$EntriesByDate = $Entries[$Oldest..$Newest]
		}
	}
	else
	{
		# Logs have rolled... and none were in the specified range.
		$EntriesByDate = $NULL
	}
	return $EntriesByDate
}

Powershell and DFSR

April 7th, 2009 Mark A. Weaver 57 comments
Rating 4.00 out of 5

Sorry for the long delay between posts, but work has been absolutely crazy.

Anyway, one of the recent tasks I have been working on is to find a way to check DFSR to make sure that our remote sites are properly replicating data back to our corporate datacenter.  Part of this new infrastructure relies heavily on Microsoft DFSR and all the cool stuff it brings (in 2003 R2).

Our support teams have been asking for ways to ensure that data has completely synchronized to our corporate datacenter every night.  Unfortunately there isn’t an easy way to determine this scriptomatically.  Well leave it to me to try some different things and attempt to put SOMETHING in place to do this.

Basically we have remote sites replicating during non-business-hours back to a central “hub” DFSR server.  We would then backup this “hub” server with our corporate backup infrastructure.  This is a WHOLE lot easier than getting users in remote sites to swap tapes or whatever and send them offsite, etc. 

The only way I have been able to determine the state of replication is to query the “backlog” of the remote site DFSR servers.  This should tell us how many files are sitting there awaiting replication. DFSRDIAG is a tool that can help us enumerate these files, but then we have to parse out the data.  We also need to know which replication partner, which replicated folder, and which replication group these remote sites belong to.

One way to enumerate that info is through a WMI query.  From the DFSR “hub” server you can enumerate all DFSR connections, groups, folders, etc. by running some queries against the “MicrosoftDFS” namespace.  This is different from standard WMI queries because the default namespace (cimv2) does not contain any DFSR configuruations.

Once we connect to this namespace, it is a fairly trivial task to cycle through all the connection partners, replication groups, and replicated folders.

We can then run the “DFSRDIAG” tool to see how many files are in the backlog.

Once we have determine how many files are out there for each replicated folder, we then write a custom event log entry and have our monitoring tools pick those up.

For this script I have set a threshold of 10 files before writing an “error” event log.  This can easily be changed based on your specific needs, though.  

You should also be able to easily customize the eventIDs and source information by modifying the values assigned to those variables.

For actually writing to the event log, I am “borrowing” some code my colleauge Mike put together.

Anyway, I think the script is fairly self explanitory.  If you need additionaly info or have questions, please let me know.

Thanks and happy scripting…

– Mark

## Check-DFSR.ps1 script
## Written by: Mark A. Weaver
## Site: http://www.vmweaver.com
## Version: 2.0
## Date: 5/7/2009
## Purpose: This script will query the local WMI root for DFS replication groups and folders.  
##				It will then run DFS utilities to determine the number of files in the backlog on the
##          destination partners in the replication group.
##          
##          This script was written for the spefic use of being run on a centralized DFSR server
##          which acts as the HUB for remote office backups.
##         
##        
##          Monitoring Rules can be setup to collect and report on the events being generated.
## 
##          Event information is written to the Application log using the EventIDs at the bottom.
## Input: None
#############################
## Updates:
##  20090408 Weaver: Fixed issue where multiple events are generated throughout the execution
##  20090408 Weaver: Added BacklogFileCount to event message
##  20090409 Weaver: Fixed list of replication connections issue due to change in replication topology
##  20090507 Weaver: Added functionality to return results from all partners in the replication
##
##
######################################################################
######################################################################
# Write-Event powershell function
# Written by Mike Hays
# http://blog.mike-hays.net
#
#
 
function Write-Event(
	[string]$Source = $(throw "An event Source must be specified."),
	[int]$EventId = $(throw "An Event ID must be specified."),
	[System.Diagnostics.EventLogEntryType] $EventType = $(throw "Event EventType must be specified. (Error, Warning, Information, SuccessAudit, FailureAudit)"),
	[string]$Message = $(throw "An event Message must be specified."),
	$EventLog
)
{
	#Uncommon event logs can be specified (even custom ones), but since that isn't generally
	#the desired result, I prevent that here
	$acceptedEventLogs = "Application", "System"
	if ($eventEventLog -eq $null)
	{
		$eventEventLog = "Application"
	}
	elseif (!($acceptedEventLogs -icontains $eventEventLog))
	{
		Write-Host "This function supports writing to the following event logs:" $acceptedEventLogs
		Write-Host "Defaulting to Application Eventlog"
		$eventEventLog = "Application"
	}
 
	#Create a .NET object that is connected to the Eventlog
	$event = New-Object -type System.Diagnostics.Eventlog -argumentlist $EventLog
	#Define the Source property
	$event.Source = $Source
	#Write the event to the log
	$event.WriteEntry($Message, $EventType, $EventId)
}
 
######################################################################
######################################################################
## Main 
## Errors written:
##   Log File: Application
##   Source: Check-DFSR Script
##   ID: 9500 - Lists fully replicated replication folders
##   ID: 9501 - Lists replication folders with less than the $BacklogErrorLevel files waiting 
##   ID: 9502 - Lists replication folders with more than the $BacklogErrorLevel files waiting
##   ID: 9503 - If a connection is not pingable, this event is written.
 
$BacklogErrorLevel = 10 
 
$ComputerName = $env:ComputerName
## Query DFSR groups from the local MicrosftDFS WMI namespace.
$DFSRGroupWMIQuery = "SELECT * FROM DfsrReplicationGroupConfig"
$RGroups = Get-WmiObject -Namespace "root\MicrosoftDFS" -Query $DFSRGroupWMIQuery
 
 
## Setup my variables
$ping = New-Object System.Net.NetworkInformation.Ping
$SuccessAudit = $Null
$WarningAudit = $Null
$ErrorAudit = $Null
$EventSource = "Check-DFSR Script"
$SuccessEventID = 9500
$WarningEventID = 9501
$ErrorEventID = 9502
$NoPingEventID = 9503
 
foreach ($Group in $RGroups)
{
	## Cycle through all Replication groups found
	$DFSRGFoldersWMIQuery = "SELECT * FROM DfsrReplicatedFolderConfig WHERE ReplicationGroupGUID='" + $Group.ReplicationGroupGUID + "'"
	$RGFolders = Get-WmiObject -Namespace "root\MicrosoftDFS" -Query $DFSRGFoldersWMIQuery
 
	## Grab all connections associated with a Replication Group
	$DFSRConnectionWMIQuery = "SELECT * FROM DfsrConnectionConfig WHERE ReplicationGroupGUID='" + $Group.ReplicationGroupGUID + "'"
	$RGConnections = Get-WmiObject -Namespace "root\MicrosoftDFS" -Query $DFSRConnectionWMIQuery	
	foreach ($Connection in $RGConnections)
	{
 
		$ConnectionName = $Connection.PartnerName.Trim()
		$IsInBound = $Connection.Inbound
		$IsEnabled = $Connection.Enabled
 
		## Do not attempt to look at connections that are Disabled
		if ($IsEnabled -eq $True)
		{  
			## If the connection is not ping-able, do not attempt to query it for Backlog info
			$Reply = $ping.send("$ConnectionName")
			if ($reply.Status -eq "Success")
			{
 
 
				## Cycle through the Replication Folders that are part of the replication group and run DFSRDIAG tool to determine the backlog on the connection partners.
				foreach ($Folder in $RGFolders)
				{
					$RGName = $Group.ReplicationGroupName
					$RFName = $Folder.ReplicatedFolderName
 
					## Determine if current connect is an inbound connection or not, set send/receive members accordingly
					if ($IsInBound -eq $True)
					{
						$SendingMember = $ConnectionName
						$ReceivingMember = $ComputerName
					}
					else
					{
						$SendingMember = $ComputerName
						$ReceivingMember = $ConnectionName
					}
					   $Out = $RGName + ":" + $RFName +  " - S:"+$SendingMember + " R:" + $ReceivingMember 
					   Write-Host $Out
						## Execute the dfsrdiag command and get results back in the $Backlog variable
						$BLCommand = "dfsrdiag Backlog /RGName:'" + $RGName + "' /RFName:'" + $RFName + "' /SendingMember:" + $SendingMember + " /ReceivingMember:" + $ReceivingMember
						$Backlog = Invoke-Expression -Command $BLCommand
 
						$BackLogFilecount = 0
						foreach ($item in $Backlog)
						{
							if ($item -ilike "*Backlog File count*")
							{
								$BacklogFileCount = [int]$Item.Split(":")[1].Trim()
							}
 
						}
 
 
						if ($BacklogFileCount -eq 0)
						{
							#Update Success Audit 
							$SuccessAudit += $RGName + ":" + $RFName + " is in sync with 0 files in the backlog from "+ $SendingMember + " to " + $ReceivingMember +".`n"					
 
						}
						elseif ($BacklogFilecount -lt $BacklogErrorLevel)
						{
							#Update Warning Audit
							$WarningAudit += $RGName + ":" + $RFName + " has " + $BacklogFileCount + " files in the backlog from " + $SendingMember + " to " + $ReceivingMember + ".`n"
						}
						else
						{
							#Update Error Audit
							$ErrorAudit += $RGName + ":" + $RFName + " has " + $BacklogFilecount + " files in the backlog from " + $SendingMember + " to " + $ReceivingMember + ".`n"
						}
						#Write-Host + $Folder.ReplicatedFolderName "- " $BackLogFilecount -foregroundcolor $FGColor
					}
				}
				else
				{ 
				Write-Host $ConnectionName "is not pingable" 
				$NoPingMessage = "Server """ + $ConnectionName + """ could not be reached.`nPlease verify it is on the network and pingable."
				Write-Event $EventSource $NoPingEventID "Warning" $NoPingMessage "Application"
				}
			}
 
	}
 
}
## Write my events to the local Application log.
 
if ($SuccessAudit -ne $Null)
{
	Write-Event $EventSource $SuccessEventID "Information" $SuccessAudit "Application"
}
 
if ($WarningAudit -ne $Null)
{
	Write-Event $EventSource $WarningEventID "Warning" $WarningAudit "Application"
}
 
if ($ErrorAudit -ne $Null)
{
	Write-Event $EventSource $ErrorEventID "Error" $ErrorAudit "Application"
}
Categories: Powershell, Scripting Tags: , ,

Zen and the Mystical Art of Candidacy Analysis

March 11th, 2009 Mark A. Weaver No comments
Rating 3.00 out of 5

Okay so maybe there is no Zen, nor is it really “mystical”. 

One of the MANY challenges we faced while trying to implement a brand-spanking-new virtualization infrastructure was to put together some type of guidance for our server admin teams to help them determine if existing systems would “live” happily in our new environment.

After reading recommendations from VMware, digging through Microsoft Management Packs for Operations Manager, etc., it didn’t seem that any of those really fit our needs nor did they make much sense.

So we happen to use Operations Manager to collect performance data on our Windows-based servers, but there is a TON of data.

We also had to make sure that the tools we were using (Operations Manager) didn’t aggregate our data.  We NEEDED to have granular data for our entire 3 month look-back period.  We just didn’t like the idea of averaging averages, so we have data points every 5 or 15 minutes (depending on the counter) for 90 days.  One of the early reports we developed let admins specify the peak usage times so that the performance analysis could be tailored for specific systems.

Now we have all of this data but how do we look at it?  How do we know what’s important?  What recommendations from VMware and MS do we use and which ones do we throw away?

Several of the deficiencies we saw with most tools are:

  1. They average entire periods of time (a day, a week, a month) with no regard for peak usage times
  2. They use aggregated data (kind of like above)
  3. They assume that “CPU %” means the same thing for all systems

So now we have to look only at peak times for specific servers.  Wow..that can be a VERY daunting task unless you make SOME assumptions.  That’s exactly what we did.

We know that not ALL systems will have the same usage patterns, but generally our systems will be the busiest during normal business hours at our corporate data center.  So, that would be Monday – Friday, 8am-5pm CST.  Well, we also have people using our systems on the east and west coasts, so if we move our “peak” time to 7am-7pm CST, we should cover most of the usage we wish to capture.

You  may be asking why not just take all data points and be done with it… I know *WE* did.

It would certainly have been easier, but far less accurate.  To demonstrate this, think about this:

  • A system has a CPU utilization of 75% all the time during peak usage times
  • This same system has a CPU utilization of 10% during off-peak times
  • This system has a CPU utilization of 10% during the ENTIRE weekend

Okay, so if we take the mean average (the normal practice) we get something in the range of…well.. I am not gonna do the math, but it is WAY less than 75%.  We THEN thought, well… why not just take the highest or maximum measurement for the entire period and plan for that?  That doesn’t work out so well either if you have systems that don’t do ANYTHING, then a virus scan kicks off and pegs the CPU for 3o minutes.  This greatly throws our performance footprint off.  So now we have to find a happy sweet-spot for evaluating performance.

After mulling through all of our options and thinking about what types of data were REALLY helpful in this process, we decided to do a “Top 20% Average”.  I know it sounds a little strange, but it is pretty straight-forward. How did we come up with this number?  Well, we had to pick a number and threw some others around and this one seems to work, so we stuck with it.  The calculation is pretty simple:

  1. Take all of your data points
  2. Sort them High to Low
  3. Take the top 20% of your values and average them

This gives us a nice look at what our systems do when they are operating at or near their highest loads.  The benefit of having a Top 20% Average and its normal average is that the greater the difference between the two, the “more spiky” the performance is.  As the numbers get closer and closer we can see that our systems are more consistently busy during our peak usage time.

The two most important things we need to be looking at are CPU and Memory utilization since those are often the most limiting resources in our environment.  Of less importance to us were Disk IO and Network IO because we were really looking at “low hanging fruit” types of systems and NORMALLY if a system is disk or network heavy, CPU utilization will also shoot up.

So now we have this notion of Top 20% Average (T20 Average) and Average, but what are we actually analyzing?

Not So Normal CPU Usage

Well, we can’t just look at CPU Utilization Percent!  Why, you may ask? Well CLEARLY the following are not equivalent:

  • 1 CPU – 700MHz system running 75% utilization
  • 4 CPU – 2.2 GHz system running 20% utilization

Many tools used to evaluate this type of data look at Total CPU Percent Utilization.  This would mean our 1 CPU system has a larger processor footprint than our 4 CPU system.  This doesn’t really make sense.

One of the interesting strategies VMware presents in some of their documentation is this idea of a “normalized” CPU.   Basically this is kind of calculating how many megahertz  a system uses instead of using a percentage (which is relative to how many horses are under the hood).  While this isn’t entirely accurate, it does help us create high-water marks for a normalized calculation.

In the above example, our 1CPU system would have a normalized CPU usage of :

  • 1 CPU x 700MHz  x .75 = 525MHz

Our 4CPU system would normalize to :

  • 4 CPU x 2200MHz x .20 = 1760MHz

It is MUCH easier to compare the 2 of them now and to see which one will probably have a larger processor footprint.  I do realize that it doesn’t really take into account performance of multi-threaded applications and such, but it does a pretty good job.

So, how do we get all that info to calculate it? Well, we also happen to have SMS or SCCM available to collect configuration information about our systems.  This enables us to create some custom database views that contained system information regarding CPU Speeds, number of CPUs,  Hyperthreading configuration, etc.  We needed to have info on Hyper-Threading enabled hardware because Windows actually reports those “HT” processors as physical processors.  Unfortunately a  HT processor doesn’t really have the same performance as a TRUE physical processor.

To get around this, we figure a HT Processor is about a half of a physical processor.  So if our 4CPU system above is really a 2CPU system with HT, it would look like this:

  • 2 CPU x 2200MHz x .20 = 880MHz
  • 2 CPU x .5 (because they are HT)  x 2200MHz x .20 = 440MHz
  • Total Normalized CPU usage is 1320MHz

I hope I did an okay job of explaining it.  If not, please let me know.

Memory and Commitment

Memory is VERY easy to evaluate compared to CPU numbers as it is already normalized (kinda).  We will use the same methodologies for memory as we did for CPU with respect to Average Values and peak times.  When looking at memory counters, we are interested in finding out how much memory our systems are using, not how much is free, or the percent free or anything like that.

As Microsoft calls it, we need the “Committed Bytes in Use” counter.   I don’t think Operations Manager collects this info by default, so you may need to start collecting it to do this type of analysis.

Other than using the right counter it is pretty much the same as CPU.

Disk and Network Stuff

While we DO care about network and disk throughput, they typically will be weighted much less than CPU and memory utilization when looking at the overall analysis.  If you are looking at virtualizing some beefy SQL or Exchange systems, then these will become MUCH more important, but probably still following memory and cpu.

We use the same Average calculations as we do for CPU and Memory, but we will look at the disk counter “Disk Bytes per Second”  for this analysis and
“Total Bytes per second”(I think) for the network counter.  It MAY be important to eliminate loopback adapters in the query for network data, but most of the time our data comes back with numbers that are WAY below our thresholds for candidacy on even relatively busy systems.

Whew!!!

So, I made it through this in one fell swoop.  I am SURE that it isn’t written perfectly, but I wanted to get my thoughts down on this topic.  If you have any questions about it or recommendations….. PLEASE comment and open a dialogue.

Thanks for making it through this… and good luck.

– Mark

Powershell Power! (Part Deux)

February 23rd, 2009 Mark A. Weaver No comments
Rating 3.00 out of 5

Okay, so this is the second installment of my little tutorial-thingy for Powershell.  After much thought on where I want this to go, I figured the next logical step would be to talk about setting up your Powershell (“PoSh”) environment.

So, let’s put together a little laundry-list of tools you may want to procure.  Just download the bits and save them for later.  We will go through some of these in more detail.

  1. Powershell installation bits (requires at least .NET 2.0 Framework)
  2. PowerTab from ThePowerShellGuy (Most EXCELLENT Site for all things PoSh)
  3. Quest PowerGUI (A powerful FREE GUI and IDE for PoSh) I won’t cover this installation here, but it is pretty straight forward.

That will probably be enough to get moving.

The Powershell install should be fairly straight forward.  Just take all the default options and let-her-role.

NOTE: If you want to install a newer version of Powershell, you will need to Uninstall the previous version.  This is kinda sucky, but not too bad.  To Uninstall it, you can go to Add/Remove Programs and look for it, but you may not see it if you have “Show Updates” option off.  It probably will show up as “Window Powershell”.

To install PowerTab, unzip the downloaded file to a folder you want it to live permanently.  I normally pick something like “C:\Program Files\PowerTab” so it is where all my other programs live.

Now open a command shell and start Powershell by typing in “powershell” and hitting Enter.

This may take a few seconds to launch, but if successful you should see your new and shiny Powershell prompt.   The first thing to do before anything else make it so we can actually run scripts.

Out-of-the-box, Powershell is delivered secure.  SO secure, in fact, that you can NOT run ANY scripts without making it a little less secure.  I will probably discuss this in more detail later or pressure my buddy Mike to write about it on his blog (this is more up his alley than mine).

Anyway…at your PoSh prompt type in the cmdlet:

Set-ExecutionPolicy RemoteSigned

If successful, you should be kicked back to your prompt.  Now we can move forward with the PowerTab install.

CD to the directory you install PowerTab to and run the “Setup.CMD” file to start the installer.  I normally just take all the defaults and let it go.  This will probably take a few minutes to finish up.

Now to show you a little about what PowerTab does for you….

If you type “Get-” and then hit the TAB key you should see a popup window with all sorts of fun things.  Basically PowerTab is like tab completion on STEROIDS.  It is very helpful for discovering what is available and such.  I hope you will take this opportunity to explore Powershell a bit.

You can get a list of all commands (called Commandlets) by typing in “Get-Command”.

I guess that is a fairly good place to break and talk about the “Cmdlets” (said CommandLets).

Cmdlets are the meat-and-potatoes of Powershell.  They are to Powershell what “cd” and “dir” are to the normal Windows command shell (cmd.exe).  Cmdlet names are constructed of 2 parts: a VERB and a NOUN.  If you look at the output of the “Get-Command” cmdlet, you will see lots of verbs on the left side of the “dash” in the cmdlet name: “Get”, “Out”, “Write”, etc.  The right side of the cmdlet name is the noun, or the thing that the verb acts on.

As you can see there are common Verbs and Nouns.  You can, in fact, do a “get-command -verb get” to list all commands that have the verb “Get”.  You can do the same for Nouns.  Go ahead and try that out on a few.

There are several cmdlets you should be using as you are learning the ins and outs of Powershell.  Probably the MOST helpful is, well “Get-Help”.

Get-Help is one I use on a VERY regular basis.  The general format for this one is:

  • “Get-Help <Cmdlet>”    : Gives basic info on the cmdlet
  • “Get-Help <Cmdlet> -Detailed”  : Gives  MUCH more info about the Cmdlet including some examples on using the cmdlet
  • “Get-Help <Cmdlet> -Full” : I think this gives ALL the info about the Cmdlet
  • “Get-Help <Cmdlet> -Examples” : Just gives you the usage examples.

Go ahead and try it now.  Do a “get-command”,  pick one that looks interesting to you and “get-help” on it.  This was crucial for me in learning the Powershell.

While I bring this part to a close I will challenge you to write something…well ANYTHING really.  The best way for me in learning was to pick a task and do it in Powershell.  The thought process for me was “Well, I could do this in vbScript in like 5 minutes, or I could take 30 minutes and do it in Powershell and learn a ton.”  If I have the time to do it in Powershell, I am doing it.

Some simple tasks would be to start using Powershell as your “normal” command shell.  This will get you used to using the cmdlets to navigate the environment and doing things.  Try NOT to use some of the default aliases that are “replacements” for the standard “cmd.exe” commands. These would include “type”, “dir”, etc.  I have been using the Powershell equivilant aliases…  “dir” becomes “Get-ChildItem” or simply “gci”.

The other thing I will challenge you to look at is the cmdlet “Get-Member”.  This will help you in this worthwhile venture into the wonderful world of Powershell.

I think the next session we will cover the notion that Powershell is “Object-Based”, which will be a good lead in from your “homework” on the “Get-Member” cmdlet.

Anyway, until next time…. happy Scripting!

– Mark

Categories: Powershell, Scripting, Tutorials Tags:

Powershell Power! (Part the First)

February 17th, 2009 Mark A. Weaver No comments
Rating 3.00 out of 5

This will (hopefully) be the first of several ‘tutorials’ on Powershell.

Many of you have probably seen code in the forums or tutorials online that show some exerpts of code that are just CRAZY!  Many containing aliases like “%” and “?” and lots of braces and parentheses of all shapes and sizes.

Well, one of the things I love about Powershell is that you can do some VERY powerful, yet hard-to-read things at the command line.  While this may seem super and efficient for someone who knows Powershell, it SUCKS if you are trying to learn the language.

When I was trying to pick it up, this is all you could really find and it wasn’t until I found (what I believe to be the Powershell BIBLE) Windows PowerShell in Action by Bruce Payette.

In my mind there are ideally two different styles to Powershell(ing).

  • The free-form, Wild-Wild-West,  I-need-to-pound-this-out  command line style
  • The structured, methodical, EXPLICIT scripting style

From a practical sense, though, I think most people end up somewhere in the middle.  What absolutely drives me bonkers is obscure-one-liners that veteran Powershell scripters spew to  the newbie scripters asking relatively simple questions like:

“How  do I get a list of all files in a directory that have been modified in the last 3 weeks?”

One may be inclined to say “Oh, that’s easy!  Here it is…”

gci |sort| %{if ($_.LastWriteTime -GT (get-date).Adddays(-21)) `
{write-host $_.name "--"  $_.LastWriteTime -foreground Red}}

But for someone unfamiliar with Powershell, it looks like a bunch of gibberish.  Now something like THIS may be more helpful for them:

$AllFiles = Get-ChildItem | Sort-Object
$Now = Get-Date
$CutoffDate = $Now.AddDays(-21)
Foreach ($File in $AllFiles) {
     If ($File.LastWriteTime -gt $CutoffDate)
     {
      Write-Host $File.Name "--" $File.LastWriteTime -Foreground Red
      }
  }

Okay, if someone is asking this question, they are certainly new to the language and so an obscure response will probably only confuse them and, hopefully, not taint their desire to learn this robust and relatively simple tool.

ANYWAY… sorry about that half rant/half informational bit…Let’s talk a little bit about Powershell…

What *IS* Powershell?

Well the quick-and-dirty is that it is a “new” scripting language and command shell geared for IT administrators with a focus on simplicity, power, and automation.

The “official” description can be found here: http://www.microsoft.com/windowsserver2003/technologies/management/powershell/default.mspx

Anyway, now that you have an idea of what Powershell is, we will take a peak at some of the easy stuff you can do with it.  I will probably start with some of the fundamentals of scripting and move forward from that.

That’s all for now, but I will write more shortly.  I still haven’t figured out what format I want to use, but we will figure it out together.

Thanks for reading, sorry for the mild rant, and happy shelling!

– Mark

Categories: Powershell, Scripting, Tutorials Tags:

Get-HACapacity

February 6th, 2009 Mark A. Weaver No comments
Rating 3.00 out of 5

This script will calculate the HighAvailability capacity of a VMware ESX Cluster.

### Written by Mark A. Weaver
##  Date: 7/27/2008
##  Version: 1.0
##  Website: www.vmweaver.com
##
##  Call this function and pass in -ServerName  -ClusterName
##  Output should be an object containing the information
##
##  Feel free to modify as needed to suit your needs, but please keep this header
##
##  Thanks  -- Mark
 
function Get-HACapacity(
[string]$ServerName,
[string]$ClusterName)
{
		if (($ServerName -ne "") -and ($ClusterName -ne ""))
	{
		# These booleans tell me if I am using the VMware default memory and cpu reservations for the cluster.
			$DASMemDefault = $True
			$DASCPUDefault = $True
 
		# The following numbers are derived from VMware published numbers for memory overhead.
		# I have dropped them into arrays using the number of vCPUs as an index to get the correct constant.
		# This is why you will notice only [1], [2], and [4] have non-zero values
		# These constants are used later on when calculating Memory Reserve.
			$MemConst32 = 0, 3.262, 5.769059, 0, 6.77933
			$MemConst64 = 0, 3.2678, 5.79251, 0, 6.82622
			$MemBase32 = 0, 87.56, 108.73, 0, 146.75
			$MemBase64 = 0, 107.54, 146.41, 0, 219.82
 
		# Initialize some Variables
			$MaxMemRes = 0
			$MacNumCPU = 0
			$MaxCPUResVM = ""
			$VMCount = 0
 
		# define default memory and cpu reservation
			$DASMinMHz = 256
			$DASMinMemory = 256
 
			$viServerName = $ServerName
			$viClusterName = $ClusterName
 
		# Connect to the VirtualCenter Server and get some info
			$viServer = Connect-VIServer $viServerName
			$viCluster = get-cluster $viClusterName
			$viHosts = get-vmhost -location $viCluster
			$viClusterV = get-view $viCluster.ID
 
		# Get the "Resources" Resource Pool from the cluster.
		# This gives us the Reservation Pools for Memory and CPU
			$viResGroup = Get-ResourcePool -Name "Resources" -Location $viCluster
			$viCPURes = $viResGroup.CpuReservationMHz
			$viMemRes = $viResGroup.MemReservationMB
			$viHostCount = $viClusterV.Summary.NumHosts
 
		# Get HA cluster configuration information
			$viHostFailures = $viClusterV.Configuration.DasConfig.FailoverLevel
 
		# Get a list of options that may be configured at the clusters level
		# We are looking for whether or not the default memory and cpu
		#  reservations have been overridden
			$viDASOptions = $viClusterV.Configuration.DASConfig.Option
			$viVMs = get-vm -Location $viCluster
 
		# Is Adminisssion Control enabled on the cluster?
			$viClusterControl = $viClusterV.Configuration.DASConfig.AdmissionControlEnabled	
 
		# See if das.vmMemoryMinMB key is defined and grab its value
		# See if das.vmCpuMinMHZ key is defined and grab its value
			if ($viDASoptions.Count -ne 0)
			{
				foreach ($viDASOption in $viDASOptions)
				{
					if ($viDASOption.Key -eq "das.vmMemoryMinMB")
					{
						$DASMemDefault = $False
					$DASMinMemory = $viDASOption.Value }
 
					if ($viDASOption.Key -eq "das.vmCpuMinMHz")
					{
						$DASCPUDefault = $False
					$DASMinMHz = $viDASOption.Value }
				}
		}
 
		# Let's go through every VM and see what the maximum CPU and Memory reservation is.
		# We will also get a count of powered on VMs.
		# When we hit a maximum reservation, save the machine name that set that maximum
			foreach ($viVM in $viVMs)
			{
				$NumCPU = $viVm.NumCPU
				$VMMem = $viVm.MemoryMB
				$MemRes = 0
 
				if ($viVM.PowerState -eq "PoweredOn")
				{
					$VMCount += 1
			}
 
			# Get the VM-view and determine if the current guest CPU or memory reservations configured
			$vmView = get-view $viVM.ID
			$vmViewCPURes = $vmView.ResourceConfig.CpuAllocation.Reservation
			$vmViewMemRes = $vmView.ResourceConfig.MemoryAllocation.Reservation
 
			# If no reservations are set at the VM level, calculate the memory reservation.
			if ($vmViewMemRes -eq 0)
			{
				if ($VMMem -le 256)
				{
					$MemRes = $MemConst64[$NumCpu] + $MemBase64[$NumCPU]
				}
				else
				{
					if ((($viVM.Guest.OSFullName | Select-String "64-bit").Matches.Count) -ge 1)
					{
						$MemRes = ($VMMem / 256) * $MemConst64[$NumCPU] + $MemBase64[$NumCPU]
					}
					else
					{
						$MemRes = ($VMMem / 256) * $MemConst32[$NumCPU] + $MemBase32[$NumCPU]
					}
				}
 
				$MemRes += $DASMinMemory
			}																			
 
			else
			{
				$MemRes = $vmViewMemRes 									
 
			}
 
			#Figure out if the current VM holds the highest reservation so far
 
				if ($vmViewCPURes -gt $DASMinMHz)
				{
					$DASMinMHz = $vmViewCPURes
					$MaxCPUResVM = $viVM.Name
				}			
 
				if ($MemRes -gt $MaxMemRes)
				{
					$MaxMemRes = $MemRes
					$MaxMemResVM = $viVM.Name
				}
 
				if ($NumCPU -gt $MaxNumCPU)
				{
					$MaxNumCPU = $NumCPU
					$MaxCPUNumVM = $viVM.Name
				}
 
			}
 
			if ($MaxCPUResVM -eq "") { $MaxCPUResVM = $MaxCPUNumVM }			
 
		$MaxCPURes = $MaxNumCPU * $DASMinMHz
 
		# Calculate the VM Capacity for the cluster based on memory and cpu reservations.
			$ClusterVMCapacityMEM = [Math]::Truncate(((($viMemRes / $MaxMemRes) * ( $viHostCount - $viHostFailures )) / $viHostCount))
			$ClusterVMCapacityCPU = [Math]::Truncate(((($viCPURes / $MaxCPURes) * ( $viHostCount - $viHostFailures )) / $viHostCount))
 
			if ($ClusterVMCapacityMEM -lt $ClusterVMCapacityCPU)
			{
				$ClusterVMCapacity = $ClusterVMCapacityMEM
			}
			else
			{
				$ClusterVMCapacity = $ClusterVMCapacityCPU
		}
 
		# Create an object to return
			$CPUObj = New-Object System.Object
			$CPUObj | Add-Member -type NoteProperty -name ClusterCPURes -value $viCPURes
			$CPUObj | Add-Member -type NoteProperty -name DefaultCPURes -value $DASCPUDefault
			$CPUObj | Add-Member -type NoteProperty -name MinCPURes -value $DASMinMHz
		   $CPUObj | Add-Member -type NoteProperty -name MaxCPUNumVM -value $MaxCPUNumVM
			$CPUObj | Add-Member -type NoteProperty -name MaxCPURes -value $MaxCPURes
			$CPUObj | Add-Member -type NoteProperty -name MaxCPUResVM -value $MaxCPUResVM
			$CPUObj | Add-Member -type NoteProperty -name MaxCPUs -value $MaxNumCPU
			$CPUObj | Add-Member -type NoteProperty -name VMCapacityCPU -value $ClusterVMCapacityCPU
 
			$MemObj = New-Object System.Object
			$MemObj | Add-Member -type NoteProperty -name ClusterMemRes -value $viMemRes
			$MemObj | Add-Member -type NoteProperty -name DefaultMemRes -value $DASMemDefault
			$MemObj | Add-Member -type NoteProperty -name MinMemRes -value $DASMinMemory
			$MemObj | Add-Member -type NoteProperty -name MaxMemRes -value $MaxMemRes
			$MemObj | Add-Member -type NoteProperty -name MaxMemResVM -value $MaxMemResVM
			$MemObj | Add-Member -type NoteProperty -name VMCapacityMem -value $ClusterVMCapacityMEM
 
			$OutObj = New-Object System.Object
			$OutObj | Add-Member -type NoteProperty -name AdmissionControl -value $viClusterControl
			$OutObj | Add-Member -type NoteProperty -name CPU -value $CPUObj
			$OutObj | Add-Member -type NoteProperty -name FailoverHosts -value $viHostFailures
			$OutObj | Add-Member -type NoteProperty -name HostCount -value $viHostCount
			$OutObj | Add-Member -type NoteProperty -name Memory -value $MemObj
			$OutObj | Add-Member -type NoteProperty -name RunningVMs -value $VMCount
			$OutObj | Add-Member -type NoteProperty -name VIServer -value $viServerName
			$OutObj | Add-Member -type NoteProperty -name VICluster -value $viClusterName
			$OutObj | Add-Member -type NoteProperty -name VMCapacity -value $ClusterVMCapacity	
 
			return($outObj)
	}
	else
	{
		# Write usage info
		Write-Host ("")
		Write-Host ("-------------------------------------")
		Write-Host ("Get-HACapacity.ps1 Usage:")
		Write-Host( "You must specify the following parameters: ")
		Write-Host ("     '-ServerName '  where  is the name of the VirtualCenter Server")
		Write-Host("     '-ClusterName '  where  is the name of the cluster to query")
		Write-Host ("")
		}
}

Let me know if you have any problems with it or would like more info..

– Mark