15 December, 2012

Useful Script functions - Scripting

As you can see in previous posts, I publish code snippets in the articles which of course are not my full scripts. There are good reasons for that, e.g. if I publish full scripts which are usually 500 - 2000 lines long, the main point of the article gets lost. The proper scripts contain much more "frippery" like parameter evaluation, logging, remote host checks...etc.

These things are quite important when you design a script for scalability and to run in a large environment, i.e. if you don't perform some sort of accessibility check (with well thought timeout interval) against the 1000+ servers your script works on, that script will run forever. OK, I'm exaggerating, but it feels like forever when people are nagging you on the phone to get results during an outage and it takes 10 mins to get them instead of 3 because the script waits for the default WMI interval for each and every query on offline hosts instead of just timing out once (and quickly) when it detects that the remote host is not accessible... and perspiration sprang from your brow.

Now, here are some important functions you want to have in your script - of course there can be many different ways of implementation for these, but you get the idea...:

Ping remote hosts:
#### 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}
}

Check if you can access remote hosts (e.g. RPC is not dead and check if you have admin rights). I usually check WMI. Why WMI? Because most of the remote actions you want to perform will use WMI, or something that builds on WMI. If WMI is broken, you might as well skip that host from the list and mark it as unhealthy. Another reason is that remote WMI is a good indicator for checking administrator access or if RPC is alive on the box.

However, do not use the native Get-WMIObject cmdlet, because it does not have a timeout value. I've seen Windows 2003 servers almost every week in the past which were in a sort of "half-hung" state. It is usually an indication of a paged or non-paged pool leak of a driver when the box is going downhill but is not quite there yet, so you can still RDP to it, but any remote query (registry, WMI, SMB...etc) will be taking hours to run or time out. Literally hours... so you need pre-defined timeout:
#### Function for checking WMI health
function checkWMI ([string]$srv){
   $checkwmi = $null
   $timeout = new-timespan -seconds 15
   $Scope = new-object System.Management.ManagementScope "\\$srv\root\cimv2", $options
   $Scope.Connect()
   $query = new-object System.Management.ObjectQuery "SELECT * FROM Win32_OperatingSystem"
   $searcher = new-object System.Management.ManagementObjectSearcher $scope,$query
   $SearchOption = $searcher.get_options()
   $SearchOption.set_timeout($timeout)
   $searcher.set_options($SearchOption)
   $checkwmi = $searcher.get()
   $lastBoot = $checkwmi | %{$_.lastbootuptime}

 
  if($lastBoot){
      return $true
   }
 
  else{

      return $false
   }
}



Logging the actions of your script. In Powershell you either write to the standard output (e.g. Write-Host) or into a file (e.g. Out-File). But what if you want to do both at the same time?  And you want to do color coding of different messages on the screen? And you want to have time stamps for each message? Here is one option:

$script:logfile = New-Item -type file "c:\temp\Test_logfile.log" -force

#### Function for creating log entries in a logfile and on standard output
function writeLog ([int]$type, [string]$message, [string]$modifier) {
   # usage: writeLog 0 "info or error message"
   # $modifier: <nonewline | extendline>
   # Value nonewline: writes the output the the console and the logfile without carriage return
   # Value extendline: writes the message to the output without date and
   # both values can be used e.g. for writing to the console and logfile and put a status message at the end of the line as a second step

   $date = get-date -uformat "%Y/%m/%d %H:%M:%S"
   if($modifier -eq "extendline"){
      switch ($type) {
         "0" {$color = "Green"}
         "1" {$color = "Yellow"}
         "2" {$color = "Red"}
      }
   }
   else{
      switch ($type) {
         "0" {$message = $date + ", INF, " + $message; $color = "Green"}
         "1" {$message = $date + ", WAR, " + $message; $color = "Yellow"}
         "2" {$message = $date + ", ERR, " + $message; $color = "Red"}
      }
   }

 
  if($modifier -eq "nonewline"){

      write-host $message -ForegroundColor $color -NoNewLine
      # need to encode with bytes to be able to drop the carriage return character in the output file
      $bytes = [text.encoding]::ascii.GetBytes($message)
      $bytes | add-content $script:logfile -enc byte
   }
   
else{

      write-host $message -ForegroundColor $color
      Add-Content $script:logfile $message
   }
}


If you create log files, you want to maintain those so they don't get out of control and consume too much disk space. An option is to rotate them based on their date:
#### Function for deleting logfiles older than X days
function logRotate ([string]$logfolder, [string]$strScriptname){
   if($strScriptname){
      $lastWriteLimit = [DateTime]::Now.AddDays(-7)

      if($logfolder){
         $tobedeleted = Get-ChildItem "$logfolder" -force -filter "$strScriptname*" | where{(!$_.PSIsContainer) -and ($_.LastWriteTime -le "$lastWriteLimit")}

         if($tobedeleted){
            foreach($delLogfile in $tobedeleted){
               $strDelLogfile = $dellogfile.name + " - " + $dellogfile.lastWriteTime
               writelog 1 "Old log to be deleted: $strDelLogfile"
               $strDellogfile = ""
            }            Get-ChildItem "$logfolder" -force -filter "$strScriptname*" | where{(!$_.PSIsContainer) -and ($_.LastWriteTime -le "$lastWriteLimit")} | Remove-Item -Force
         }
      }
   }
}


Obviously, these are just my way of performing the checkouts and doing logging, but the principal is there: always perform quick checks if you handle large number of hosts, not just because you want to timeout on hosts quickly but because you want to have a proper output and useful info on all hosts, even if it says: "not pingable".

Clipboard friendly code:
 #### 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}  
 }  
   


 #### Function for checking WMI health  
 function checkWMI ([string]$srv){  
      $checkwmi = $null  
      $timeout = new-timespan -seconds 15  
      $Scope = new-object System.Management.ManagementScope "\\$srv\root\cimv2", $options  
      $Scope.Connect()  
      $query = new-object System.Management.ObjectQuery "SELECT * FROM Win32_OperatingSystem"  
      $searcher = new-object System.Management.ManagementObjectSearcher $scope,$query  
      $SearchOption = $searcher.get_options()  
      $SearchOption.set_timeout($timeout)  
      $searcher.set_options($SearchOption)  
      $checkwmi = $searcher.get()  
      $lastBoot = $checkwmi | %{$_.lastbootuptime}  
        
      if($lastBoot){  
           return $true  
      }  
      else{  
           return $false  
      }  
 }  
   


 $script:logfile = New-Item -type file "c:\temp\Test_logfile.log" -force  
   
 #### Function for creating log entries in a logfile and on standard output  
 function writeLog ([int]$type, [string]$message, [string]$modifier) {   
      #usage: writeLog 0 "info or error message"  
      # $modifier: <nonewline | extendline>  
      # Value nonewline: writes the output the the console and the logfile without carriage return  
      # Value extendline: writes the message to the output without date and   
      # both values can be used e.g. for writing to the console and logfile and put a status message at the end of the line as a second step  
   
      $date = get-date -uformat "%Y/%m/%d %H:%M:%S"  
      if($modifier -eq "extendline"){  
           switch ($type) {  
                "0"     {$color = "Green"}  
                "1"     {$color = "Yellow"}  
                "2"     {$color = "Red"}  
           }  
      }  
      else{  
           switch ($type) {  
                "0"     {$message = $date + ", INF, " + $message; $color = "Green"}  
                "1"     {$message = $date + ", WAR, " + $message; $color = "Yellow"}  
                "2"     {$message = $date + ", ERR, " + $message; $color = "Red"}  
           }  
      }  
        
      if($modifier -eq "nonewline"){  
           write-host $message -ForegroundColor $color -NoNewLine  
           $bytes = [text.encoding]::ascii.GetBytes($message)  
           $bytes | add-content $script:logfile -enc byte  
      }  
      else{  
           write-host $message -ForegroundColor $color  
           Add-Content $script:logfile $message  
      }  
 }  
   


 #### Function for deleting logfiles older than X days  
 function logRotate ([string]$logfolder, [string]$strScriptname){  
      if($strScriptname){  
           $lastWriteLimit = [DateTime]::Now.AddDays(-7)  
           if($logfolder){  
                $tobedeleted = Get-ChildItem "$logfolder" -force -filter "$strScriptname*" | where{(!$_.PSIsContainer) -and ($_.LastWriteTime -le "$lastWriteLimit")}   
                if($tobedeleted){  
                     foreach($delLogfile in $tobedeleted){  
                          $strDelLogfile = $dellogfile.name + " - " + $dellogfile.lastWriteTime  
                          writelog 1 "Old log to be deleted: $strDelLogfile"  
                          $strDellogfile = ""  
                     }  
                     Get-ChildItem "$logfolder" -force -filter "$strScriptname*" | where{(!$_.PSIsContainer) -and ($_.LastWriteTime -le "$lastWriteLimit")} | Remove-Item -Force  
                }  
           }       
      }  
 }  
   

May The Force...
t

2 comments:

  1. What does the Trap line do in the ping function? Great collection, thanks, Master tompa.

    ReplyDelete
    Replies
    1. If you have a terminating error, e.g. the .net object (System.Net.Networkinformation.Ping) throws an unhandled exception or even a handled one which is a terminating error, it terminates the whole PS session, so your script will not continue. That line traps the error and as the trap has a 'Continue' command in there, the function will simply contiue doing its job.

      If you use the PingServer function against 100+ servers, you really want the full list to be pinged instead of seeing your script terminate on the first unpinagle on the list, that's why the trap is there.

      to read more, run this in PS:
      PS C:\> man about_trap

      t

      Delete