Retrieve SQL Database and Transaction Log File Sizes using Get-Counter

This is a nice little script that I used to find the size of all of the SQL server database (MDF) and Transaction Log (LDF) files on each SQL server that we have. This stems from a task I had to write a script that could find out just what the sizes are of each database and each Transaction Log file we had on our network.

At first I thought about using the SMO assemblies into PowerShell and then creating my own objects to query each SQL server for the information. But then I thought that not every client in my office has the SQL Administrator tools loaded on each machine in order to run the script.  Instead, I did some research on using performance counters to possibly pull this information.  And with that, I was able to find exactly what I was looking for.  Using the Get-Counter cmdlet, I was able to to query for a select counter and the parse the information to display the file sizes.  Since I am using performance counters, the actual location of the files will not be shown.  But for me, this was OK as all I needed was the sizes.

The Advanced Function that I wrote is Get-SQLFileSize and has an option to query one or more computers and display the MDF, LDF or both types of files along with their size in Megabytes.

Get-SQLFileSize -comp dc1

Computer                      Database                      FileType                                            Size_MB
——–                      ——–                      ——–                                            ——-
dc1                           susdb                         MDF                                                  391.36
dc1                           tempdb                        MDF                                                   8.192
dc1                           msdb                          MDF                                                   4.992
dc1                           model                         MDF                                                   1.216
dc1                           mssqlsystemresource           MDF                                                   39.36
dc1                           master                        MDF                                                   4.096
dc1                           susdb                         LDF                                                   5.176
dc1                           tempdb                        LDF                                                   7.608
dc1                           msdb                          LDF                                                   3.448
dc1                           model                         LDF                                                   0.504
dc1                           mssqlsystemresource           LDF                                                   0.504
dc1                           master                        LDF                                                   1.016

 

You can easily send the output to a CSV file using the Export-CSV cmdlet and send it via email as a report.

Note that there are possibilities where the counters used are inaccessible for one reason or another.  If this is the case, then the function will not work as designed.

Code

Script Repository

PoshCode

Function Get-SQLFileSize { 
<#   
.SYNOPSIS   
    Retrieves the file size of a MDF or LDF file for a SQL Server 
.DESCRIPTION 
    Retrieves the file size of a MDF or LDF file for a SQL Server 
.PARAMETER Computer 
    Computer hosting a SQL Server 
.NOTES   
    Name: Get-SQLFileSize 
    Author: Boe Prox 
    DateCreated: 17Feb2011         
.EXAMPLE   
Get-SQLFileSize -Computer Server1 
 
Description 
----------- 
This command will return both the MDF and LDF file size for each database on Server1 
.EXAMPLE   
Get-SQLFileSize -Computer Server1 -LDF 
 
Description 
----------- 
This command will return LDF file size for each database on Server1 
Description 
----------- 
This command will return both the MDF and LDF file size for each database on Server1 
.EXAMPLE   
Get-SQLFileSize -Computer Server1 -MDF 
 
Description 
----------- 
This command will return MDF file size for each database on Server1 
 
#>  
[cmdletbinding( 
    DefaultParameterSetName = 'Default', 
    ConfirmImpact = 'low' 
)] 
    Param( 
        [Parameter( 
            Mandatory = $True, 
            Position = 0, 
            ParameterSetName = '', 
            ValueFromPipeline = $True)] 
            [string[]]$Computer, 
        [Parameter( 
            Mandatory = $False, 
            Position = 1, 
            ParameterSetName = '', 
            ValueFromPipeline = $False)] 
            [switch]$Mdf, 
        [Parameter( 
            Mandatory = $False, 
            Position = 2, 
            ParameterSetName = '', 
            ValueFromPipeline = $False)] 
            [switch]$Ldf                         
        ) 
Begin { 
    If (!($PSBoundParameters.ContainsKey('Mdf')) -AND !($PSBoundParameters.ContainsKey('Ldf'))) { 
        Write-Verbose "MDF or LDF not selected, scanning for both file types" 
        $FileFlag = $True
        $Flag = $False 
        } 
    #Create holder for data 
    Write-Verbose "Creating holder for data" 
    $report = @()
    } 
Process {     
    ForEach ($comp in $Computer) { 
        #Check for server connection 
        Write-Verbose "Testing server connection" 
        If (Test-Connection -count 1 -comp $comp -quiet) { 
            If ($PSBoundParameters.ContainsKey('Mdf') -OR $FileFlag) { 
                Write-Verbose "Looking for MDF file sizes"  
                    Try { 
                        Write-Verbose "Attempting to retrieve counters from server" 
                        $DBDataFile = Get-Counter -Counter '\SQLServer:Databases(*)\Data File(s) Size (KB)' -MaxSamples 1 -comp $comp -ea stop 
                        $DBDataFile.CounterSamples | % { 
                            If ($_.InstanceName -ne "_total") { 
                                $temp = "" | Select Computer, Database, FileType, Size_MB 
                                $temp.Computer = $comp 
                                $temp.Database = $_.InstanceName 
                                $temp.FileType = 'MDF' 
                                $temp.Size_MB = $_.CookedValue/1000 
                                $report += $temp 
                                } 
                            } 
                        } 
                    Catch { 
                        $Flag = $True 
                        }
                    If ($Flag) {                 
                        Try { 
                            Write-Verbose "Attempting to retrieve counters from server" 
                            $DBDataFile = Get-Counter -Counter '\MSSQL$MICROSOFT##SSEE:Databases(*)\Data File(s) Size (KB)' -MaxSamples 1 -comp $comp -ea stop 
                            $DBDataFile.CounterSamples | % { 
                                If ($_.InstanceName -ne "_total") { 
                                    $temp = "" | Select Computer, Database, FileType, Size_MB 
                                    $temp.Computer = $comp 
                                    $temp.Database = $_.InstanceName 
                                    $temp.FileType = 'MDF' 
                                    $temp.Size_MB = $_.CookedValue/1000 
                                    $report += $temp 
                                    } 
                                }             
                            } 
                        Catch { 
                            Write-Warning "$($Comp): Unable to locate Database Counters or Database does not exist on this server"
                            Break
                            }
                        }
                    Else {
                        Write-Warning "$($Comp): Unable to locate Database Counters or Database does not exist on this server"
                        Break
                        } 
                } 
            If ($PSBoundParameters.ContainsKey('Ldf') -OR $FileFlag) {  
                Write-Verbose "Looking for LDF file sizes"                
                    Try { 
                        Write-Verbose "Attempting to retrieve counters from server" 
                        $DBDataFile = Get-Counter -Counter '\SQLServer:Databases(*)\Log File(s) Size (KB)' -MaxSamples 1 -comp $comp -ea stop 
                        $DBDataFile.CounterSamples | % { 
                            If ($_.InstanceName -ne "_total") { 
                                $temp = "" | Select Computer, Database, FileType, Size_MB 
                                $temp.Computer = $comp 
                                $temp.Database = $_.InstanceName 
                                $temp.FileType = 'LDF' 
                                $temp.Size_MB = $_.CookedValue/1000 
                                $report += $temp 
                                } 
                            } 
                        } 
                    Catch { 
                        $Flag = $True  
                        }
                    If ($flag) {                 
                        Try { 
                            Write-Verbose "Attempting to retrieve counters from server" 
                            $DBDataFile = Get-Counter -Counter '\MSSQL$MICROSOFT##SSEE:Databases(*)\Log File(s) Size (KB)' -MaxSamples 1 -comp $comp -ea stop 
                            $DBDataFile.CounterSamples | % { 
                                If ($_.InstanceName -ne "_total") { 
                                    $temp = "" | Select Computer, Database, FileType, Size_MB 
                                    $temp.Computer = $comp 
                                    $temp.Database = $_.InstanceName 
                                    $temp.FileType = 'LDF' 
                                    $temp.Size_MB = $_.CookedValue/1000 
                                    $report += $temp 
                                    } 
                                }             
                            } 
                        Catch { 
                            Write-Warning "$($Comp): Unable to locate Database Counters or Database does not exist on this server"
                            Break
                            }
                        }
                    Else {
                        Write-Warning "$($Comp): Unable to locate Transaction Log Counters or Database does not exist on this server"
                        Break
                        } 
                    } 
            } 
        Else { 
            Write-Warning "$($Comp) not found!" 
            }                
        }         
    } 
End { 
    Write-Verbose "Displaying output" 
    $report 
    }                 
}
Posted in powershell, scripts | Tagged , , | 3 Comments

Microsoft Community Contributor Award | 2011

On Friday afternoon I was checking my email when I came across an email from support@microsoftcommunitycontributor.com saying that I was being awarded for my contributions to the Microsoft community.

Dear Boe,
Congratulations! We’re pleased to inform you that your contributions to Microsoft online technical communities have been recognized with the Microsoft Community Contributor Award.
The Microsoft Community Contributor Award is reserved for participants who have made notable contributions in Microsoft online community forums such as TechNet, MSDN and Answers. The value of these resources is greatly enhanced by participants like you, who voluntarily contribute your time and energy to improve the online community experience for others.

Needless to say, I am honored that I would be considered, let alone actually receiving this award. I appreciate everyone who nominated me for this and awarding this to me. I hope that my blogs and contributions continue to help everyone out in one way or another with PowerShell.

Thank you!

MCC11_Social-Media_Logo

Posted in News, powershell | Tagged , , , | 3 Comments

Creating a Report of Log File Data using Regular Expressions, Arrays and Hash Table

As I was checking my email early in the morning, I saw an email from co-worker asking me to write a script to parse through some logs that were generated from another script that listed the date the script is ran each day. In short, the script checks the network connectivity of some systems. If the system is up, nothing else happens, otherwise the system name is written to the log. My task was to go through each of the log files for the past week and month and grab each hostname and find out how many times the system was showing as down. Simple enough I thought, I can whip up a PowerShell script and go through those logs in a quick amount of time and use a hash table to collect the data I needed.

Here is an example of the logfile (the hostnames have been changed to protect the innocent):

Current Date is 02/25/2011 20:09:46

TEST1-568956
TEST1-568923

Current Date is 02/25/2011 20:09:46

TEST2-895645
TEST2-568956

For the sake of testing at home, I wrote a small script to create some log files to match what I needed to work with.

Code

$workstations = @('TEST1-788989','TEST1-568923','TEST1-568956','TEST1-568956')
$workstations1 = @('TEST2-568956','TEST2-23589','TEST2-895645','TEST2-56895623')
1..5 | % {
@"
Current Date is $((Get-Date).AddDays($_))

$($workstations | Get-Random)
$($workstations | Get-Random)

Current Date is $((Get-Date).AddDays($_))

$($workstations1 | Get-Random)
$($workstations1 | Get-Random)
"@ | Out-File "log$($_).txt"
}

This uses a Here-String to generate the body for each log file. Now that I have my log files, I needed to begin the process of being able to collect all of the log files before parsing through them for the information I needed.

For my code, I decided to use a hash table since all I was looking for was the workstation and the number of times it was down based on the log files. I create an empty hash table using the following code:

$hash = @{}

I also enlist the use of a Switch statement using the –regex and –file to parse through the logs for some specific values, namely the workstation name. Using –file will allow you to supply a filename to read through it line by line and parse through the lines for the information required. This is different than using the Get-Content Cmdlet as it only reads one line at a time rather than using Get-Content to grab all of the contents of the file and store it into memory.  Think about grabbing all of the contents of a 1GB file using Get-Content. That is not only going to be slow, it will also bog down your system.

Using the –regex allows me to perform a regular expression match of a specific item in each line that I am going through on the log file. I did write a post on regular expressions here. Since I know that the workstations follow a specific naming convention of “Test-1” and “Test-2”, I am figure out the regular expression that I need to use for each switch statement to locate the workstations. I can use “^Test-1\w+” and “^Test-2\w+”.

Once I find a workstation, I then check to see if the workstation name already exists in my hash table and if it doesn’t exist, adds the workstation along with a counter of 1 to show that this is the first instance of that system being down. Otherwise, the hash counter will be incremented by 1. The $_ represents the workstation name that matched the regular expression that was used to locate that specific workstation name.

If ($hash.ContainsKey ($_) {
	$hash[$_]++
	}
Else {
	$hash.add($_,1)
	}

When it was all said and done, I also had to send an email to some customers listed the contents of the hash table. The problem with a hash table is that you if you are going to write it out as a string using Write-Host or sending it in an email is that when you try to do the following things, it will not come out quite like you expect it to…

Write-Host $hash
Write-Host $hash.GetEnumerator()
Write-Host $($hash)
Write-Host $($hash.GetEnumerator())

Capture

A whole lot of formatting types come out. Not exactly what I call reportable material. Using the following code will give me the proper format that I am looking for.

Write-Host $($hash.GetEnumerator() | Out-String)

I wrap the commands in a $() so the expression can be evaluated when I call it with the Write-Host cmdlet and the Out-String Cmdlet converts the output of the GetEnumerator()   method (which is used to get all of the entries) into a string format.

Capture

 

Code

<#  
.SYNOPSIS  
    Parses the client status log files to report down statuses for a month
.DESCRIPTION
    Parses the client status log files to report down statuses for a month. 
    Email is then sent to users listing report
.NOTES  
    Name: Status_Report.PS1
    Author: Boe Prox
    DateCreated: 24Feb2011        

#> 
Begin {
    #Create header dates
    $begin = ((Get-Date).AddMonths(-1)).Toshortdatestring()
    $today = (Get-Date).Toshortdatestring()
    
    #Create empty hash table
    $hash = @{}
    
    #Gather logs
    $logs = GCI -Filter *.txt
    }
Process {
    #Iterate through each log
    ForEach ($log in $logs) {
        Write-Verbose "$($log.FullName)"
        Switch -regex -file $log.FullName {
            "^Current\sDate/Time:\s\w+" {
                Write-Verbose "Date Run: $($i)"
                }
            "^TEST1-\w+" {
                If ($hash.ContainsKey($_)) {
                    #Increment value
                    $hash[$_]++
                    }
                Else {
                    #Add to hash table
                    $hash.Add($_,1)
                    }               
                }
            "^TEST2-\w+" {
                If ($hash.ContainsKey($_)) {
                    #Increment value
                    $hash[$_]++
                    }
                Else {
                    #Add to hash table
                    $hash.Add($_,1)
                    }            
                } 
            Default {
                Write-Verbose "Blank Space"
                }       
            }
        }
    }
End {
    #Write header
    Write-Host "Down systems from $begin to $today"
    Write-Output $hash
    
    Send-MailMessage -to group@email.com `
        -From group@email.com `
        -smtp server.server.com `
        -Subject "Status from $begin to $today" `
        -Body "Down systems from $begin to $today `r
$(($hash.GetEnumerator() | Select @{Label = 'Workstation';Expression = {$_.Name}}, @{Label = 'Days_Down';Expression = {$_.Value}}) | out-string)"
    }

Running the script presented the following output and also sent an email to the users requesting this information:

Down systems from 1/26/2011 to 2/26/2011

Name                                 Value                                                     
—-                                       —–                                                     
TEST2-56895623                 2                                                         
TEST1-568923                   4                                                         
TEST1-568956                   5                                                         
TEST2-23589                    2                                                         
TEST1-788989                   1                                                         
TEST2-895645                   3                                                         
TEST2-568956                   3
  

Content that everything was good after sending the finished product, I went on to work on other items. Fast-forward an hour or so and I am forwarded an email asking if the actual dates can also be included in the report. I’m thinking to myself that it should be doable, I just have to re-think how I was going to accommodate that request. Crazy thoughts such as trying to merge a hash table, combining a hash table with an array among other things began to dance in my head. So before continuing on this downward spiral, I decided to take a break and return to this later on. a few hours later, I finally came up with my solution using a combination of a hash table and then creating a report using an array.

 

Code

Begin {
    $logs = GCI -Filter *.txt
    $hash = @{}
    $report = @()
    [regex]$regex = "\d{2}/\d{2}/\d{4}\s\d{1,2}:\d{2}:\d{2}"
    }
Process {
    ForEach ($log in $logs) {
        Switch -regex -file $log.fullname {
            "\d{2}/\d{2}/\d{4}\s\d{1,2}:\d{2}:\d{2}" {
                $date = $regex.matches($_) | Select -ExpandProperty Value
                }
            "^TEST1-\w+" {
                If ($hash.contains($_)) {
                    $hash[$_] += $date
                    }
                Else {
                    $hash.Add($_,@($date))
                    }
                }
            "TEST2-\w+" {
                If ($hash.contains($_)) {
                    $hash[$_] += $date
                    }
                Else {
                    $hash.Add($_,@($date))
                    }           
                }
            }
        }
    ForEach ($key in $hash.keys) {
        $temp = "" | Select Workstation, DaysDown, DatesDown
        $temp.Workstation = $key
        $temp.DatesDown = $hash[$key]
        $temp.DaysDown = $hash[$key].count  
        $report += $temp  
        }
    }
End {
    Write-Output $report
    }

The output for the script is shown below:

Capture

As you can see, there are a lot of similarities between this and my original code. I am using the same type of parsing and am also parsing the date this time as that is now needed for the report and still using a hash table to save the workstation name. To parse the date using regular expressions, I decided on a different approach using the following regular expression:  “\d{2}/\d{2}/\d{4}\s\d{1,2}:\d{2}:\d{2}”  which will grab the following date format: 01/12/2011 12:12:23.  Instead of incrementing a counter for the number of days down, I have created an array of the dates that the workstation was shown as being offline. 

I wanted to make sure that the hash table was going to show an array of the dates, so I declared the array along with the creation of the hash table using the following code:

$hash.Add($_,@($date))

The $_ represents the workstation name and the $date represents the date that was parsed from the log file.  Now that the array has been declared within the hash stable, the next time that I pull the same workstation, it will add the new date to the array within the hash table using the following line of code:

$hash[$_] += $date

Again, the $_ represents the workstation value retrieved from using the regular expression and the $date shows the date.  What I did was call the key in the hash table for the workstation which allows me to then add the date to the array using the += operator. Had I not declared the Value of the hash table as an array, it would have just appended the date to the existing value instead of adding it to the collection.

 

Since this was a report being emailed out to some users, I need to convert this information over to a CSV file and then send it out as an attachment. I decided to create a tab delimited file as using the Export-CSV will not work correctly. If I use the Export-CSV Cmdlet, everything would come out correctly with the exception of the DatesDown portion and will just show you what type is residing. Instead I looped through the hash table and wrote the data to a Tab-Delimited file. The final code I used for that is below:

Code

<#  
.SYNOPSIS  
    Parses the  client status log files to report down statuses for a month
.DESCRIPTION
    Parses the  client status log files to report down statuses for a month. 
    Email is then sent to users listing report
.NOTES  
    Name: ClientStatus_Report.PS1
    Author: Boe Prox
    DateCreated: 24Feb2011        

#> 
Begin {
    #Create header dates
    Write-Verbose "Creating date strings"
    $begin = ((Get-Date).AddMonths(-1)).Toshortdatestring()
    $today = (Get-Date).Toshortdatestring()
    
    #Create empty hash table
    Write-Verbose "Creating hash table"
    $hash = @{}
    $report = @()
    
    #Create regular expression to parse dates
    [regex]$regex = "\d{2}/\d{2}/\d{4}\s\d{1,2}:\d{2}:\d{2}"
    
    #Create header for tab-delimited file
    $logfile = 'client_status.csv'
    "Workstation`tDaysDown`tDatesDown" | Out-File $logfile
    
    #Gather logs
    Write-Verbose "Gathering log files"
    $logs = GCI *.txt
    }
Process {
    #Iterate through each log
    ForEach ($log in $logs) {
        Write-Verbose "$($log.FullName)"
        Switch -regex -file $log.FullName {
            "\d{2}/\d{2}/\d{4}\s\d{1,2}:\d{2}:\d{2}" {
                Write-Verbose "Date Run: $($i)"
                
                #Parse date for log file and report
                [datetime]$Date = $regex.matches($_) | Select -ExpandProperty Value
                [string]$date = $date.ToShortDateString()
                $date = $date.Replace("\d{1,2}:\d{2}:\d{2}\s\w{2}","")
                }
            "^Test1-\w+" {
                If ($hash.ContainsKey($_)) {
                    Write-Verbose "$($_) already in table"
                    #Increment value
                    $hash[$_] += $date
                    }
                Else {
                    Write-Verbose "Adding $($_) to table"
                    #Add to hash table
                    $hash.Add($_,@($date))
                    }               
                }
            "^Test2-\w+" {
                If ($hash.ContainsKey($_)) {
                    Write-Verbose "$($_) already in table"
                    #Increment value
                    $hash[$_] += $date
                    }
                Else {
                    Write-Verbose "Adding $($_) to table"
                    #Add to hash table
                    $hash.Add($_,@($date))
                    }            
                } 
            Default {
                Write-Verbose "Blank Space"
                }       
            }
        }
        #Begin processing the values in the hash table to convert into Array
        ForEach ($key in $hash.keys | Sort) {
            Write-Verbose "Formatting dates prior to adding into CSV"
            #Format the dates to make easier to read
            If ($hash[$key] -is [array]) {
                $value = "$([string]::Join('; ',$hash[$key]))"
                }
            Else {
                $value = "$($hash[$key])"
                }
            Write-Verbose "Adding to log file"
            "$($key)`t$($value); `t$($hash[$key].count)" | Out-File -Append $logfile
            $temp = "" | Select Workstation, DaysDown, DatesDown
            $temp.Workstation = $key
            $temp.DatesDown = $hash[$key]
            $temp.DaysDown = $hash[$key].count  
            $report += $temp  
            }
    }
End {
    Write-Verbose "Displaying Report"
    Write-Host " Client Status from $begin to $today"
    Write-Output $Report | Sort Workstation | FT -auto
    Write-Verbose "Sending Email notification" 
    <# 
    Send-MailMessage -to email@email.com `
        -From email@email.com `
        -smtp '<server>' `
        -Subject " Client Status from $begin to $today" `
        -att $log `
        -BodyAsHTML "<html> Please review the attached file listing down systems from <b> $begin </b> to <b> $today </b> </html>"
    #>
    #Remove Log file
    Write-Verbose "Removing log file"
    #Remove-Item $logfile -Force
    }

Capture

I decided to append a “;” after each date, to include the cases where I only had one date so when a user would open up the file, all of the dates would be aligned to the left instead of being all the way to the right.

Using this code, I can then email the CSV file as an attachment to the customers. This same type of process can easily be used to create a report for your Windows Firewall logs, Windows Update logs or any other log that had data in it that you want to report on.  The possibilities are pretty much endless!

This just shows again what PowerShell is able to do to save a lot of manual effort going through logs to generate a report!

Posted in powershell, scripts | Tagged , , , , , | 5 Comments

Querying UDP Ports with PowerShell

It was brought to my attention earlier in the week that my Test-Port script had a fairly big bug involving checking the UDP port to determine whether it was open or not. Initially, the function would always come back with a True statement giving a false positive that the port was open, when in fact it was not. In the case of the bug report, the system in question was actually turned off, making it even more of a false positive. The reason behind this is that sending out a message or request via UDP does not guarantee that a response will be given back, unlike TCP where you will know pretty quickly if the port is open or closed. More information here, here and here.

So with that, I set off to find a good way of querying for an open UDP port.  I immediately went to the MSDN page for the System.Net.Sockets.UDPClient to see what methods were available to me. I also took note of the example that was given for making a UDP connection and receiving a message from that port. Since this was written in C#, I knew I could easily translate this into PowerShell (and in fact I left a community contribution on the page).

There are some gotchas  here that I will share with you that I came across while working on this. I can tell you that one of these caused me some pain as it made the executed code hang forever.

The first thing we need to do is create the UDPClient object. Using a constructor, I am opening the local UDP port on my laptop using port 11000:

$udpobject = new-Object system.Net.Sockets.Udpclient(11000)

Now that I have my object created, I can now proceed to set up my message string that will be sent along with actually sending out the data. The trick here with sending out a message via the UDPclient is that it must be in bytes. So while I supply a string message to send, it must be converted into a byte format.  This is done by using the System.Text.ASCIIEncoding class.  For this message, I will just send a date string to the remote server.

$a = new-object system.text.asciiencoding
$byte = $a.GetBytes("$(Get-Date)")

Capture

Ok, now lets create the “connection” to the remote server using the Connect() method. This method requires us to supply a hostname and port to connect on. I say “connection” because even if the port isn’t open, it will still act like a connection was made. You will only receive an error if the host does not exist, or is not on the network.

$udpobject.Connect("dc1",11000)

Now lets send the message to the remote host. Again, just like making the connection, we will not receive any sort of response if the send was successful or not. By doing this, we are forcing some sort of response by the port, either it will receive the data, or we will receive an error stating the port was closed.

[void]$udpobject.Send($byte,$byte.length)

After sending the data, we now need to setup the receiving portion of the code. The Receive() method requires the System.Net.IPEndpoint class to be supplied. That can be created using the following code by declaring that we are looking for data sent to us from any source.

#IPEndPoint object will allow us to read datagrams sent from any source.
$remoteendpoint = New-Object system.net.ipendpoint([system.net.ipaddress]::Any,0)

The 0 stands for any port and [System.Net.IPAddress]::Any means from any host.

Now that I have created an endpoint, lets now set the udp object to wait until a response is given back from the host which we sent the data to. We will also read the response back from the remote host.

 #Blocks until a message returns on this socket from a remote host.
$receivebytes = $udpobject.Receive([ref]$remoteendpoint)

#Convert returned data into string format
[string]$returndata = $a.GetString($receivebytes)

#Uses the IPEndPoint object to show that the host responded.
Write-Host "This is the message you received: $($returndata.ToString())"
Write-Host "This message was sent from: $($remoteendpoint.address.ToString()) on their port number: $($remoteendpoint.Port.ToString())"

Let’s assume that the port is closed on the remote host. Running the first line of the code will produce an error such as the one below.

Capture

Luckily, this error can be caught using Try/Catch so you can easily write to a log or anything else if this happens. Ok, so we know how to tell if a UDP port is closed on a remote machine, but what is going to happen if that port is open?  Let’s find out.  I am going to open a UDP port on my server, DC1 and then we will run the same code to see what happens.

First, lets open up that port on DC1:

Capture

Perfect, now lets re-run the same code and see what happens:

Capture

Cool, no errors and now we are just waiting for a response back from the remote server.

10 minutes later… (crickets chirping)

Capture

Hmmm… nothing at all being returned. You might think that the code is hung or something else is going on.  And you are right about something else going on.  Remember what I said earlier about how UDP works? It will not return anything to use even if you send data to that port. So in effort, this wait will basically go on forever.

Here is a gotcha that I was telling you about.  One way to resolve this is by setting a timeout on the receiving of data. To do this, you need to look at the properties of the $udpobject object we created:

PS C:\Users\boe> $udpobject | Format-List *

Client              : System.Net.Sockets.Socket
Available           : 0
Ttl                 : 64
DontFragment        : False
MulticastLoopback   : True
EnableBroadcast     : False
ExclusiveAddressUse : False

Take note of the client property which consists of the System.Net.Sockets.Socket class. Within this class is where we need to make a slight adjustment to the receiving timeout. Now we will expand out the Client property to find out where we need to make that change.

PS C:\Users\boe> $udpobject.client

Available           : 0
LocalEndPoint       : 0.0.0.0:11000
RemoteEndPoint      :
Handle              : 1364
Blocking            : True
UseOnlyOverlappedIO : False
Connected           : False
AddressFamily       : InterNetwork
SocketType          : Dgram
ProtocolType        : Udp
IsBound             : True
ExclusiveAddressUse : False
ReceiveBufferSize   : 8192
SendBufferSize      : 8192
ReceiveTimeout      : 0
SendTimeout         : 0
LingerState         :
NoDelay             :
Ttl                 : 64
DontFragment        : False
MulticastLoopback   : True
EnableBroadcast     : False

And there it is, the ReceiveTimeout property. Currently, it is set to 0, meaning that this will never timeout.  Keep in mind that the value you supply will be in milliseconds. I will go ahead and set this to 10000 milliseconds and now we will re-run the code again to see what happens.

Capture

Now, after 10 seconds, we get the error stating:

Exception calling “Receive” with “1” argument(s): “A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond”

Setting the timeout now allows the code to error out when the port is open so it does not wait forever for a response back. Plus, this can be caught with a Try/Catch statement so we can parse the error and show that the port is open on the system. One more gotcha is that if the server is powered down, but still in DNS, then the error will still occur instead of automatically reporting an error. So keep that in mind. In fact, performing a ping test (or Test-Connection)  prior to making the port check would be recommended.

Sending and Receiving a Message

Ok, now for something fun.  I am going to go onto DC1 and send a response to my laptop on the same port. So instead of a timeout, you will see the message and the ip and port that the message responded back on.

Lets start it off by opening port 11000 on DC1 and connecting to my laptop.

$udp = new-object System.Net.Sockets.Udpclient(11000)
$udp.Connect("boe-laptop",11000)

Capture

Now we create our UDP object here and wait for our message to be sent from DC1. For this I will not specify a timeout as I need to run some code on DC1. Most of the code will not run until it has received a message from DC1.

$udpobject = new-Object system.Net.Sockets.Udpclient(11000)
$remoteendpoint = New-Object system.net.ipendpoint([system.net.ipaddress]::Any,0)
$receivebytes = $udpobject.Receive([ref]$remoteendpoint)
[string]$returndata = $a.GetString($receivebytes)
Write-Host -fore green "This is the message you received: $($returndata.ToString())"
Write-Host -fore green "This message was sent from: $($remoteendpoint.address.ToString()) on their port number: $($remoteendpoint.Port.ToString())"

Capture

Now the command is waiting for a message from DC1. Lets go back to DC1 and create a message from a string and convert it into bytes. Once converted to bytes, we can then send the message to the waiting laptop.

$a = New-Object system.text.asciiencoding
$byte = $a.GetBytes("This message sent from DC1 to boe-laptop")
$udp.Send($byte,$byte.length)

If you have both windows available to view. You will notice that as soon as you perform the Send() method, the waiting PowerShell console will immediately process the message and display the results.

Capture

Pretty cool, isn’t it? You could go back and forth between these to systems and send messages if you wanted to.

That wraps it up for my post on working with UDP ports in PowerShell. As you can see, it is pretty easy to find out if a UDP port is actually open as long as you have a timeout specified in the UDPClient.Client properties. Once you do that, just some simple parsing of the error can determine if the port was open or closed.

Code

I am going to reference my Test-Port function I wrote a while back that is now updated with the UDP code bug fix.

Script Repository

PoshCode

function Test-Port{  
<#    
.SYNOPSIS    
    Tests port on computer.  
    
.DESCRIPTION  
    Tests port on computer. 
     
.PARAMETER computer  
    Name of server to test the port connection on.
      
.PARAMETER port  
    Port to test 
       
.PARAMETER tcp  
    Use tcp port 
      
.PARAMETER udp  
    Use udp port  
     
.PARAMETER UDPTimeOut 
    Sets a timeout for UDP port query. (In milliseconds, Default is 1000)  
      
.PARAMETER TCPTimeOut 
    Sets a timeout for TCP port query. (In milliseconds, Default is 1000)
                 
.NOTES    
    Name: Test-Port.ps1  
    Author: Boe Prox  
    DateCreated: 18Aug2010   
    List of Ports: http://www.iana.org/assignments/port-numbers  
      
    To Do:  
        Add capability to run background jobs for each host to shorten the time to scan.         
.LINK    
    https://boeprox.wordpress.org 
     
.EXAMPLE    
    Test-Port -computer 'server' -port 80  
    Checks port 80 on server 'server' to see if it is listening  
    
.EXAMPLE    
    'server' | Test-Port -port 80  
    Checks port 80 on server 'server' to see if it is listening 
      
.EXAMPLE    
    Test-Port -computer @("server1","server2") -port 80  
    Checks port 80 on server1 and server2 to see if it is listening  
    
.EXAMPLE
    Test-Port -comp dc1 -port 17 -udp -UDPtimeout 10000
    
    Server   : dc1
    Port     : 17
    TypePort : UDP
    Open     : True
    Notes    : "My spelling is Wobbly.  It's good spelling but it Wobbles, and the letters
            get in the wrong places." A. A. Milne (1882-1958)
    
    Description
    -----------
    Queries port 17 (qotd) on the UDP port and returns whether port is open or not
       
.EXAMPLE    
    @("server1","server2") | Test-Port -port 80  
    Checks port 80 on server1 and server2 to see if it is listening  
      
.EXAMPLE    
    (Get-Content hosts.txt) | Test-Port -port 80  
    Checks port 80 on servers in host file to see if it is listening 
     
.EXAMPLE    
    Test-Port -computer (Get-Content hosts.txt) -port 80  
    Checks port 80 on servers in host file to see if it is listening 
        
.EXAMPLE    
    Test-Port -computer (Get-Content hosts.txt) -port @(1..59)  
    Checks a range of ports from 1-59 on all servers in the hosts.txt file      
            
#>   
[cmdletbinding(  
    DefaultParameterSetName = '',  
    ConfirmImpact = 'low'  
)]  
    Param(  
        [Parameter(  
            Mandatory = $True,  
            Position = 0,  
            ParameterSetName = '',  
            ValueFromPipeline = $True)]  
            [array]$computer,  
        [Parameter(  
            Position = 1,  
            Mandatory = $True,  
            ParameterSetName = '')]  
            [array]$port,  
        [Parameter(  
            Mandatory = $False,  
            ParameterSetName = '')]  
            [int]$TCPtimeout=1000,  
        [Parameter(  
            Mandatory = $False,  
            ParameterSetName = '')]  
            [int]$UDPtimeout=1000,             
        [Parameter(  
            Mandatory = $False,  
            ParameterSetName = '')]  
            [switch]$TCP,  
        [Parameter(  
            Mandatory = $False,  
            ParameterSetName = '')]  
            [switch]$UDP                                    
        )  
    Begin {  
        If (!$tcp -AND !$udp) {$tcp = $True}  
        #Typically you never do this, but in this case I felt it was for the benefit of the function  
        #as any errors will be noted in the output of the report          
        $ErrorActionPreference = "SilentlyContinue"  
        $report = @()  
    }  
    Process {     
        ForEach ($c in $computer) {  
            ForEach ($p in $port) {  
                If ($tcp) {    
                    #Create temporary holder   
                    $temp = "" | Select Server, Port, TypePort, Open, Notes  
                    #Create object for connecting to port on computer  
                    $tcpobject = new-Object system.Net.Sockets.TcpClient  
                    #Connect to remote machine's port                
                    $connect = $tcpobject.BeginConnect($c,$p,$null,$null)  
                    #Configure a timeout before quitting  
                    $wait = $connect.AsyncWaitHandle.WaitOne($TCPtimeout,$false)  
                    #If timeout  
                    If(!$wait) {  
                        #Close connection  
                        $tcpobject.Close()  
                        Write-Verbose "Connection Timeout"  
                        #Build report  
                        $temp.Server = $c  
                        $temp.Port = $p  
                        $temp.TypePort = "TCP"  
                        $temp.Open = "False"  
                        $temp.Notes = "Connection to Port Timed Out"  
                    } Else {  
                        $error.Clear()  
                        $tcpobject.EndConnect($connect) | out-Null  
                        #If error  
                        If($error[0]){  
                            #Begin making error more readable in report  
                            [string]$string = ($error[0].exception).message  
                            $message = (($string.split(":")[1]).replace('"',"")).TrimStart()  
                            $failed = $true  
                        }  
                        #Close connection      
                        $tcpobject.Close()  
                        #If unable to query port to due failure  
                        If($failed){  
                            #Build report  
                            $temp.Server = $c  
                            $temp.Port = $p  
                            $temp.TypePort = "TCP"  
                            $temp.Open = "False"  
                            $temp.Notes = "$message"  
                        } Else{  
                            #Build report  
                            $temp.Server = $c  
                            $temp.Port = $p  
                            $temp.TypePort = "TCP"  
                            $temp.Open = "True"    
                            $temp.Notes = ""  
                        }  
                    }     
                    #Reset failed value  
                    $failed = $Null      
                    #Merge temp array with report              
                    $report += $temp  
                }      
                If ($udp) {  
                    #Create temporary holder   
                    $temp = "" | Select Server, Port, TypePort, Open, Notes                                     
                    #Create object for connecting to port on computer  
                    $udpobject = new-Object system.Net.Sockets.Udpclient
                    #Set a timeout on receiving message 
                    $udpobject.client.ReceiveTimeout = $UDPTimeout 
                    #Connect to remote machine's port                
                    Write-Verbose "Making UDP connection to remote server" 
                    $udpobject.Connect("$c",$p) 
                    #Sends a message to the host to which you have connected. 
                    Write-Verbose "Sending message to remote host" 
                    $a = new-object system.text.asciiencoding 
                    $byte = $a.GetBytes("$(Get-Date)") 
                    [void]$udpobject.Send($byte,$byte.length) 
                    #IPEndPoint object will allow us to read datagrams sent from any source.  
                    Write-Verbose "Creating remote endpoint" 
                    $remoteendpoint = New-Object system.net.ipendpoint([system.net.ipaddress]::Any,0) 
                    Try { 
                        #Blocks until a message returns on this socket from a remote host. 
                        Write-Verbose "Waiting for message return" 
                        $receivebytes = $udpobject.Receive([ref]$remoteendpoint) 
                        [string]$returndata = $a.GetString($receivebytes)
                        If ($returndata) {
                           Write-Verbose "Connection Successful"  
                            #Build report  
                            $temp.Server = $c  
                            $temp.Port = $p  
                            $temp.TypePort = "UDP"  
                            $temp.Open = "True"  
                            $temp.Notes = $returndata   
                            $udpobject.close()   
                        }                       
                    } Catch { 
                        If ($Error[0].ToString() -match "\bRespond after a period of time\b") { 
                            #Close connection  
                            $udpobject.Close()  
                            #Make sure that the host is online and not a false positive that it is open 
                            If (Test-Connection -comp $c -count 1 -quiet) { 
                                Write-Verbose "Connection Open"  
                                #Build report  
                                $temp.Server = $c  
                                $temp.Port = $p  
                                $temp.TypePort = "UDP"  
                                $temp.Open = "True"  
                                $temp.Notes = "" 
                            } Else { 
                                <# 
                                It is possible that the host is not online or that the host is online,  
                                but ICMP is blocked by a firewall and this port is actually open. 
                                #> 
                                Write-Verbose "Host maybe unavailable"  
                                #Build report  
                                $temp.Server = $c  
                                $temp.Port = $p  
                                $temp.TypePort = "UDP"  
                                $temp.Open = "False"  
                                $temp.Notes = "Unable to verify if port is open or if host is unavailable."                                 
                            }                         
                        } ElseIf ($Error[0].ToString() -match "forcibly closed by the remote host" ) { 
                            #Close connection  
                            $udpobject.Close()  
                            Write-Verbose "Connection Timeout"  
                            #Build report  
                            $temp.Server = $c  
                            $temp.Port = $p  
                            $temp.TypePort = "UDP"  
                            $temp.Open = "False"  
                            $temp.Notes = "Connection to Port Timed Out"                         
                        } Else {                      
                            $udpobject.close() 
                        } 
                    }     
                    #Merge temp array with report              
                    $report += $temp  
                }                                  
            }  
        }                  
    }  
    End {  
        #Generate Report  
        $report 
    }
}
Posted in powershell, scripts | Tagged , , , | 6 Comments

More Web Services with PowerShell: Geo IP Location

Now that I have been bitten by the New-WebServiceProxy bug and built the weather function in a previous blog, I decided to work on another function to grab an IP address and show the country of origin. So without re-hashing what I already talked about, I will just go right into the function and show an example of what it can do.

Get-GeoIP -IP 65.68.15.56

ReturnCode        : 1
IP                : 65.68.15.56
ReturnCodeDetails : Success
CountryName       : United States
CountryCode       : USA

You can also view your Internet IP Address using the following command:

Get-GeoIP -ShowInternetIP

Sorry, not showing you the output on that one. Smile

Code

Script Repository

Function Get-GeoIP { 
<#   
.SYNOPSIS   
   Display weather data for a specific country and city. 
.DESCRIPTION 
   Display weather data for a specific country and city. There is a possibility for this to fail if the web service being used is unavailable. 
.PARAMETER Ip 
    IP address to find country to origin 
.PARAMETER ShowInternetIP 
    Gets your internet IP address and country of origin   
.PARAMETER Credential 
    Use alternate credentials 
.PARAMETER UseDefaultCredential             
    Use default credentials 
.NOTES   
    Name: Get-GeoIP 
    Author: Boe Prox 
    DateCreated: 16Feb2011  
.LINK  
    http://www.webservicex.net/ws/default.aspx 
.LINK  
    https://boeprox.wordpress.com        
.EXAMPLE   
    Get-GeoIP -IP 192.168.1.1 
 
Description 
-----------     
Returns the country of origin for the specified IP Address. In this case, United States 
 
.EXAMPLE   
    Get-GeoIP -ShowInternetIP 
 
Description 
-----------     
Returns the Internet address from where this command was run. 
 
#>  
[cmdletbinding( 
    DefaultParameterSetName = 'Default', 
    ConfirmImpact = 'low' 
)] 
    Param( 
        [Parameter( 
            Mandatory = $False, 
            Position = 0, 
            ParameterSetName = '', 
            ValueFromPipeline = $True)] 
            [string]$Ip, 
        [Parameter( 
            Position = 1, 
            Mandatory = $False, 
            ParameterSetName = '')] 
            [switch]$ShowInternetIP,  
        [Parameter( 
            Position = 2, 
            Mandatory = $False, 
            ParameterSetName = 'DefaultCred')] 
            [switch]$UseDefaultCredental,   
        [Parameter( 
            Position = 3, 
            Mandatory = $False, 
            ParameterSetName = 'AltCred')] 
            [System.Management.Automation.PSCredential]$Credential                                                 
                         
        ) 
Begin { 
    $psBoundParameters.GetEnumerator() | % {   
        Write-Verbose "Parameter: $_"  
        } 
    #Ensure that user is not using both -City and -ListCities parameters 
    Write-Verbose "Verifying that both City and ListCities is not being used in same command." 
    If ($PSBoundParameters.ContainsKey('ListCities') -AND $PSBoundParameters.ContainsKey('City')) { 
        Write-Warning "You cannot use both -City and -ListCities in the same command!" 
        Break 
        } 
    Switch ($PSCmdlet.ParameterSetName) { 
        AltCred { 
            Try { 
                #Make connection to known good geo ip service using DefaultCredentials 
                Write-Verbose "Create web proxy connection to geo ip service using Alternate Credentials" 
                $geoip = New-WebServiceProxy 'http://www.webservicex.net/geoipservice.asmx?WSDL' -Credential $credential 
                } 
            Catch { 
                Write-Warning "$($Error[0])" 
                Break 
                }  
           } 
        DefaultCred {              
            Try { 
                #Make connection to known good geo ip service using Alternate Credentials 
                Write-Verbose "Create web proxy connection to geo ip service using DefaultCredentials" 
                $geoip = New-WebServiceProxy 'http://www.webservicex.net/geoipservice.asmx?WSDL' -UseDefaultCredential 
                } 
            Catch { 
                Write-Warning "$($Error[0])" 
                Break 
                }  
           }  
        Default {              
            Try { 
                #Make connection to known good geo ip service 
                Write-Verbose "Create web proxy connection to geo ip service" 
                $geoip = New-WebServiceProxy 'http://www.webservicex.net/geoipservice.asmx?WSDL' 
                } 
            Catch { 
                Write-Warning "$($Error[0])" 
                Break 
                }  
           }  
       }                      
    } 
Process { 
    #Determine if we are only to list the cities for a given country or get the weather from a city 
    If ($PSBoundParameters.ContainsKey('Ip')) { 
        Try { 
            #List all cities available to query for geo ip 
            Write-Verbose "Retrieving location of IP: $($ip)" 
            $geoip.GetGeoIP($ip) 
            Break 
            } 
        Catch { 
            Write-Warning "$($Error[0])" 
            Break 
            } 
        } 
    If ($PSBoundParameters.ContainsKey('ShowInternetIP')) { 
        Try { 
            #Get your Internet IP and geo location 
            Write-Verbose "Retrieving internet IP and location" 
            $geoip.GetGeoIPContext() 
            Break 
            } 
        Catch { 
            Write-Warning "$($Error[0])" 
            Break 
            } 
        } 
    } 
End { 
    Write-Verbose "End function" 
    }    
}     
Posted in powershell, scripts | Tagged , , | 1 Comment