I have a script that's designed to ping another host across a site-to-site VPN tunnel every minute. After 10 minutes, it checks the average uptime of test-connection, and if it falls below a certain threshold, it sends a Teams message telling us to check things out.
This works perfectly well when I manually run the script in situ, however, when I leave it to run as a background job, it isn't sending the Teams messages.
My question is this: as a relatively new sysadmin, the tools in my toolkit are pretty limited. Does anyone have a good tip for where I should start looking, to troubleshoot this issue? To rule out potential problems with my script, I've also included it below. But I suspect the issue is more to do with leaving the script to run on a server that I then log out of. The server in question is running Windows Server 2012 (yes I know, migration is on my to-do list).
Import-Module message_module # a module i wrote to wrap messages to Teams webhooks (included below)
# this array will accept output values from the ongoing test
$test_table = new-object system.collections.arraylist
# this index counts how many times we've checked recently
[int32[]]$test_index = 1
# our desired threshold for uptime / response
$uptime = .8
# how many minutes to count before testing
$count_length = 10
# IP to ping
$ping_ip = 'XXX.XXX.XXX.XXX'
$test_ip = '142.251.33.110' # google.com, used for testing
# here's the actual function that does the pinging and puts values in the arraylist
function Ping-VPN {
$ping_host = test-connection $ping_ip -erroraction silentlycontinue
if ( $ping_host ) {
$test_table.add(1) > $null
} else {
$test_table.add(0) > $null
}
}
# this function calculates the average of the values in test_table, and then clears them
function Get-Average-Uptime {
$sum = 0
foreach ($entry in $test_table) {
$sum += $entry
}
$avg = $sum / $test_table.count
return $avg
}
function Main-Loop {
while ( $test_index -lt $count_length ) {
Ping-VPN
$test_index += 1
start-sleep -seconds 60
}
$avguptime = Get-Average-Uptime
$test_table.clear
if ( $avguptime -lt $uptime ) {
$title = "XXX/XXX VPN Down"
$message = "XXXXXX response to ping from XXXXXXX at less than desired rate. Please investigate."
Send-TeamsMessage -Message $message -Title $title
start-sleep -seconds 3600 # sleep for an hour, to avoid spamming us
}
$test_index = 0 # restart the testing interval
Main-Loop
}
Main-Loop
And the module code:
function Send-TeamsMessage {
Param(
[Parameter(Position = 0, Mandatory = $true)][String]$Message,
[Parameter(Position = 1, Mandatory = $true)][String]$Title
)
$JSONBody = [PSCustomObject][Ordered]#{
"#type" = "MessageCard"
"#context" = "http://schema.org/extensions"
"themeColor" = '0078D7'
"title" = $Title
"text" = $Message
}
$TeamMessageBody = ConvertTo-Json $JSONBody -Depth 100
$parameters = #{
"URI" = 'XXXXXXXX (webhook URI)'
"Method" = 'POST'
"Body" = $TeamMessageBody
"ContentType" = 'application/json'
}
Invoke-RestMethod #parameters | Out-Null
}
Export-ModuleMember -Function Send-TeamsMessage
Right now, I'm calling the main file with:
start-job -file C:\path\to\file.ps1
Then minimizing the terminal and disconnecting from the server. I suspect the problem is something to do with this, that I'm missing something really obvious.
As it turns out, this question is quite similar to another, though they're phrased very differently.
Basically, what I need to do is to run the script as NT AUTHORITY\System on startup. Run an infinite command on Windows Server even if someone is logged out
Related
I have a fairly large PowerShell script that I've broken into two separate scripts. The first script ends with a call to an async function and then exits.
The function checks on the status of an e-mail export that was generated in the previous script, performing a WHILE loop where it makes a call to Google every sixty seconds to see if the export is ready. Once the export is ready it updates a couple of SQL dbs and my second script knows to take over.
This works 100% of the time when I run the "first script" in the shell/console. But I've started noticing that when my scheduled task is triggered in Task Scheduler that... nothing happens. I have extensive logging, so I know that the parameters ARE being sent over to the async function, but it seems to just poop out rather than continue to loop through the WHILE and do the every-sixty-second checks.
I feel like I've done my due diligence in Googling here, but... is there something I'm missing with a Task Scheduler job to ensure that a function containing a WHILE loop will properly run?
EDIT BELOW
To better explain what I'm doing I will include stripped code from my function and the call to the function from the main script below.
First, the call to the function at the very end of "script_01."
# Let's send the Google Vault export information over to our function.
Try {
$VaultParameters = #{
employee_name = "$employee_name"
export_name = "$export_name"
sql_id = "$sql_id"
vault_status_id = "$vault_status_id"
}
VaultExport #VaultParameters
$LoggingParameters = #{
logfile = "C:\script_logs\log.log"
log = "INFO: Sent the Google Vault export information over to our async function."
}
EventLogging #LoggingParameters
} Catch {
$LoggingParameters = #{
logfile = "C:\script_logs\log.log"
log = "ERROR: Could not send the Google Vault export information over to our async function.`n$_"
}
EventLogging #LoggingParameters
}
And now the function itself. It is large...
function VaultExport {
param (
[cmdletbinding()]
[parameter()]
[string]$employee_name,
[parameter()]
[string]$export_name,
[parameter()]
[string]$sql_id,
[parameter()]
[string]$vault_status_id
)
$scriptBlock = {
param ($employee_name,$export_name,$sql_id,$vault_status_id)
Import-Module SimplySQL
$logfile = "C:\script_logs\log.log"
$now = (Get-Date).tostring("MM-dd-yyyy hh:mm:ss")
# Let's define our MySQL database credentials for later use.
# DEFINING SQL CREDS HERE
# Let's generate secure credentials for our MySQL 'terms' db.
# GENERATING SECURE CREDS HERE
# And now we'll connect to our SQL db...
# CONNECTING TO SQL HERE
$vault_ready = "no"
Add-Content $logfile "$now INFO: Beginning the WHILE loop while $export_name completes..."
while ($vault_ready -eq "no") {
$vault_status = gam info export "Email Exports" "$export_name"
$vault_status = $vault_status -Match "COMPLETED"
$vault_status = $vault_status -split(": ")
$vault_status = $vault_status[1]
if ($vault_status -eq "COMPLETED") {
$vault_ready = "yes"
$completed = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Invoke-SqlUpdate -Query "UPDATE ``table`` SET ``vault_status`` = '$vault_status', ``vault_completed`` = '$completed' WHERE ``id`` = '$vault_status_id'"
Invoke-SqlUpdate -Query "UPDATE ``table`` SET ``vault_status`` = '1' WHERE ``id`` = '$sql_id'"
$now = (Get-Date).tostring("MM-dd-yyyy hh:mm:ss")
Add-Content $logfile "$now INFO: $export_name is ready to download. Updated vault_status in our dbs."
} else {
$vault_status = gam info export "Email Exports" "$export_name"
$vault_status = $vault_status -Match "IN_PROGRESS"
$vault_status = $vault_status -split(": ")
$vault_status = $vault_status[1]
Invoke-SqlUpdate -Query "UPDATE ``table`` SET ``vault_status`` = '$vault_status' WHERE ``id`` = '$vault_status_id'"
$now = (Get-Date).tostring("MM-dd-yyyy hh:mm:ss")
Add-Content $logfile "$now INFO: $export_name is not yet ready: ($vault_status). Checking again in sixty seconds."
Start-Sleep 60
}
}
}
Start-Job -ScriptBlock $scriptBlock -ArgumentList #($employee_name,$export_name,$sql_id,$vault_status_id)
}
exit
I'm unsure why you wouldn't just have the task execute every 30 seconds instead of having a process run indefinitely and using it's own timers
The condition you are setting for the WHILE statement is being met, this is why the loop doesn't continue.
Change the condition to something that will never be met and the problem should go away, i.e.
$Value1 = 0
WHILE($Value1 -ne 1){
#do things, never updating the $value1 variable
}
I have been struggling for a few days now with running Start-Job with a -Scriptblock.
If I run this, I get today's date back in Receive-Job:
$sb = {Get-Date}
Start-Job -ScriptBlock $sb
Now, I have a script which does some reporting via 65+ API calls for 8 different statuses. In total, it does 500+ API calls and takes almost 20mins to complete (2-5seconds per API call, x 500), so I am looking to run each foreach block in parallel.
The script works well as a script but not as a Start-Job.
The Scriptblock will get a session token, gets data from API server in a foreach loop and then populates the results into a $Global: variable as it goes.
When I run the job, it "Completes" instantly and Has More Data is False (so I can't see any error messages):
67 Job67 BackgroundJob Completed False localhost foreach ($item....
Any suggestions as to where I am going wrong?
My sample code looks like this:
$sb = {# Get count of Status1
foreach ($item in $Global:GetItems) {
$item1 = $item.id
If ($null -eq $TokenExpiresIn) { .\Get-Token.ps1 }
$Global:TokenExpiresIn = New-TimeSpan -Start $TokenExpiresNow -End $Script:TokenExpiresTime
$Method = "GET"
$Fromitems = $item1
$Status = "foo"
$Limit = "1"
$Fields = "blah"
$URL = "https://$APIServer/apis/v1.1/status?customerId=1&itemId=$Fromitems&status=$Status&fields=$Fields&limit=$Limit"
$Header = #{"Accept" = "*/*"; "Authorization" = "Bearer $SessionToken" }
$itemStatsResponse = Invoke-WebRequest -Method $Method -Uri $URL -Header $Header
$itemStatsJSON = ConvertFrom-Json -InputObject $itemStatsResponse
$itemCount = $itemStatsJSON.metadata.count
$Global:GetItems | Where-Object -Property id -EQ $item1 | Add-Member -MemberType NoteProperty "foo" -Value $itemCount
}
}
I am running the Scriptblock as follows:
Start-Job -Name "foo" -ScriptBlock $sb
I am using PowerShell 7.1. The script as a whole runs successfully through all 500+ API calls, however it's a bit slow, so I am also looking at how I can "milti-thread" my API calls moving forward for better performance.
Thanks in advance for your support/input. The other posts on StackOverflow relating to PowerShell and Scriptblocks have not assisted me thus far.
I wrote a PowerShell script which uses APIs to parse through non-compliant (NC) mobile device logs from our MDM to determine if a device has been NC for over 60 days and delete it. We realized that current compliant devices that become NC might have an older record from a previous time they were NC. We only want to grab the latest log entry. Any help to understand how to only grab the most recent NC entry would be great.
Have not been able to write code that would only grab the latest non-compliant entry from the logs. Currently it will grab all NC log entries ≥ 60 days from each device that is NC. In the function I return the serial number of the device if it has been NC for over 60 days.
The code below is the function that I wrote to parse the event logs and does work, minus the fact that it grabs any older NC log entry ≥60 days. I'm assuming I could put some code in the foreach statement.
function get_events {
Param([string]$serial)
$sixtydays = (Get-Date).AddDays(-60)
$url = "https://$ourmdm/api/mdm/devices/eventlog?searchby=serialnumber&id=$serial&pagesize=10000&dayrange=365"
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("tenant-code", $Code)
$headers.Add("Authorization", $Auth)
try {
$response = Invoke-RestMethod -Uri $url -Headers $headers
} catch {
$null = "Device Info Not Found"
}
$ServicePoint = [System.Net.ServicePointManager]::FindServicePoint($url)
$ServicePoint.CloseConnectionGroup("") | Out-Null
$data = $response.DeviceEventLogSearchResult.DeviceEventLogEntries
foreach ($event in $data) {
if ($event.Event -eq 'ComplianceStatusChanged') {
if ($event.EventDataList.NewValue -eq 'NonCompliant') {
$request = $event.Timestamp
$eventdate = [DateTime]$request
if ($eventdate -lt $sixtydays) {
return $serial
}
}
}
}
}
This tool has 2 possible options;
Reset 1 Access Point
Reset ALL Access Points at a site
For the sake of record keeping, I have a function that sends an email alert when either of these events occur.
Reset a single AP:
Function Manage-APReset {
Write-Verbose "Function start: Manage-APReset"
Write-Host "Executing access point reset for $apName .."
IF($controllerName -eq $null) {
Error-NoCon }
else {
## Establish connection(s)
[string]$cUser = "srv-****"
$cPassword = ConvertTo-SecureString -String "X***********" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential ($cUser, $cPassword)
Write-Host "Establishing SSH connection to Cisco Controller $controllerName"
New-SSHSession -ComputerName $controllerName -Credential $cred
$session = Get-SSHSession -Index 0
$stream = $session.Session.CreateShellStream("PS-SSH", 0, 0, 0, 0, 100)
sleep 4
Write-Host "Connected. Authenticating for SSH Stream.."
## Invoke login
$stream.WriteLine('srv-*****')
sleep 3
$stream.WriteLine('X********j8')
sleep 2
$Stream.Read()
Write-Host "Authenticated!"
## Invoke commands
$stream.WriteLine("config ap reset $apName")
sleep 2
$stream.WriteLine('y')
sleep 3
$stream.Read()
Write-Host "$apName has been reset successfully. Please allow up to 5 minutes to come back up"
Admin-SendAlert($event = 1)
Remove-SSHSession -SessionId 0,1,2,3,4,5,6,7,8,9
Repeat
}
}
You'll notice at the end I call Admin-SendAlert which handles the email alert. During this time I pass $event = 1 to allow Admin-SendAlert to know what condition is occurring.
Function Admin-SendAlert {
Write-Verbose "Function started: Admin-SendAlert"
## Event 1: Single AP Reset Successfully
if($event = 1) {
$eventSub = "Single: $apName has been reset"
$eventBod = "$apName has been reset successfully by $tech`n Reason Summary: $reasonSum"
}
if($event = 2) {
$eventSub ="Full Store Reset: $Store All APs Reset"
$eventBod = "The following APs have been reset at Store $Store by user $tech. `n`nAll APs:`n $apArray`n Reason Summary: $reasonSum"
}
Send-MailMessage -To "CSOC <blank#email.com>" -From "AP Manager Beta <srv-blank#email.com>" -Subject $eventSub -SmtpServer smtp.email.com -Body $eventBod
}
I don't believe this is how this should be handled as the value of $event remains whichever comes first. How should I be doing this?
Your main issue is that you are using the assignment operator in place of the equality comparator. So if($event = 1) should be if($event -eq 1)
I see room for improvement as well for you if clauses. You are checking the numerical value of $event. It will only be one of those values. Never two. Your if clauses are mutually exclusive yet you attempt to evaluate both -eq 1 and -eq 2. Not the best idea as it make for muddy code. You should be using if and elseif to contain it in the same block
if($event -eq 1) {
# Stuff happens
} elseif($event -eq 2) {
# Stuff happens
} else {
# Last resort
}
Further to that, if yourself with too many elseif clauses, you would be better off using switch
switch($event){
1 {
# Stuff happens
break
}
2 {
# Stuff happens
break
}
default {
# Last resort
}
}
Note: if you don't use break it will evaluate all switch conditions and execute all that match.
I'm trying to put together a script to monitor the MSMQ on a server. I found this and it works like a charm but I also need to get the time of arrival.
When using the Get-Member cmdlet I get a list of properties but none of the seem to get me what I need.
Does anyone know how to get time of arrival?
/G
Finally figured it out.
[String]$cName = $Env:COMPUTERNAME
[Reflection.Assembly]::LoadWithPartialName("System.Messaging") | out-null
[System.Messaging.MessageQueue[]]$queues = [System.Messaging.MessageQueue]::GetPrivateQueuesByMachine($cName.ToLower())
Foreach ($queue in $queues) {
$queue.MessageReadPropertyFilter.SetAll()
try {
[System.Messaging.Message]$message = $queue.Peek(10)
Write-Host $queue.QueueName $message.ArrivedTime
}
catch {
#Write-Host "Timeout"
}
}
The trick is to set the MessageReadPropertyFilter. It is possible to set each property separately but this will do for now.
$qname = "<YourQueueNameHere>"
[Reflection.Assembly]::LoadWithPartialName("System.Messaging") | out-null
$q = new-object System.Messaging.MessageQueue($qname)
$msgs = $q.GetAllMessages()
foreach ( $msg in $msgs )
{
Write-Host $msg.ArrivedTime
}
Documentation for Message here.
Documentation for Message.ArrivedTime here.
Here is what worked for me. In my case, the script counts how many messages have been received for the past hour.
$QueueArray = "MyQueue1.ERRORS", "MyQueue2.ERRORS";
foreach ($queue in $QueueArray)
{
# Create each MSMQ object
[System.Reflection.Assembly]::LoadWithPartialName("System.Messaging") | Out-Null
$MessageQueue = New-Object System.Messaging.MessageQueue (".\private$\" + $queue)
# Get the amount of the messages in the queue for the past hour.
$MessageQueue.MessageReadPropertyFilter.SetAll()
$PastHour = (Get-Date).AddHours(-1)
$countFailedMessages = 0
foreach ($message in $MessageQueue )
{
if ($PastHour -lt $message.ArrivedTime)
{
#Write-Host $message.ArrivedTime
$countFailedMessages++
#Write-Host $countFailedMessages
}
}
Write-Host $queue `t $countFailedMessages "failed messages for the past hour"
}