Friday, September 20, 2013

Powershell Script - More Logparsing Multiple Servers

9/20/2013: I've been putting in a fair amount of time recently repetitively Logparsing the IIS logs on several regions of CAS servers

-- I'm attempting to guage MS Lync 'Exchange Web Services' load and demand on our production CAS's, ahead of new Lync capacity rollouts overseas --

So here's another quicky code chunk that I routinely grab and put to use for Logparsing multiple CAS servers of IIS logs.

It's a simple variant of my prior post "Powershell Script - Tip: Delimited String "Contants" & Multi-server Logparser scripts", only this time, rather than substitute in the entire LogPath, I'm foreach looping through the CAS server names.

Here's a specific example I just used this morning that builds a multi-server set of looped LogParser queries to pull out and group the Client 'agent' strings for all EWS traffic, and output the result into a csv file, leveraging substitution of the proper $CAS server name within the target UNC path to a log file...

Logparsing multiple servers, by foreach-ing an array variable
Language/Tools: Powershell, MS Logparser
Target: EMS or Powershell, parsing Exchange 2007/2010 CAS Server IIS logs
# comma-delimited server name array, 
#   foreach into logparser command
# Note: edit/update the logname string, to specify variant dates
# one-line follows (manually wrapped for clarity)
foreach ($CAS in $CASs){
write-host ("`n" + $CAS) ; 
.\logparser.exe "SELECT cs(User-Agent) as Client, count(*) as Hits INTO $CAS-EWS-ALLClients.csv FROM '\\$CAS\e$\Weblogs\W3SVC1\ex130919*.log' WHERE cs-uri-stem LIKE '%/EWS/%' GROUP BY Client ORDER BY Hits DESC" -o:csv 

Powershell Script - Comment-Based Help Template (Powershell v2)

9/20/2013: Another frequently useful, but occasionally fiddly, feature of Powershell (as of revision 2), is the Comment-Based Help system. The features intention is to make it quick & easy to include the proper values within your script to provide self-assembling get-help output, to make your script as well-documented for other users, as the stock Powershell Cmdlets.

Unfortunately, the area that most frequently breaks when trying to use what I'll refer to as 'CBH', is in the specifics of formatting and placement and exactly which of the various options you may want to include in most of your scripts.

Like a lot of folks I spent some time poking around in the docs and on the web, and figured out the details to make things function, and generally distilled that material down into a simple boilerplate 'Template' that I use within in every new script I write. And since my goal is is to 'share' with the world, I figured I'd post up my template, along with some useful notes on keeping it functioning trouble-free. :)

Todd's Comment-Based Help Template

#*----------V Comment-based Help (leave blank line below) V---------- 

[NAME].ps1 - [1-LINE-DESC]

Written By: Todd Kadrie

Additional Credits: [REFERENCE]
Website: [URL]
Twitter: [URL]

Change Log


    .PARAMETER  <Parameter-Name>
<parameter comment>

None. Does not accepted piped input.

None. Returns no objects or output.
                True if the current Powershell is elevated, false if not.
[use a | get-member on the script to see exactly what .NET obj TypeName is being returning for the info above]

[use an .EXAMPLE keyword per syntax sample]

< name of a related topic, one .LINK keyword per related topic. Can also include a URI to online help (start with http:// or https://)>
*----------^ END Comment-based Help  ^---------- #>

Comment-Based Help Notes:

  • ALWAYS INDENT KEYWORDS WITH SPACES NOT TABS: For Keywords, the period must be the first character on the line, excepting spaces or # (hashmark).

  • A blank line must appear between any unrelated leading comments, and the start of the CBH <#...#> block:
  • Within Comment-Blocks (<#...#>) a .KEYWORD must be the first item on the line immediately below the opening <# 

Powershell Script - Tip: Delimited String "Contants" & Multi-server Logparser scripts

9/19/2013: This is a useful tip that I get some solid routine mileage out of: Using a delimited string variable, to function as an Array Constant. Primary usage is when I want to loop through a set of variant values, and execute the same commands against each value.

In the code below (which returns some sample sorted information on the various Exchange versions, and roles) I'm setting up a $Sites variable, containing a semi-colon-delimited string, that is then split and reassigned to the variable as an Array. The resulting array is then fed through a simple foreach loop to output information about the servers and roles in each AD Site.

Array Stored as Semi-colon-delimited string. 

Language/Tools: Powershell
Target: Exchange 2007/2010 Management Console

$Sites = "US;AU;EU" ; 
$Sites=$Sites.split(";") ; 
foreach ($Site in $Sites) {
  write-host -foregroundcolor yellow "SITE: " $Site; get-exchangeserver | 
    where { $_.Site -like "*$Site" } | 
    sort AdminDisplayVersion,ServerRole,Name | 
    select Name,ServerRole,AdminDisplayVersion 

Typical output:

Name                                                                 ServerRole AdminDisplayVersion
----                                                                 ---------- -------------------
US-ServerUM1                                                   UnifiedMessaging Version 14.2 (Build 247.5)
US-MbxServer2                                                           Mailbox Version 14.3 (Build 123.4)
US-MbxServer2                                                           Mailbox Version 14.3 (Build 123.4)
US-HUBCAS1                                           ClientAccess, HubTransport Version 14.3 (Build 123.4)
US-HUBCAS2                                           ClientAccess, HubTransport Version 14.3 (Build 123.4)
US-PFServer1                                                            Mailbox Version 8.3 (Build 83.6)
US-MbxServer1                                                           Mailbox Version 8.3 (Build 83.6)

Multi-server Logparser scripts (using Delimited-String Constants):

The above is a simple contrived example to get the concept across. More frequently I use delimited-variable strings for storing and looping through arrays of log directories (for get-messagetracking or logparser processing). For example: Below I have a script that runs a Microsoft Logparser query (summarizing hourly EWS hits) from the IIS logs matching today's date -- pretty common stuff.
But this version has the added benefit of executing the command against a series of different CAS servers (by walking a delimited-string constant containing the UNC paths to each server's logs)...

Logparsing multiple CAS server IIS log directories for EWS traffic per hour. 

Language: Powershell /MS LogParser
Target: EMS or Powershell, parsing Exchange 2007/2010 CAS Server IIS logs

# Array of IIS paths, stored as semi-colon-delimited string
$LogDirs="\\US-HUBCAS1\e$\Weblogs\W3SVC1\;\\US-HUBCAS2\e$\Weblogs\W3SVC1\" ; $LogDirs=$LogDirs.split(";") ; 
# constants for single & double quotes
$sQuot = [char]34 ; $sQuotS = [char]39
foreach ($LogDir in $LogDirs) {
  # build the log path to today's logs
  $sTodaysLogsStr = $LogDir
  # Ex2007 IIS log filename variant
  #$sTodaysLogsStr += "ex" 
  # Ex2010 variant
  $sTodaysLogsStr += "u_ex" 
  $sTodaysLogsStr += (Get-Date -format "yMMdd") + "*.log"
  # build the logparser sql command
  $strSQL =  "SELECT QUANTIZE(TO_LOCALTIME(time),3600) AS Hour, count(*) AS Hits" ;    
  $strSQL += " FROM " + ($sQuotS + $sTodaysLogsStr + $sQuotS) 
  $strSQL += " WHERE cs-uri-stem LIKE '%/EWS/%' GROUP BY Hour ORDER BY Hour" ; 
  # assemble the logparser commandline
  $strExecCmd = ". logparser.exe " + $sQuot + $strSQL + $sQuot + " -rtp:-1" ;
  # execute the assembled command
  Invoke-Expression  $strExecCmd 
Which produces two passes of Logparser output like so:
[PS] C:\usr\local\bin>c:\tmp\tmp1.ps1
. logparser.exe "SELECT QUANTIZE(TO_LOCALTIME(time),3600) AS Hour, count(*) AS Hits FROM '\\US-HUBCAS1\e$\IIS Weblog\W3SVC1\u_ex130920*.log' WHERE cs-uri-stem LIKE '%/EWS/%' GROUP BY Hour ORDER BY Hour" -rtp:-1
Hour     Hits
-------- ----
00:00:00 477
01:00:00 2289
02:00:00 2809
03:00:00 3300
04:00:00 3655
05:00:00 5657
06:00:00 7350
07:00:00 9135
08:00:00 9340
09:00:00 9053
10:00:00 8615
11:00:00 8894
12:00:00 6568
18:00:00 1132
19:00:00 758
20:00:00 749
21:00:00 612
22:00:00 661
23:00:00 571

Elements processed: 392382
Elements output:    19
Execution time:     12.12 seconds

. logparser.exe "SELECT QUANTIZE(TO_LOCALTIME(time),3600) AS Hour, count(*) AS Hits FROM '\\US-HUBCAS2\e$\IIS Weblog\W3SVC1\u_ex130920*.log' WHERE cs-uri-stem LIKE '%/EWS/%' GROUP BY Hour ORDER BY Hour" -rtp:-1
Hour     Hits
-------- -----
00:00:00 3205
01:00:00 2979
02:00:00 3133
03:00:00 3554
04:00:00 3833
05:00:00 5564
06:00:00 9284
07:00:00 10352
08:00:00 10016
09:00:00 9733
10:00:00 9869
11:00:00 10096
12:00:00 7526
18:00:00 5183
19:00:00 3445
20:00:00 2850
21:00:00 3447
22:00:00 3367
23:00:00 3387

Elements processed: 495761
Elements output:    19
Execution time:     14.72 seconds

Wednesday, September 18, 2013

Powershell - The Big All-in-One All Exchange 2010 DAG Mailboxes in a Site Query

9/18/2013: Here's a one-line Exchange Management Shell Powershell command that will retrieve useful attributes on all Exchange 2010 DAG-hosted mailboxes in the specified site.

Describing the process used by the command:
  1. Retrieve all exchange servers, that are Mailbox Servers, in the designated AD Site,with Exchange version 14 (Exchange 2010)
  2. Then retrieve all databases on those servers, where the ReplicationType attribute is 'Remote' (e.g. DAG replicated)
  3. Then retrieve all mailboxes in those databases, 
  4. And return a list of specific attributes of the mailbox, including a custom field that encapulates a semi-colon-delimited joined string created from all EmailAddresses of the mailbox. 
  5. And finally, export that collection of data to a csv file.
# one-line follows
get-exchangeserver | 
where { $_.IsMailboxServer -eq $true -AND $_.Site -like '*SiteA*' -AND $_.admindisplayversion.major -eq 14 } | 
get-mailboxdatabase | 
where {$_.ReplicationType -eq "Remote"} | 
get-mailbox -resultsize unlimited | 
select samaccountname,DisplayName,Alias,
@{Name='Addresses';Expression={[string]::join(";", ($_.EmailAddresses))}},
Guid,ObjectCategory,WhenChanged,WhenCreated |export-csv .\Ex2010-mbx-users.csv -notype 

Powershell - Test a file list of mailboxes for Outlook Latency

9/18/2013: Another quicky broad troubleshooting script. This one is aimed at quickly answering those, "Users are reporting slow Outlook Response. Is there an issue?", helpdesk escalations.

Run Test-MAPIConnectivity against a list of mailboxes and report access latency

# one line follows...
get-content .\list.txt |
get-mailbox |
Test-MAPIConnectivity  |
select Server, Database,Mailbox, Result,
@{label="Latency(ms)";expression={($_.Latency.Milliseconds)}} |
ft -auto | out-default

Typical output:
Server     Database Mailbox      Result  Latency(ms)
------     -------- -------      ------  -----------
SRVR-MAIL1 Mdb01    TestU1, User Success          70

Powershell - Test all mailboxes in a store

9/18/2013: This is another item I wrote a few years ago to tackle testing of Content Indexing and Exchange Search function, on an Exchange 2007 CCR cluster that was experiencing drive I/O issues, leading to sporadic problems with the update speed and function of the Content Indexing processes:

Test all mailboxes in a store (in this case using the test-exchangesearch cmdlet)

Two variants...

# database is identified using -database parameter 
#   and the db's Identity string
get-mailbox -database Server1\Sg1\Db1 |
foreach {
  write-host $_.Alias; (test-exchangesearch $_ |
  select ResultFound,SearchTime | 
  ft -auto)
Typical output:

ResultFound SearchTime
----------- ----------
False       -1

# Run Test with a one-per-line output reporting just Alias & SearchTime:
get-mailbox -database Server1\SG1\DB1 |
foreach { `
  write-host ($_.Alias + " ") -NoNewline  ;
  (test-exchangesearch $_).SearchTime ;
Typical output:
WalkerTest 101
TestMailbox 115

Powershell - Draw a random mailbox for testing

9/18/2013: Here's something I make use of for quick-evaluating mailbox server health, immediately after a switchover/failover or returning a system to online status after a maintenance outage:

Draw a random (non-monitoring) mailbox for testing:
# Assign the target database identity string to a variable
$TargDB = "Server1\Sg1\Db1"
# Retrieve all matching mailboxes,
#   that do not contain a distictinve DisplayName string
#   that indicates a monitoring-type mailbox
#   (in this case '-NOC-' for 'Network Operations Center') .
$Mailboxes = Get-Mailbox -Database $TargDB -ResultSize "Unlimited" |
Where-Object {$_.DisplayName -notlike "-NOC-*"}
# initate a randomizer & retrieve an integer between
#   0 and retrieved mailboxes count, less one
$iRandMbx = New-Object System.Random
$iRandMbx = $iRandMbx.Next(0, ($Mailboxes.Count - 1))
# perform some task with the matching mailbox;
#   in this case, run a test on Exchange Search
#   function for the mailbox.
write-host ("Random Mbx: Name:'"+($Mailboxes[$iRandMbx])+"', Acct:'"+($Mailboxes[$iRandMbx]).samaccountname + "'")
write-host "Run: " test-exchangesearch -Identity $Mailboxes[$iRandMbx]
test-exchangesearch -Identity $Mailboxes[$iRandMbx]
Typical Output:
Random Mbx: Name:'Walker, Test', Acct:'WalkerTest'
Run:  test-exchangesearch -Identity Walker, Test

                            ResultFound                              SearchTime
                            -----------                              ----------
                                   True                                     115

Note:I'd be remiss in the above, if I didn't point out that another simple way exists to draw a random object in Powershell V2+, without relying on the  System.Random object used in the code above: You can make use of the new get-random cmdlet. Feed it some form of array of values, or a range, and it will draw one at random:

Get-MailboxDatabase | where {$_.ExchangeVersion.ExchangeBuild.Major -eq 14}| get-random | get-mailbox | get-random

But, in spite of a good alternative being available, I still tend to use the older object, because I frequently need my code to run on a variety of revisions of Powershell, and Exchange, and a decent 'functional' if older alternative is often less trouble and more dependable in the long run than the latest and greatest options. :D

Powershell - Report on Distribution Groups with AcceptMessageOnlyFrom set

9/18/2013: My day job is as a Messaging & Collaboration admin, running 33,000 mailboxes for Hexaware/Unisys. And because I spend most of my day working with Exchange 2010 & 2007, I make a ton of routine use of Powershell, to automate routine tasks, multitask, and generally get more done in less time. The way I look at it, anything I need to do more than once or twice, can probably benefit from taking a little time to code it out and automate it Powershell.

And since I've gotten tons of useful tips and scripting examples from folks like Paul Cunningham  and Shay Levy, I've decided to give back some tips & code-bits to help other folks quickly tackle their own admin needs with Powershell & Exchange Management Shell.

Report on Distribution Groups with AcceptMessageOnlyFrom set
Toward that end, here's a piece of code I put together some time ago, to pull out all Distribution Groups with the AcceptMessagesOnlyFrom, and then concatonate together the AcceptMessagesOnlyFrom strings, and export the results to CSV file.

Get-DistributionGroup -filter { AcceptMessagesOnlyFrom -ne $null} | 
select Name,@{Name="AcceptMessagesOnlyFrom";Expression={ `
[string]::join(";",$_.AcceptMessagesOnlyFrom) } } |
export-csv .\dlacceptmessages.csv -notype

Typical output csv contents: (heading and first line)
"X Domain Admins","OU/United States/Users/Blow, Joe;OU/United States/Users/Blow, Josie"

The components of the command used are a simple Get-DistributionGroup command leveraging the -filter parameter to retrieve matches, piped into a Select command that retrieves the Name attribute and a custom field that joins the multiple AcceptMessageOnlyFrom strings into a single semi-colon-delimited string. The export-csv then outputs the Name & AcceptMessagesOnlyFrom values to the specified CSV file.