27 October, 2013

Object output for Powershell scripts - Scripting

Through previous articles I kept writing about object output and how good that is for further processing in Powershell. What is an object... well... I'm not a developer, so I won't be able to explain it with nice sophisticated phrases. The object is a thing :)

For me the easiest way to imagine an object as a row of a table. The table has several columns which will be the object's properties and you can put multiple objects into an object collection or array, that will form the table.


Listing a custom object collection
 
Why is this a good thing? First of all, if you want to store more than one type of data about a computer within your script, you can create those properties for your object and just add the data into them as you can see on the picture above. I stored information about 3 servers such as whether the servers are pingable, accessible and put some different test results as well.
Also, you can use filtering on your object collection, e.g. if I want to list rows (objects) where the 'Ping' column (property) is Error:
$objColl | where{$_.ping -eq "Error"}

Filtering a custom object collection

There are multiple ways to create an object.

  • System.Object with Add-Member - it is slower than other methods, however, you can specify the order of properties, you can add values to the properties when creating them:
    $myObject = new-Object -typename System.Object
    $myObject | add-Member -memberType noteProperty -name ComputerName -Value $srvname
    $myObject | add-Member -memberType noteProperty -name Ping -Value "N/A"
    $myObject | add-Member -memberType noteProperty -name Accessible -Value "N/A"
    $myObject | add-Member -memberType noteProperty -name Test1_Result -Value "N/A"
    $myObject | add-Member -memberType noteProperty -name Test2_Result -Value "N/A"


  • PSobject with Select - it is quicker than add-member and you can specify the order of properties, however you cannot add values to the properties when creating them and you assume all properties will have string type. It's not a big deal but not very elegant:
    $myObject = "" | Select ComputerName,Ping,Accessible,Test1_Result,Test2_Result

  • You can store property names in an array and pass it to the Select-Object filter:
    $arrProperties = @("ComputerName","Ping","Accessible","Test1_Result","Test2_Result")
    $myObject = "" | Select $arrProperties

  • PSObject defined via hash table - it's quick, you can add values to the properties in one go, however, as with hash tables in general, you cannot define the order of the properties, so it can be odd when you see the result and the columns are in random order
    $myObject = New-Object PSObject -Property @{ComputerName = $srvname; Ping = "OK"; Accessible = "N/A"; Test1_Result = "N/A"; Test2_Result = "N/A"}

The object is created, as you can see above, depending on how you create it, you can add values to properties on the fly or when the object is ready and can access the properties easily just referring to them with a dot:






More than one object

When you have more than one object, you can add them into an array and form a 'table':
$objColl = @()
$objColl += $myObject

Then you can modify the objects in the collection if you want to, e.g. if I want to make sure the Test1_Result column contains only a string of aaaa, I can simply do this:







There's one more good trick with object collections. I usually write scripts for large number of objects, e.g. check out 5000 servers, get data of 100 000 AD objects...etc. Therefore the object collection is big and non-indexed. Even though it's in the memory during runtime, it takes a while to search and find objects. One of my colleague had a good idea and I've been using it since then: put the objects into a hash table with the key that you want to search for. E.g. if you know that you want to search for the ComputerName and you want to read a property of that object from your collection you can make the ComputerName the hash key and the object itself the hash value:
$objCollHash = @{}
$objColl | %{$objCollHash.add($_.ComputerName, $_)}

Now we can test how much time it takes to find an object in a collection of 9999 objects with 3 attributes, first with normal filter:








900+ millisecond, not bad, so less than a second to find an object from 9999.

Ok, let's see the hash table:










Ughm... less than a millisecond! 900+ times quicker than the previous one. I guess it's worth the effort to put those 2 lines into the script.

That's all for today. Hope this helps people to understand why objects and pipe'ing is useful.

t

12 October, 2013

DFS link count check - DFS

There have been cases when people started screaming that their folders disappeared or they are there but are not accessible. OK... nicely defined problem, this is the sort of issue you want on a Monday morning, just to get the adrenalin flowing a bit. Let's see then, after checking a couple of these folders, it turns out that all of  them are DFS links. If I check those, they work for me... so this issue is even better that I thought 5 mins ago.
Let's check all DFS servers then. All of them seem to be healthy, but hold on... there's one which shows half of the number of pointers than it supposed to.
If you check locally, the DFS folder structure is there, but if you check it via the DFS root share, half the pointers are gone... weird, quick reboot helps, MSFT wants to reproduce the issue to get to the bottom of it... well, I don't want to reproduce it to be honest, but what I can do is run a quick check in the morning which would make sure I catch this before the users do.

If you look at the issue again, it's easy to pinpoint what to check: the count of directories on the DFS root on the DFS server's local drive is OK, however, the count of DFS links are not the same. Ok, so I need a script which counts both, compares them and if they are different, sends an alert.

Count number of folders which point to another folder (ReparsePoint):
dir $dfsdir -rec | ? {$_.Attributes -imatch 'ReparsePoint'} | measure | %{$_.count}


Count number of DFS links:
dfscmd /view $dfsroot /batch | ?{($_ -imatch "\\\\$dfsserver\\$dfsRootName") -and ($_ -inotmatch "^rem| /add")} | measure | %{$_.count}


Dfscmd lists all pointers and actions how to recreate them (map or add), we filter out all lines apart from the ones with /map and we also filter out every line which does not have the DFS root in them.


We just need to compare these numbers for each server, evaluate the result, put them in a nice output and e.g. send them via email.

Output of a GREEN checkout on two DFS roots


In my final script, I used Win32_Process to run the dir and dfscmd commands on the remote DFS servers and also used PowerShell jobs to run them in parallel therefore it's a lot quicker, however for the purpose of this article, I simplified the code to show the essence of the logic.


 $ErrorActionPreference = "SilentlyContinue"  
   
 #### Function for checking if a host is alive on the network  
 function PingServer ([string]$srv){  
      $ping = new-object System.Net.Networkinformation.Ping  
      Trap {Continue} $pingresult = $ping.send($srv, 3000, [byte[]][char[]]"z"*16)  
      if($pingresult.Status -ieq "Success"){$true} else {$false}  
 }  
                  
 $objColl = @()  
 $dfsrootlist = @($Input)  
   
 foreach($dfsroot in $dfsrootlist){  
      $obj = "" | Select DFS_Root,Access,Dir_Count,Link_Count,Result  
      $obj.DFS_Root = $dfsroot  
      $obj.Access="N/A"  
      $obj.Dir_Count="N/A"  
      $obj.Link_Count="N/A"  
      $obj.Result="N/A"  
        
      $dfsserver = $dfsroot.split("\")[2]  
      $dfsRootName = $dfsroot.split("\")[3]  
      $dfsdir = "\\" + $dfsserver + "\d$\DFSRoots\" + $dfsRootName  
        
      if(PingServer $dfsserver){  
           $obj.Access = "OK"  
   
           $obj.Dir_count = dir $dfsdir -rec | ? {$_.Attributes -imatch 'ReparsePoint'} | measure | %{$_.count}  
           $obj.Link_count = dfscmd /view $dfsroot /batch | ?{($_ -imatch "\\\\$dfsserver\\$dfsRootName") -and ($_ -inotmatch "^rem| /add")} | measure | %{$_.count}  
   
           # checking if link or directory count is 0  
           if(!$obj.Link_Count -or ($obj.Link_Count -eq 0)){  
                $obj.Result = "Error: Link count is 0"  
           }  
             
           if($obj.Link_Count -eq "N/A"){  
                $obj.Result += "Error-Could not collect dfs link count data"  
           }  
             
           if($obj.Dir_Count -eq "N/A"){  
                $obj.Result += "Error-Could not collect directory count data"  
           }  
             
           if(!$obj.Dir_Count -or ($obj.Dir_Count -eq 0)){  
                $obj.Result = "Error: Directory count is 0 <br>"  
           }  
             
           if($obj.Dir_Count -ne $obj.Link_Count){  
                if($obj.Result -eq "N/A"){$obj.Result = "Link/Dir count mismatch"}  
           }  
           else{  
                if($obj.Result -eq "N/A"){$obj.Result = "OK"}  
           }  
      }  
      else{  
           $obj.Access = "Error"  
      }  
      $objColl += $obj  
 }  
   
 $objColl  
   
 $rfbcount = ($objColl | ?{$_.result -ine "ok"} | measure).count  
 $Subject = "DFS Link Count"  
 if($rfbcount -ge 1){  
      if($rfbcount -ge 2)     {$subject += " - RED"}  
      else                    {$subject += " - AMBER"}  
 }  
 else{  
      $subject += " - GREEN"  
 }  
   
 $smtpServer = "smtpdroid.tatooine.com"  
 $smtpFrom = "reports@tatooine.com"  
 $smtpTo = "tompa@tatooine.com"  
 $message = New-Object System.Net.Mail.MailMessage $smtpfrom, $smtpto  
 $message.Subject = $Subject  
 $message.IsBodyHTML = $true  
 $message.Body = $objColl | ConvertTo-Html  
 $smtp = New-Object Net.Mail.SmtpClient($smtpServer)  
 $smtp.Send($message)  
   


t