Finding success with SCCM – trigger Schedule

If you’ve ever dealt with SCCM you’ll understand to get a client to forcibly download patches / software from SCCM you’ll need to call WMI to trigger a schedule.

The list of triggers can be found here. Trigger schedule

This post is about how to find / determine success for one of the triggers in this list:

To start with I chose to create a hashTable to hold the triggers with a name:


function New-CMSccmTriggerHashTable
{
    $Sccmhash =@{HardwareInventoryCollectionTask='{00000000-0000-0000-0000-000000000001}'
		SoftwareInventoryCollectionTask='{00000000-0000-0000-0000-000000000002}'
		HeartbeatDiscoveryCycle='{00000000-0000-0000-0000-000000000003}'
		SoftwareInventoryFileCollectionTask='{00000000-0000-0000-0000-000000000010}'
		RequestMachinePolicyAssignments='{00000000-0000-0000-0000-000000000021}'
		EvaluateMachinePolicyAssignments='{00000000-0000-0000-0000-000000000022}'
		RefreshDefaultMPTask='{00000000-0000-0000-0000-000000000023}'
		RefreshLocationServicesTask='{00000000-0000-0000-0000-000000000024}'
		LocationServicesCleanupTask='{00000000-0000-0000-0000-000000000025}'
		SoftwareMeteringReportCycle='{00000000-0000-0000-0000-000000000031}'
		SourceUpdateManageUpdateCycle='{00000000-0000-0000-0000-000000000032}'
		PolicyAgentCleanupCycle='{00000000-0000-0000-0000-000000000040}'
		CertificateMaintenanceCycle='{00000000-0000-0000-0000-000000000051}'
		PeerDistributionPointStatusTask='{00000000-0000-0000-0000-000000000061}'
		PeerDistributionPointProvisioningStatusTask='{00000000-0000-0000-0000-000000000062}'
		ComplianceIntervalEnforcement='{00000000-0000-0000-0000-000000000071}'
		SoftwareUpdatesAgentAssignmentEvaluationCycle='{00000000-0000-0000-0000-000000000108}'
		SendUnsentStateMessages='{00000000-0000-0000-0000-000000000111}'
		StateMessageManagerTask='{00000000-0000-0000-0000-000000000112}'
		ForceUpdateScan='{00000000-0000-0000-0000-000000000113}'
		AMTProvisionCycle='{00000000-0000-0000-0000-000000000120}'}
    $Sccmhash
}

Now any of triggers can be accounted for with this hashtable.


$sccmHash = New-CMSccmTriggerHashTable
$sccmHash['RequestMachinePolicyAssignments']
{00000000-0000-0000-0000-000000000021}

With a human readable form for the trigger a  function to Invoke the schedule for the trigger can be constructed:

function Invoke-CMRequestMachinePolicyAssignments
{
    param([Parameter(Mandatory=$true)]$computername, 
    [Parameter(Mandatory=$true)]$Path = 'c:\windows\ccm\logs',
            [pscredential]$credential)
    $Sccmhash = New-CMSccmTriggerHashTable
    if(Test-CCMLocalMachine $computername)
    {
        $TimeReference =(get-date)
    }
    else
    {
        $TimeReference = invoke-command -ComputerName $computername `
        -scriptblock {get-date} -credential $credential
    }
    if($credentials)
    {
        Invoke-WmiMethod -Class sms_client -Namespace 'root\ccm' 
       ​-ComputerName $computername -credential $credential 
      ​-Name TriggerSchedule 
      ​-ArgumentList "$($Sccmhash["RequestMachinePolicyAssignments"])" 
    }
    else
    {
        $SmsClient =[wmiclass]("\\$ComputerName\ROOT\ccm:SMS_Client")
        $SmsClient.TriggerSchedule`
        ($Sccmhash["RequestMachinePolicyAssignments"])
    }
    $RequestMachinePolicyAssignments = $false

    # can see when this is requested from the Policy agentlog:
    $RequestMachinePolicyAssignments = Test-CMRequestMachinePolicyAssignments`
     -computername $computername -Path $Path -TimeReference $TimeReference `
     -credential $credential

    [PSCustomObject]@{'RequestMachinePolicyAssignments' = $RequestMachinePolicyAssignments
                      'TimeReference' = ($TimeReference)}
}

Once the Request Machine Policy Assignments is triggered.  Another function is called.  This is where a search through the client logs determine success of the trigger invocation.

The value that determines success is Evaluation not required. No changes detected. Which can be found in the PolicyEvaluator.log

In order to find this value we first need to find out if the computer name that was passed is a local machine or a remote machine. This is done with the function Test-CCMLocalMachine. This function is used in both the invoke and the test to determine if excution is on the local machine or a remote machine.  To make sure when searching the log a $TimeReference is used. If it is passed in one of the parameters then that value is used to search through the log.  If it is not passed  the current time from the remote or local machine will be used.


function Test-CMRequestMachinePolicyAssignments
{
    param([Parameter(Mandatory=$true)]$computername, 
    [Parameter(Mandatory=$true)]$Path = 'c:\windows\ccm\logs'
    ,[datetime]$TimeReference,
    [pscredential] $credential)
    if ($TimeReference -eq $null)
    {
        if(Test-CCMLocalMachine $computername)
        {
            $TimeReference =(get-date)
        }
        else
        {
            [datetime]$TimeReference = invoke-command 
            ​-ComputerName $computername -scriptblock {get-date}
        }
    }
    $RequestMachinePolicyAssignments = $false
    # can see when this is requested from the Policy agentlog:
    Push-Location
    Set-Location c:
    if(Test-CCMLocalMachine $computername)
    {
        $p = Get-CMLog -path "$path\policyevaluator.log"
        $runResults = $P |Where-Object{$_.Localtime -gt $TimeReference} `
       | where-object {$_.message -like `
         "*Evaluation not required. No changes detected.*"}
    }
    else
    {
        $p = Get-CCMLog -ComputerName $computerName -path $Path -log policyevaluator -credential $credential
        $runResults = $P.policyevaluatorLog |Where-Object{$_.Localtime -gt $TimeReference} | where-object {$_.message -like "*Evaluation not required. No changes detected.*"}
    }
    Pop-Location
    #if in the 

        if($runResults)
        {
            $RequestMachinePolicyAssignments = $true
        }
    $RequestMachinePolicyAssignments
}

Finding this value in the PolicyEvaluator.log can take up to 45 minutes or more depending on the setup of your SCCM environment.

To allow for finding the value described above and two other triggers. The following script demonstrates its usage:

GetAvailUpdates.ps1

All of the functions shown above can be found in this github repository:

SCCMUtilities

 

I hope this helps someone

Until then

 

Keep Scripting

 

thom

Advertisement

PowerShell Conference Book – My Contribution

If you follow Twitter  like I do especially when it comes to Powershell,  you will already have noticed there is a new book out in the community called Powershell Conference Book.  There are some very sharp PowerShell MVPs/Experts/Enthusiasts that have posted a chapter about what they’ve spoken on in conferences or will speak about.

Author Website
Mike F Robbins https://mikefrobbins.com
Jeff Hicks https://jdhitsolutions.com
Michael Lombardi https://appoint.ly/t/michaeltlombardi
Adam Murry https://tikabu.com.au/blog/
Anthony Nocentino http://www.centinosystems.com
Brandin Olin https://devblackops.io
Brian Bunke https://www.brianbunke.com
Don Jones https://donjones.com
Doug Finke https://dfinke.github.io
Emin Atac https://p0w3rsh3ll.wordpress.com
Fred Weinman https://allthingspowershell.blogspot.com
Graham Beer https://graham-beer.github.io
Irwin Strachan https://pshirwin.wordpress.com
James Petty https://scriptautomaterepeat.com
Jeremy Murrah https://murrahjm.github.io
Juston Sider https://invoke-automation.blog
Luc Dekens http://www.lucd.info
Mark Kraus https://get-powershellblog.blogspot.com
Mark Wragg https://wragg.io
Mike Shepard https://powershellstation.com
Patrick Gruenhaer https://sid-500.com
Prateek Singh https://ridicurious.com
Rob Pleau https://ephos.github.io
Rob Sewell https://sqldbawithabeard.com
Thomas Lee https://tfl09.blogspot.com
Thomas Raynor https://workingsysadmin.com
Thom Schumacher https://powershellposse.com
Tim Curwick https://MadWithPowerShell.com
Tim Warner https://timwarnertech.com
Tommy Maynard https://tommymaynard.com
Tore Groneg https://asaconsultant.blogspot.com
Wesley Kirkland https://wesleyk.me

I am very honored that I was also chosen to write a chapter in this book.  My chapter is about the work I’ve done (SCCM), and dynamic parameters. I explore how to Parse a SCCM log file and then use it together with a Dynamic parameter.  Using this dynamic parameter you can more efficiently locate a log entry with a few keystrokes.

A huge thank you for the opportunity and to the organizers of this endeavor (Mike F Robbins, Jeff Hicks, Michael Lombardi).  Not only did they author their own chapter they also proof read and made sure the other authors were providing good content.

I would encourage everyone to go  get a copy.  There is a wealth of material in this book, while it is intended for experienced users, I believe that everyone can benefit from its content.

The proceeds from this book are going to a worthy cause:

The DevOps Collective, Inc. OnRamp Scholarship

Your contribution will help someone learn a new skill and move forward in their career.  I’m honored to be able to have contributed with a chapter in this Awesome book.

until then

Keep scripting

 

thom

Parsing CCM/Logs – Part 2 using a Dynamic Parameter

In the first Blog post Parsing CCM\Logs I showed you how I was able to get a Script from the community and make a few tweaks to allow for it to parse logs for CCM.  In this blog post I’m going to show you how I took the next step.   My next step was to use the parsing logic in a script and incorporate a Dynamic Parameter.

To begin with I wanted to have the user not have to go to the machine and find every log for CCM\logs and then type in the value of that log name.  For instance in my directory for c:\windows\ccm\logs there were 178 files with the .log extension.  Trying to create a Validate set for this many logs is also problematic.  So I chose the dynamic Parameter approach for this function.  Now on to the script.

The first portion of the script is standard parameter values.


param([Parameter(Mandatory=$true,Position=0)]$ComputerName = '$env:computername', [Parameter(Mandatory=$true,Position=1)]$path = 'c:\windows\ccm\logs')

The next portion of this script is where the “magic” is.  The next parameter -log is created dynamically from the first two variables ($computerName, $path).


DynamicParam
{
$ParameterName = 'Log'
if($path.ToCharArray() -contains ':')
{

$FilePath = "\\$ComputerName\$($path -replace ':','$')"
}
else
{
$FilePath = "\\$computerName\$((get-item $path).FullName -replace ':','$')"
}

$logs = Get-ChildItem "$FilePath\*.log"
$LogNames = $logs.basename

$logAttribute = New-Object System.Management.Automation.ParameterAttribute
$logAttribute.Position = 2
$logAttribute.Mandatory = $true
$logAttribute.HelpMessage = 'Pick A log to parse'

$logCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$logCollection.add($logAttribute)

$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)
$logCollection.add($logValidateSet)

$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)

$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$logDictionary.Add($ParameterName,$logParam)
return $logDictionary

}

To explain what is going on here I’ll start with the code I used to get me to this point. Martin Schvartzman wrote a great article that showed how to do most of what I’ve posted here Dynamic ValidateSet in a Dynamic Parameter. 

I’ll do my best to explain how his code works.  First step is the DynamicParam Statement.  This tells PowerShell that we are going to create a dynamic Parameter.  Very simply stated a dynamic parameter is a Parameter that is added at runtime only when needed.

The first thing that is done in this dynamic parameter is to create a Runtime Defined Parameter Dictionary.  In order to add a runtime parameter we have to define the parameter and it’s attributes and then add it to the collection to return to the runtime so it will be added properly.

<pre>System.Management.Automation.RuntimeDefinedParameterDictionary</pre>

This next portion of the code creates a object that will contain our parameter attributes.


$logAttribute = New-Object System.Management.Automation.ParameterAttribute

For the purposes of this script we are only going to make the parameter mandatory, set it’s position in the pipeline, and create a help message.  There are other items that can be defined if required.  We can see this by getting the members of the $logAttribute:


$logAttribute  | get-member -properties

TypeName: System.Management.Automation.ParameterAttribute

Name MemberType Definition
---- ---------- ----------
DontShow Property bool DontShow {get;set;}
HelpMessage Property string HelpMessage {get;set;}
HelpMessageBaseName Property string HelpMessageBaseName {get;set;}
HelpMessageResourceId Property string HelpMessageResourceId {get;set;}
Mandatory Property bool Mandatory {get;set;}
ParameterSetName Property string ParameterSetName {get;set;}
Position Property int Position {get;set;}
TypeId Property System.Object TypeId {get;}
ValueFromPipeline Property bool ValueFromPipeline {get;set;}
ValueFromPipelineByPropertyName Property bool ValueFromPipelineByPropertyName {get;set;}
ValueFromRemainingArguments Property bool ValueFromRemainingArguments {get;set;}

Since we are going to need this set of attributes in our parameter we need to add this to the Attribute collection ($logCollection) that will in turn be added to the runtime Parameter $logParam.

Next we’ll create our Validate set item from the list of logs on the remote machine which was gathered with the FilePath variable then added to a new object that will contain our ValidateSet attributes. Then add it to our LogCollection.


$FilePath = "\\$ComputerName\$($path -replace ':','$')"

<br>

$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)

 $logCollection.add($logValidateSet)

Finally we’ll add our parameter name and LogCollection to a Runtime Defined parameter.  Then put this all in our Runtime Defined Parameter Dictionary. Then hand it back to PowerShell.


$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)

$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
 $logDictionary.Add($ParameterName,$logParam)
 return $logDictionary

Now  that we have the Full explanation of the Dynamic Parameter we can stitch our previous Log Parser together with this function to give us back any one of the logs on our remote machine.  We’ll put this in our Process block of our function:


 $sb2 = "$((Get-ChildItem function:get-cmlog).scriptblock)`r`n"
 $sb1 = [scriptblock]::Create($sb2)
 $results = Invoke-Command -ComputerName $ComputerName -ScriptBlock $sb1 -ArgumentList "$path\$log.log"
 [PSCustomObject]@{"$($log)Log"=$results}

Now when we call Get-CcmLog we’ll get a return with a parsed log that has Log appended in the object name.

dynparam3

Full code is posted in a gist here:


function Get-CMLog
{
<#
.SYNOPSIS
Parses logs for System Center Configuration Manager.
.DESCRIPTION
Accepts a single log file or array of log files and parses them into objects. Shows both UTC and local time for troubleshooting across time zones.
.PARAMETER Path
Specifies the path to a log file or files.
.INPUTS
Path/FullName.
.OUTPUTS
PSCustomObject.
.EXAMPLE
C:\PS> Get-CMLog -Path Sample.log
Converts each log line in Sample.log into objects
UTCTime : 7/15/2013 3:28:08 PM
LocalTime : 7/15/2013 2:28:08 PM
FileName : sample.log
Component : TSPxe
Context :
Type : 3
TID : 1040
Reference : libsmsmessaging.cpp:9281
Message : content location request failed
.EXAMPLE
C:\PS> Get-ChildItem -Path C:\Windows\CCM\Logs | Select-String -Pattern 'failed' | Select -Unique Path | Get-CMLog
Find all log files in folder, create a unique list of files containing the phrase 'failed, and convert the logs into objects
UTCTime : 7/15/2013 3:28:08 PM
LocalTime : 7/15/2013 2:28:08 PM
FileName : sample.log
Component : TSPxe
Context :
Type : 3
TID : 1040
Reference : libsmsmessaging.cpp:9281
Message : content location request failed
.LINK
http://blog.richprescott.com
#>
param(
[Parameter(Mandatory=$true,
Position=0,
ValueFromPipelineByPropertyName=$true)]
[Alias("FullName")]
$Path,
$tail =10
)
PROCESS
{
if(($Path -isnot [array]) -and (test-path $Path PathType Container) )
{
$Path = Get-ChildItem "$path\*.log"
}
foreach ($File in $Path)
{
if(!( test-path $file))
{
$Path +=(Get-ChildItem "$file*.log").fullname
}
$FileName = Split-Path Path $File Leaf
if($tail)
{
$lines = Get-Content Path $File tail $tail
}
else {
$lines = get-Content path $file
}
ForEach($l in $lines ){
$l -match '\<\!\[LOG\[(?<Message>.*)?\]LOG\]\!\>\<time=\"(?<Time>.+)(?<TZAdjust>[+|-])(?<TZOffset>\d{2,3})\"\s+date=\"(?<Date>.+)?\"\s+component=\"(?<Component>.+)?\"\s+context="(?<Context>.*)?\"\s+type=\"(?<Type>\d)?\"\s+thread=\"(?<TID>\d+)?\"\s+file=\"(?<Reference>.+)?\"\>' | Out-Null
if($matches)
{
$UTCTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)$($matches.TZAdjust)$($matches.TZOffset/60)"),"MM-dd-yyyy HH:mm:ss.fffz", $null, "AdjustToUniversal")
$LocalTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)"),"MM-dd-yyyy HH:mm:ss.fff", $null)
}
[pscustomobject]@{
UTCTime = $UTCTime
LocalTime = $LocalTime
FileName = $FileName
Component = $matches.component
Context = $matches.context
Type = $matches.type
TID = $matches.TI
Reference = $matches.reference
Message = $matches.message
}
}
}
}
}
function Get-CCMLog
{
param([Parameter(Mandatory=$true,Position=0)]$ComputerName = '$env:computername', [Parameter(Mandatory=$true,Position=1)]$path = 'c:\windows\ccm\logs')
DynamicParam
{
$ParameterName = 'Log'
if($path.ToCharArray() -contains ':')
{
$FilePath = "\\$($ComputerName)\$($path -replace ':','$')"
}
else
{
$FilePath = "\\$($ComputerName)\$((get-item $path).FullName -replace ':','$')"
}
$logs = Get-ChildItem "$FilePath\*.log"
$LogNames = $logs.basename
$logAttribute = New-Object System.Management.Automation.ParameterAttribute
$logAttribute.Position = 2
$logAttribute.Mandatory = $true
$logAttribute.HelpMessage = 'Pick A log to parse'
$logCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$logCollection.add($logAttribute)
$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)
$logCollection.add($logValidateSet)
$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)
$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$logDictionary.Add($ParameterName,$logParam)
return $logDictionary
}
begin {
# Bind the parameter to a friendly variable
$Log = $PsBoundParameters[$ParameterName]
}
process {
$sb2 = "$((Get-ChildItem function:get-cmlog).scriptblock)`r`n"
$sb1 = [scriptblock]::Create($sb2)
$results = Invoke-Command ComputerName $ComputerName ScriptBlock $sb1 ArgumentList "$path\$log.log"
[PSCustomObject]@{"$($log)Log"=$results}
}
}

view raw

get-ccmlog.ps1

hosted with ❤ by GitHub

Parsing CCM\Logs

If you’ve ever worked with Configuration manager you’ll understand that there are quite a few logs on the Client side.  Opening and searching through them for actions that have taken place can be quite a task.  I needed to find when an item was logged during initial startup/build of a vm.  So I sought out tools to parse these logs to find out the status of  Configuration Manager client side. This post is about the tools/scripts I found and what I added to them to make it easier to discover and parse all the log files.

I started with the need to be able to just parse the log files.  I discovered that Rich Prescott in the community had done the work of parsing these log files with this script:

http://blog.richprescott.com/2017/07/sccm-log-parser.html

With that script in had I made two changes to the script.  The first change was to allow for all the files in the directory to be added to the return object.

 if(($Path -isnot [array]) -and (test-path $Path -PathType Container) )
{
$Path = Get-ChildItem "$path\*.log"
}

The second change allowed for the user to specify a tail amount. This allows for just a portion of the end of the log to be retrieved instead of the entire log.   That script can be found on one of my gists at the Tail end of this article.

 if($tail)
{
$lines = Get-Content -Path $File -tail $tail
}
else {
$lines = get-Content -path $file
}
ForEach($l in $lines )

 


function Get-CMLog
{
<#
.SYNOPSIS
Parses logs for System Center Configuration Manager.
.DESCRIPTION
Accepts a single log file or array of log files and parses them into objects. Shows both UTC and local time for troubleshooting across time zones.
.PARAMETER Path
Specifies the path to a log file or files.
.INPUTS
Path/FullName.
.OUTPUTS
PSCustomObject.
.EXAMPLE
C:\PS> Get-CMLog -Path Sample.log
Converts each log line in Sample.log into objects
UTCTime : 7/15/2013 3:28:08 PM
LocalTime : 7/15/2013 2:28:08 PM
FileName : sample.log
Component : TSPxe
Context :
Type : 3
TID : 1040
Reference : libsmsmessaging.cpp:9281
Message : content location request failed
.EXAMPLE
C:\PS> Get-ChildItem -Path C:\Windows\CCM\Logs | Select-String -Pattern 'failed' | Select -Unique Path | Get-CMLog
Find all log files in folder, create a unique list of files containing the phrase 'failed, and convert the logs into objects
UTCTime : 7/15/2013 3:28:08 PM
LocalTime : 7/15/2013 2:28:08 PM
FileName : sample.log
Component : TSPxe
Context :
Type : 3
TID : 1040
Reference : libsmsmessaging.cpp:9281
Message : content location request failed
.LINK
http://blog.richprescott.com
#>
param(
[Parameter(Mandatory=$true,
Position=0,
ValueFromPipelineByPropertyName=$true)]
[Alias("FullName")]
$Path,
$tail =10
)
PROCESS
{
if(($Path -isnot [array]) -and (test-path $Path PathType Container) )
{
$Path = Get-ChildItem "$path\*.log"
}
foreach ($File in $Path)
{
if(!( test-path $file))
{
$Path +=(Get-ChildItem "$file*.log").fullname
}
$FileName = Split-Path Path $File Leaf
if($tail)
{
$lines = Get-Content Path $File tail $tail
}
else {
$lines = get-Content path $file
}
ForEach($l in $lines ){
$l -match '\<\!\[LOG\[(?<Message>.*)?\]LOG\]\!\>\<time=\"(?<Time>.+)(?<TZAdjust>[+|-])(?<TZOffset>\d{2,3})\"\s+date=\"(?<Date>.+)?\"\s+component=\"(?<Component>.+)?\"\s+context="(?<Context>.*)?\"\s+type=\"(?<Type>\d)?\"\s+thread=\"(?<TID>\d+)?\"\s+file=\"(?<Reference>.+)?\"\>' | Out-Null
if($matches)
{
$UTCTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)$($matches.TZAdjust)$($matches.TZOffset/60)"),"MM-dd-yyyy HH:mm:ss.fffz", $null, "AdjustToUniversal")
$LocalTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)"),"MM-dd-yyyy HH:mm:ss.fff", $null)
}
[pscustomobject]@{
UTCTime = $UTCTime
LocalTime = $LocalTime
FileName = $FileName
Component = $matches.component
Context = $matches.context
Type = $matches.type
TID = $matches.TI
Reference = $matches.reference
Message = $matches.message
}
}
}
}
}
function Get-CCMLog
{
param([Parameter(Mandatory=$true,Position=0)]$ComputerName = '$env:computername', [Parameter(Mandatory=$true,Position=1)]$path = 'c:\windows\ccm\logs')
DynamicParam
{
$ParameterName = 'Log'
if($path.ToCharArray() -contains ':')
{
$FilePath = "\\$($ComputerName)\$($path -replace ':','$')"
}
else
{
$FilePath = "\\$($ComputerName)\$((get-item $path).FullName -replace ':','$')"
}
$logs = Get-ChildItem "$FilePath\*.log"
$LogNames = $logs.basename
$logAttribute = New-Object System.Management.Automation.ParameterAttribute
$logAttribute.Position = 2
$logAttribute.Mandatory = $true
$logAttribute.HelpMessage = 'Pick A log to parse'
$logCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$logCollection.add($logAttribute)
$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)
$logCollection.add($logValidateSet)
$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)
$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$logDictionary.Add($ParameterName,$logParam)
return $logDictionary
}
begin {
# Bind the parameter to a friendly variable
$Log = $PsBoundParameters[$ParameterName]
}
process {
$sb2 = "$((Get-ChildItem function:get-cmlog).scriptblock)`r`n"
$sb1 = [scriptblock]::Create($sb2)
$results = Invoke-Command ComputerName $ComputerName ScriptBlock $sb1 ArgumentList "$path\$log.log"
[PSCustomObject]@{"$($log)Log"=$results}
}
}

view raw

get-ccmlog.ps1

hosted with ❤ by GitHub

I hope this helps someone.

Until then

Keep scripting

Thom

TNSNames File Parsing

If you’ve ever worked with  Oracle you are familiar with Oracle’s TNSNAMES file. This file describes how to get to a database.   With ODP.Net it doesn’t provide a means to parse the TNSNAMES.ora file and then in turn use it with ODP.Net.  From everything I’ve read you must just copy from the Description() and put Data Source = Description(). Then you can use that as a means to connect to your Oracle Database server.    With that in mind I set out to write some scripting to help with this problem.

The first thing I did was to follow this great article by the Scripting Guys about how to use ODP.NET.  After reading that article I found a great module on the Gallery that implemented much of what is spoken about there and I’ll be using that module here in this posting (SimplySQL)

Now I know where my  TNSNAMES.ora file is located so I’ll bring it into my session with:

$tnsnamesPath = 'c:\tns\tnsnames.ora'
$tn = get-content $tnsnamesPath -raw

I brought the file in -raw so that I knew I would have a full object.  Now with some REGEX I can get this file in to the fashion I want.  First to look for the common string in my TNSNAMES.ora file somename = (Description = . 

$parsedTN = $tn -replace '(.*\=.*|\n.*\=)(.*|\n.*)\(DESCRIPTION*.\=' ,'Data Source = (DESCRIPTION ='

Now that I have the connection Name replaced with Data Source = I can now split it into an array and then select my connection based on that array:

$splitTN = $parsedTN -split '(?=.*Data Source = \(DESCRIPTION \=)' 
$splitTN.count
3

$splitTN[1]
Data Source = (DESCRIPTION =
 (ADDRESS_LIST=
 (ADDRESS = (PROTOCOL = TCP)(HOST = server3)(PORT = 1521))
 (ADDRESS = (PROTOCOL = TCP)(HOST = server58)(PORT = 1521)))
 (LOAD_BALANCE = YES)(CONNECTION_TIMEOUT=5)(RETRY_COUNT=3)
 (CONNECT_DATA = (SERVER = DEDICATED)(SERVICE_NAME = ketchup)
 (FAILOVER_MODE = (TYPE = SELECT)(METHOD = BASIC)(RETRIES = 180)(DELAY = 5)))
 )

Now that  I have the connections split into an array I can now select the one I want using Where-Object -like “myconnectionName“.  Then with this handy commandlet Open-OracleConnection From this module (simplySQL) , all i have to do next is pass in my username and password and that should open My oracle connection.

$tnsnames = $splitTN |?{$_ -like "*$connectionName*"}
$connstring = "$tnsnames;User Id=$username;Password=$password"
Open-OracleConnection -ConnectionString $connstring -ConnectionName $connectionName

Below is the full script in a GitHub Gist:


param($tnsnamesPath = 'c:\tns\tnsnames.ora',$username = 'user',$password = 'gotmehere', $connectionName = 'mustard', $query = 'Select sysdate from dual')
$simplySQLPath = (Get-Module ListAvailable simplySQL).ModuleBase
if($simplySQLPath -and (test-path $tnsnamesPath PathType Leaf) -and (![string]::IsNullOrEmpty($node)))
{
[System.Reflection.Assembly]::LoadFile("$simplySQLPath\DataReaderToPSObject.dll") | OUT-NULL
Import-Module SimplySql Force
$parsedTN = (get-content $tnsnamesPath raw) -replace '(.*\=.*|\n.*\=)(.*|\n.*)\(DESCRIPTION*.\=' ,'Data Source = (DESCRIPTION ='
$splitTN = $parsedTN -split '(?=.*Data Source = \(DESCRIPTION \=)'
$tnsnames = $splitTN |?{$_ -like "*$connectionName*"}
$connstring = "$tnsnames;User Id=$username;Password=$password"
try
{
Open-OracleConnection ConnectionString $connstring ConnectionName $connectionName
$result = Invoke-SqlQuery ConnectionName $connectionName Query "$SQLQuery"
Close-SqlConnection ConnectionName $connectionName
}
catch
{
$_.exception
}
}
Else
{
if(!(test-path $tnsnamesPath PathType Leaf ErrorAction Ignore))
{
Throw "Check TNSnamesPath: $tnsNamesPath"
}
else
{
Throw "Exeception SIMPLYSQL not found in module Path $($env:PSModulePath)"
}
}
$result


ketchup=(DESCRIPTION =
(ADDRESS_LIST=
(ADDRESS = (PROTOCOL = TCP)(HOST = server3)(PORT = 1521))
(ADDRESS = (PROTOCOL = TCP)(HOST = server58)(PORT = 1521)))
(LOAD_BALANCE = YES)(CONNECTION_TIMEOUT=5)(RETRY_COUNT=3)
(CONNECT_DATA = (SERVER = DEDICATED)(SERVICE_NAME = ketchup)
(FAILOVER_MODE = (TYPE = SELECT)(METHOD = BASIC)(RETRIES = 180)(DELAY = 5)))
)
mustard =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = host1)(PORT = 1521))
(ADDRESS = (PROTOCOL = TCP)(HOST = host2)(PORT = 1521))
(LOAD_BALANCE = YES)(CONNECTION_TIMEOUT=5)(RETRY_COUNT=3)
(FAILOVER = on)
)
(CONNECT_DATA =
(SERVER = DEDICATED)
(SERVICE_NAME = mustard)
(FAILOVER_MODE = (TYPE = SELECT)(METHOD = BASIC)(RETRIES = 180)(DELAY = 5))))
)
)
)
)

view raw

tnsnames.ora

hosted with ❤ by GitHub

I hope this helps someone.

 

Until then

 

Keep scripting

 

Thom

 

[QuickScript] Find out what that MAC is

I wanted to find out each mac address was on my router.  So I decided to find out what was available for a given IP Address.  What I found was there is an API that you can query to Get information about what company owns that mac address.

Now to see how we query and get that information from the API:

According to the site: http://macvendors.co/api/

we only need to Query the api and pass it a mac address and then pass in the url either JSON or XML:


invoke-restmethod -uri http://macvendors.co/api/58:EF:68:00:00:00/json | select result

result
------
@{company=Belkin International Inc.; mac_prefix=58:EF:68; address=12045 East Waterfront Drive,Playa Vista 90094,U...

Without a json Tag


(invoke-restmethod -uri http://macvendors.co/api/7C:01:91:00:00:00).result

company : Apple, Inc.
mac_prefix : 7C:01:91
address : 1 Infinite Loop,Cupertino CA 95014,US
start_hex : 7C0191000000
end_hex : 7C0191FFFFFF
country : US
type : MA-L

Telling the API to return XML


(invoke-restmethod -uri http://macvendors.co/api/58:EF:68:00:00:00/Xml).result

company : Belkin International Inc.
mac_prefix : 58:EF:68
address : 12045 East Waterfront Drive,Playa Vista 90094,US
start_hex : 58EF68000000
end_hex : 58EF68FFFFFF
country : US
type : MA-L

As you can see getting the results already comes in Json and or form of an object so getting this with PowerShell is pretty straightforward.

 

Hope this helps Someone

 

Until then

 

Keep Scripting

 

thom

DacPac to Folders with PowerShell

A Question was posed on Stackoverflow.  How do you create a folder structure from a created DacPac or BacPac.  This article is how I went about doing this with PowerShell.

The first means was to try and find someone who’d tried this before. The best I could find searching was this article: Deploy DACPACs with PowerShell.  This Script does an excellent job of showing how you can use the SQL 2014 DLL’s to create a Script that can than be deployed to your database.   This did not answer the question though.

Thankfully one of the participants in the question was kind enough to show how to do this very thing in C# so I took their pseudo code and turned it into PowerShell Code.


using (TSqlModel modelFromDacpac = new TSqlModel(dacpacPath))
{
 IEnumerable<TSqlObject> allObjects = model.GetObjects(QueryScopes);
 foreach (TSqlObject tsqlObject allObjects)
 {
 string script;
 if (tsqlObject.TryGetScript(out script))
 {
 // Some objects such as the DatabaseOptions can't be scripted out.

// Write to disk by object type
 string objectTypeName = tsqlObject.ObjectType.Name;
 // pseudo-code as I didn't bother writing.
 // basically just create the folder and write a file
 this.MkdirIfNotExists(objectTypeName);
 this.WriteToFile(objectTypeName, tsqlObject.Name + '.sql', script;
 }
 }
}

 

Starting at the top of the script I need to translate the using Statement into a New-object in PowerShell. In order to do that I needed to find what Dot net class TSQLModel was in.  Based on that research I found that I needed to add the type to my session(Microsoft.SqlServer.Dac.Extensions.dll).  Once the type was added I was then able to get the model from my DacPac.

<br>

add-type -path 'C:\Program Files (x86)\Microsoft SQL Server\120\DAC\bin\Microsoft.SqlServer.Dac.Extensions.dll'

$model =[Microsoft.SqlServer.Dac.Model.TSqlModel]::new(((get-item ".\$dacpac").fullname))

 

Now that I have the Model of my dacpac I need to figure out how to make this into a PowerShell piece of code IEnumerable<TSqlObject> allObjects = model.GetObjects(QueryScopes);

I know my return type is IEnumerable Tsql object.. Now the question is how do I query my model and get that return object.  Based on the C# code I need to call GetObjects. GetObjects expects you to pass an object type (QueryScopes variable) and optionally you  can pass the Object Identifier ID or the Object type.   The queryScopes is an enumeration that has the following values(All, Builtin, Default, None, SameDatabase, System, UserDefined). I chose the All so I could see what this method would return.

 

$returnObjects = $model.GetObjects([Microsoft.SqlServer.Dac.Model.DacQueryScopes]::All)

Now the next step is to step through the return result and test each item to see if it can be scripted.  When calling the TryGetScript it has an output so we must declare a variable before we call this method.

<br>

$s = ''
foreach($r in $returnObjects)
{
 if ($r.TryGetScript([ref]$s))
 {
 $objectTypeName = $r.ObjectType.Name;
 $d="c:\temp\db\$objectTypeName"
 if(!(test-path $d ))
 {
 new-item $d -ItemType Directory
 }
 $filename = "$d\$($r.Name.Parts).sql"

 if(! (test-path $filename))
 {new-item $filename -ItemType File}
 $s | out-file $filename -Force
 write-output $filename
 }

}

 

I found when I ran this that it would error on creation of a directory.

dactest.ps1 (28, 10): ERROR: At Line: 28 char: 10
ERROR: + $s | out-file $filename -Force
ERROR: + ~~~~~~~~~~~~~~~~~~~~~~~~~~
ERROR: + CategoryInfo : OpenError: (:) [Out-File], NotSupportedException
ERROR: + FullyQualifiedErrorId : FileOpenFailure,Microsoft.PowerShell.Commands.OutFileCommand
ERROR:

In order to find this exception I decided to put a Try Catch around the act of creating the file:

Try
 {
 new-item $filename -ItemType File
 }
 Catch
 {
 "Filename error $filename"
 }

 

 

What I found after putting the Try catch in place was that the exception was be cause the object names were a Url:

Filename error c:\temp\db\Service\http://schemas.microsoft.com/SQL/Notifications/EventNotificationService.sql

In order to fix this I implemented a test to see if the item name was a url

[system.uri]::IsWellFormedUriString(‘http://schemas.microsoft.com/SQL/Notifications/EventNotificationService.sql’, [system.uri]::IsWellFormedUriString)

Now that I know that the filename is a uri I can parse and get the last item in the uri for the filename:


$url = "$($r.Name.Parts)"
 if ([system.uri]::IsWellFormedUriString($url, [system.urikind]::Absolute))
 {
 $u = ([uri]"$url").Segments[-1]
 $filename = "$d\$u.sql"
 new-item $filename -ItemType File -ErrorAction Stop -Force
 }

Example output

Directory: C:\temp\db

Mode LastWriteTime Length Name
—- ————- —— —-
d—– 3/5/2018 7:10 PM Assembly
d—– 3/6/2018 9:04 AM Contract
d—– 3/5/2018 7:18 PM DataType
d—– 3/5/2018 7:19 PM Endpoint
d—– 3/5/2018 7:19 PM Filegroup
d—– 3/6/2018 9:04 AM MessageType
d—– 3/5/2018 7:20 PM Queue
d—– 3/5/2018 7:20 PM Role
d—– 3/5/2018 7:20 PM Schema
d—– 3/6/2018 9:04 AM Service
d—– 3/5/2018 7:20 PM Table
d—– 3/5/2018 7:20 PM User
d—– 3/5/2018 7:20 PM UserDefinedType
-a—- 3/5/2018 7:06 PM 0 [Microsoft.SqlServer.Types].sql

Directory: C:\temp\db\Schema

Mode LastWriteTime Length Name
—- ————- —— —-
-a—- 3/6/2018 9:11 AM 54 dbo.sql
-a—- 3/6/2018 9:11 AM 76 db_accessadmin.sql
-a—- 3/6/2018 9:11 AM 82 db_backupoperator.sql
-a—- 3/6/2018 9:11 AM 74 db_datareader.sql
-a—- 3/6/2018 9:11 AM 74 db_datawriter.sql
-a—- 3/6/2018 9:11 AM 70 db_ddladmin.sql
-a—- 3/6/2018 9:11 AM 82 db_denydatareader.sql
-a—- 3/6/2018 9:11 AM 82 db_denydatawriter.sql
-a—- 3/6/2018 9:11 AM 64 db_owner.sql
-a—- 3/6/2018 9:11 AM 80 db_securityadmin.sql
-a—- 3/6/2018 9:11 AM 58 guest.sql
-a—- 3/6/2018 9:11 AM 84 INFORMATION_SCHEMA.sql
-a—- 3/6/2018 9:11 AM 54 sys.sql

 

 

The entire script is posted in a gist:


param($dacpacPath = 'c:\dacpacPath', $dacpac = 'your.dacpac')
add-type path 'C:\Program Files (x86)\Microsoft SQL Server\120\DAC\bin\Microsoft.SqlServer.Dac.Extensions.dll'
cd $dacpacPath
$model =[Microsoft.SqlServer.Dac.Model.TSqlModel]::new(((get-item ".\$dacpac").fullname))
$queryScopes = [Microsoft.SqlServer.Dac.Model.DacQueryScopes]::All
$returnObjects = $model.GetObjects([Microsoft.SqlServer.Dac.Model.DacQueryScopes]::All)
$s = ''
foreach($r in $returnObjects)
{
if ($r.TryGetScript([ref]$s))
{
$objectTypeName = $r.ObjectType.Name;
$d="c:\temp\db\$objectTypeName"
if(!(test-path $d ))
{
new-item $d ItemType Directory
}
$filename = "$d\$($r.Name.Parts).sql"
if (! (test-path $filename))
{
Try
{
new-item $filename ItemType File ErrorAction Stop Force
}
Catch
{
$url = "$($r.Name.Parts)"
if ([system.uri]::IsWellFormedUriString($url, [system.urikind]::Absolute))
{
$u = ([uri]"$url").Segments[-1]
$filename = "$d\$u.sql"
new-item $filename ItemType File ErrorAction Stop Force
}
}
}
$s | out-file $filename Force
write-output $filename
}
}

view raw

DacToFile.ps1

hosted with ❤ by GitHub

The Power of the Round Table (AZ PowerShell)

Last night we had some technical difficulties with our user group and getting the broadcast and the speaker setup and going.    So we had to make up something good to talk about in the user group.

So what we ended up doing was what I like to call a round table.  In this discussion everyone relayed some of their successes with PowerShell.   This article is just a “glimpse’ of some of the tidbits I was able to capture during the meeting.

If you are anything like me you like things that you can do to help you remember a command you last typed or you like to bring back a command from history and modify it slightly and try again.   One of the users last night showed us this wonder full trick in powershell the #(tab).

The best way to show this feature is to tell you to use the Get-History cmdlet in PowerShell to show your last few Items you’ve typed.


PSGit:\> Get-History

Id CommandLine
-- -----------
1 $env:COMPUTERNAME
2 h
3 $env:PSModulePath
4 $env:PSModulePath -split ';'
5 h
6 ($env:PSModulePath -split ';')[0]
7 ($env:PSModulePath -split ';')[1]
8 ($env:PSModulePath -split ';')[2]
9 h
10 get-process
11 h
12 get-service

I only show the history so there is context around what this little gem of a tip does. if I type #PS and then hit tab I’ll get each item from history that contains PS.  Demonstrated below:

poundtip

Now for another tip that I found useful as well.  Have you ever wanted  to create a variable and have the output of the variable on the screen as well.  This can be done with Tee-Object but there is a much shorter method that one of the users in the AZ Powershell User group demonstrated:


PSGit:\> ($var = ($env:PSModulePath -split ';')[0])
C:\Users\crshn\Documents\WindowsPowerShell\Modules

PSGit:\> $var
C:\Users\crshn\Documents\WindowsPowerShell\Modules

Simply enclose your command in parens and you get the output in your variable and to your screen.

We had a great discussion about this post Merging hashtables. This spurned a discussion on a very cool means to copy your object intact to another object.  The participant in the user group informed me that he’d share his code with me.  When I receive it I’ll add to this Post.

Lastly another user demonstrated how they use data from the perfmon reliability counters that every windows machine has.   you can view those reliability counters through a simple command at your prompt:

perfmon /rel

Turns out these counters are part of WMI and you could drill into these to help you with diagnosing problems in your infrastructure through Powershell and WMI.

Here is a post from Richard Siddaway on how he used some of the items in the class that this provides:

https://richardspowershellblog.wordpress.com/2015/09/29/win32_reliabilityrecords-class

It was a very fun Users group. If you are online or in Phoenix area Drop by and we’ll entertain you with a speaker or any of the great folks that attend.

 

Until then

Keep Scripting

 

Thom

UCS Director – Creating a Custom Workflow Task for PowerShell

If you are familiar with UCS director then you know you can create custom workfow tasks for anything that is JavaScript based.  I haven’t seen a means to do this for a PowerShell Script.  This is how I figured this out.

Note: Without the help of my co-worker  Don Reilly the task would have never worked.  He was able to find the correct methods to call for Director. 

First thing I did was to clone the in box PowerShell Task.

2018-01-05 09_11_59-Clipboard

That brings up a dialog to allow you to choose to clone from tasks that are already there

2018-01-05 09_15_01

Now that I have that cloned I can look at the contents of the javascript to find out how to call my script.   The script I chose to run is one from another community member.  His script gets the last error from the PowerShell agent.  I took his script and saved it to my PSA server in d:\director\powershell\director folder and am using this custom work flow task to call this when there is an error.

On to the rest of the setup.   Once  I cloned the script and then put in the code that I needed with the necessary input’s from the director custom task.  I found that the task wouldn’t run.  Researching further I found that when you clone the task the PowerShell Task itself is running differently than any other custom task.  You can see this behaviour in the screen shot below:

2018-01-05 09_23_22

So I then had to goto my resident expert Don Reilly who helped me with discovering the right controller to add to my custom workflow task.  I’ll step through in pictures what my work flow in UCS Director 6.5 looks like:

2018-01-05 09_26_58

2018-01-05 09_28_50

The input for tasks has one property that i chose to just enter in its values.  In the LOV values for OutputFormat I entered in the values of XML and JSON.

2018-01-05 09_28_50-2

2018-01-05 09_28_50-3

2018-01-05 09_28_50-4

2018-01-05 09_28_50-5

The method added for the controller is before martial with the following calls to the clopia libs:


importPackage(com.cloupia.feature.powershellAgentController.wftasks);

var agentPairs = PSAgentTabularLOV.getAllPowerShellAgentsLOV();
page.setEmbeddedLOVs(id + ".psAgent", agentPairs);

Here is what my script code looks like in the custom task (Powershell is highlighted in blue in the Java script).

For Clarity here is the script$s = “D:\Director\powershell\director\Get-LastUCSDError.ps1”; if(test-path $s){. $s;}else{“Cannot find $s check path on PowerShell Agent Server”}


// Auto generated to code invoke following task
// Task Label:  Execute Native PowerShell Command
// Task Name:  Execute Native PowerShell Command
importPackage(java.lang);
importPackage(java.util);
importPackage(com.cloupia.model.cIM);
importPackage(com.cloupia.service.cIM.inframgr);

function Execute_Native_PowerShell_Command()
{
    var task = ctxt.createInnerTaskContext("Execute Native PowerShell Command");

    // Input 'Label', mandatory=true, mappableTo=
    task.setInput("Label", input.label);

    // Input 'PowerShell Agent', mandatory=true, mappableTo=gen_text_input
    task.setInput("PowerShell Agent", input.psAgent);

    // Input 'Hide Input in PSA, inframgr logs', mandatory=false, mappableTo=gen_text_input
    task.setInput("Hide Input in PSA, inframgr logs", input.isHideInput);

    // Input 'Hide Output in PSA, inframgr logs', mandatory=false, mappableTo=gen_text_input
    task.setInput("Hide Output in PSA, inframgr logs", input.isHideOutput);

    // Input 'Commands/Script', mandatory=true, mappableTo=gen_text_input
    //Changed the command to be hard coded to a script on the
	var command = '$s = "D:\\Director\\powershell\\director\\Get-LastUCSDError.ps1"; if(test-path $s){. $s;}else{"Cannot find $s check path on PowerShell Agent Server"}';
    task.setInput("Commands/Script", command);

    // Input 'Commands/Rollback Script', mandatory=false, mappableTo=gen_text_input
    task.setInput("Commands/Rollback Script", '');

    // Input 'Output Format', mandatory=false, mappableTo=
    task.setInput("Output Format", input.outputFormat);

    // Input 'Depth', mandatory=true, mappableTo=
    task.setInput("Depth", input.depth);

    // Input 'Maximum Wait Time', mandatory=true, mappableTo=
    task.setInput("Maximum Wait Time", input.maxWaitTimeMinutes);

    // Now execute the task. If the task fails, then it will throw an exception
    task.execute();

    // Now copy the outputs of the inner task for use by subsequent tasks
    // Type of following output: gen_text_input
    output.POWERSHELL_NATIVE_COMMAND_RESULT = task.getOutput("POWERSHELL_NATIVE_COMMAND_RESULT");
}

// Invoke the task
Execute_Native_PowerShell_Command();
<span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>

Monitor your Connection to Internet – PowerShell

Recently I’ve been having issues with my internet connection.  So I decided to Write a script to monitor my connection and then record how long my connection drops to my Internet Service Provider.

To start this process out I had to make sure that I could ping the gateway of the adapter that I’m using to connect to the internet with.  So first step was to find my IP address and gateway.  I was able to do this using Get-NetIPConfiguration.


Get-NetIPConfiguration -InterfaceAlias 'vEthernet (ExternalSwitch)'

InterfaceAlias : vEthernet (ExternalSwitch)
InterfaceIndex : 11
InterfaceDescription : Hyper-V Virtual Ethernet Adapter
NetProfile.Name : Conn
IPv4Address : 192.168.1.12
IPv6DefaultGateway :
IPv4DefaultGateway : 192.168.1.1
DNSServer : 192.168.1.1

This told me my address and my Gateway.  So I put them in a variable:


$IP = (Get-NetIPConfiguration -InterfaceAlias 'vEthernet (ExternalSwitch)').ipv4address.ipaddress

$gateway = (Get-NetIPConfiguration).ipv4defaultGateway.nexthop

Now that I have them in a variable I can begin the process of  Pinging both addresses.  I chose to add this to a Function:


function Start-ConnectionMonitoring
{
param($isp, $gateway, $Logfile,[int]$Delay = 10,[Ipaddress] $adapter, [switch]$ispPopup, [switch]$gateWayPopup)
$spacer = '--------------------------'
while($true)
{
if(!(Test-Connection $gateway -source $adapter -count 1 -ea Ignore))
{
get-date | Add-Content -path $Logfile
"$gateWay Connection Failure" |add-content -Path $Logfile
$outagetime = Start-ContinousPing -address $gateway -adapter $adapter -Delay $Delay
"Total Outage time in Seconds: $outageTime" | Add-Content -path $Logfile
if($gateWayPopup)
{
New-PopupMessage -location $gateway -outagetime $outagetime
}
$spacer |add-content -Path $Logfile
}
if((!(Test-Connection $isp -Source $adapter -count 1 -ea Ignore)) -and (Test-Connection $gateway -count 1 -ea Ignore))
{
get-date | Add-Content -path $Logfile
"$isp Connection Failure" | Add-Content -Path $Logfile
$outagetime = Start-ContinousPing -address $isp -adapter $adapter -Delay $Delay
"Total Outage time in Seconds: $outageTime" | Add-Content -path $Logfile
if($ispPopup)
{
New-PopupMessage -location $isp -outagetime $outagetime
}
$spacer|add-content -Path $Logfile
}
Start-Sleep -Seconds $Delay
}
}

In this function I have two Nested functions.  I’ll explain the first function(Start-ContinousPing).  If the connection/ping to either the local router ($gateway) or ($isp) fails then we call this function.  This puts the Ping/connection check in a loop until the connectivity comes back. At the end of the non connectivity the function passes back the seconds that we couldn’t reach that resource.

The Second function (New-PopupMessage) serves as a means to allow the user to choose whether or not they get a popup when there is  a period of no activity.  If the switch -ispPopup is set then when we have no connectivity to the ISP resource we’ll get a popup indicating no connection and how long the connection was out.

Finally we’ll look at the contents of the log:

12/27/2017 4:59:29 PM
192.168.1.1 Connection Failure
Total Outage time in Seconds: 0.0380652
————————–
12/27/2017 4:59:33 PM
http://www.cox.com Connection Failure
Total Outage time in Seconds: 0.0353273
————————–

As you can see the connection to my first gate way was out of .038 seconds. Also the connection to my provider cox.com was out for .035 seconds.

The entire script is located in this gist:


function Start-ConnectionMonitoring
{
param($isp, $gateway, $Logfile,[int]$Delay = 10,[Ipaddress] $adapter, [switch]$ispPopup, [switch]$gateWayPopup)
$spacer = '————————–'
while($true)
{
if(!(Test-Connection $gateway source $adapter count 1 ea Ignore))
{
get-date | Add-Content path $Logfile
"$gateWay Connection Failure" |add-content Path $Logfile
$outagetime = Start-ContinousPing address $gateway adapter $adapter Delay $Delay
"Total Outage time in Seconds: $outageTime" | Add-Content path $Logfile
if($gateWayPopup)
{
New-PopupMessage location $gateway outagetime $outagetime
}
$spacer |add-content Path $Logfile
}
if((!(Test-Connection $isp Source $adapter count 1 ea Ignore)) -and (Test-Connection $gateway count 1 ea Ignore))
{
get-date | Add-Content path $Logfile
"$isp Connection Failure" | Add-Content Path $Logfile
$outagetime = Start-ContinousPing address $isp adapter $adapter Delay $Delay
"Total Outage time in Seconds: $outageTime" | Add-Content path $Logfile
if($ispPopup)
{
New-PopupMessage location $isp outagetime $outagetime
}
$spacer|add-content Path $Logfile
}
Start-Sleep Seconds $Delay
}
}
function Start-ContinousPing
{
param($address,[ipaddress] $adapter, [int]$Delay = 10)
$currentTime = get-date
While(!(Test-Connection $address Source $adapter count 1 ea Ignore))
{
Sleep Seconds $Delay
}
$outageTime = ((get-date) $currentTime).TotalSeconds
$outageTime
}
function New-PopupMessage
{
param($location, $outagetime)
$Popup = New-Object ComObject Wscript.Shell
$popup.popup("$location Failure – seconds: $outagetime ",0,"$location",0x1)
}
$Logfile = "c:\temp\connection.log"
$isp = 'http://www.cox.com'
if(!(test-path $Logfile))
{
new-item Path $Logfile
}
$IP = (Get-NetIPConfiguration InterfaceAlias 'YourAdapterName').ipv4address.ipaddress
$gateway = (Get-NetIPConfiguration).ipv4defaultGateway.nexthop
Start-ConnectionMonitoring isp $isp gateway $gateway Logfile $Logfile adapter $IP ispPopup gateWayPopup

I Hope this helps someone.

 

Until then

Keep Scripting

 

Thom