Version History:
2021.01.21.1153 - Improved logging for DurationMinutes data.
2021.01.08.2347 - Added $DurationMinutesMaxAllowed. Thanks, Richard!
2020.08.10.1342 - Improved MG group connection.
2020.08.07.1454 - Updated Get-SCOMMaintenanceMode to included related objects so that MM window update would be recursive
2020.08.06.1419 - Updated logging.
2020.08.05.1405 - Integrated WriteStatusEvent logic into this script instead of using the separate WriteAction.
2020.08.04.1234 - Added 'Force' param. If object is already in MM window, will updated new End time to agree with DurationMinutes. Added property bag.
2020.07.22.0000 - Added activity #2 above to write current mm status to agent. Added property bag for composite DS. Added SpreadInitializationOverIntervalSeconds.
2020.07.21.0000 - Initial version
#>
# How long the MM window will be
[int]$DurationMinutes,
[int]$DurationMinutesMaxAllowed,
# If Computer is already in MM, force the End time to agree with the DurationMinutes
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false,
Position=0,
ParameterSetName='Parameter Set 1')]
[ValidateSet("True", "False")]
[string]$Force = 'False',
# Required for initiating MM
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false,
Position=0,
ParameterSetName='Parameter Set 1')]
[ValidateSet("PlannedOther","UnplannedOther","PlannedHardwareMaintenance","UnplannedHardwareMaintenance","PlannedHardwareInstallation","UnplannedHardwareInstallation","PlannedOperatingSystemReconfiguration","UnplannedOperatingSystemReconfiguration","PlannedApplicationMaintenance","UnplannedApplicationMaintenance","ApplicationInstallation","ApplicationUnresponsive","ApplicationUnstable","SecurityIssue","LossOfNetworkConnectivity")]
[string]$Reason = "PlannedOther",
# This is a clever way to force the workflow to randomize a delay (seconds) in progress/activity. If a large number of instances trigger at once, the DB could become sluggish. This will help balance load if needed.
[int]$SpreadInitializationOverIntervalSeconds = 0,
# User context that initiated the MM activity
[string]$UserName,
# This is used here to help determine if it is needed to launch the WriteMMStatus task.
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false,
ParameterSetName='Parameter Set 1')]
[ValidateSet("Workflows","MgmtPerspectiveOnly","None")]
[string]$Verify ='MgmtPerspectiveOnly',
# After making changes to MM status, wait for mgmt server to recognize this (backend DB really) before proceding. Also this is used for waiting for the agent Task to complete.
[int]$WaitForStatusToUpdateSeconds,
# This is very useful in logging activity to identify the workflow that created the log entries.
[string]$WorkflowName,
[string]$WriteToEventLog = 'False'
)
# Convert to boolean
[Bool]$WriteToEventLog = [System.Convert]::ToBoolean($WriteToEventLog)
# Force Duration to within max and min allowed values.
$AdjustedDurationMinutes = [math]::Min(([math]::Max($DurationMinutes,$DurationMinutesMinAllowed)), $DurationMinutesMaxAllowed)
If (-NOT $WinComp) {
LogIt -EventID 9996 -Type $critical -Message "FAILURE. Unable to find Microsoft.Windows.Computer object with name [$($AgentDisplayName)] . Exiting." -Proceed $true -Line $(_LINE_)
Exit
}
# Either Start or End the MM period
Switch ($Action) {
"Start" {
$EndMMDateStamp = ((Get-Date).AddMinutes($AdjustedDurationMinutes))
If ($WinComp.InMaintenanceMode) {
$tmpMessage = "Maintenance mode already active on Microsoft.Windows.Computer object with name [$($AgentDisplayName)]. 'FORCE' param [$($Force)]."
Switch ($Force){
# Update existing MM. Force the existing MM 'window' to agree with the DurationMinutes
'True' {
Try {
# Update MM for object and any associated objects. This is effectively a recursive action.
$IsInMM = Get-SCOMMaintenanceMode -Instance ($WinComp + ($WinComp.GetRelatedMonitoringObjects()))
Set-SCOMMaintenanceMode -MaintenanceModeEntry $IsInMM -EndTime $EndMMDateStamp -Comment $Comment -Reason $Reason -ErrorAction Stop
$MMWindowUpdated = 'True'
LogIt -EventID 9992 -Type $info -Message "$($tmpMessage) Updated End time: [$($EndMMDateStamp.ToString())]." -Proceed $WriteToEventLog -Line $(_LINE_)
Break;
} Catch {
LogIt -EventID 9992 -Type $info -Message "$($tmpMessage) Failed to update End time: [$($EndMMDateStamp.ToString())]." -Proceed $WriteToEventLog -Line $(_LINE_)
Break;
}
}
Default {
LogIt -EventID 9992 -Type $info -Message "$($tmpMessage) No action taken. Use '-Force' parameter to update the MM window. Exiting now." -Proceed $WriteToEventLog -Line $(_LINE_)
}
}
}
# It is assumed that the object is not already in MM
Else {
Try {
Start-SCOMMaintenanceMode -Instance $WinComp -EndTime $EndMMDateStamp -Comment $Comment -Reason $Reason -ErrorAction Stop
LogIt -EventID 9992 -Type $info -Message "Maintenance mode started on Microsoft.Windows.Computer object with name [$($AgentDisplayName)]." -Proceed $WriteToEventLog -Line $(_LINE_)
} Catch {
LogIt -EventID 9996 -Type $critical -Message "FAILURE initiating maintenance mode on Microsoft.Windows.Computer object with name [$($AgentDisplayName)]. Exiting." -Proceed $true -Line $(_LINE_)
}
}
}#end match
"End" {
If (-NOT ($WinComp.InMaintenanceMode)) {
LogIt -EventID 9992 -Type $info -Message "Maintenance not active on Microsoft.Windows.Computer object with name [$($AgentDisplayName)]. No action taken. Exiting." -Proceed $WriteToEventLog -Line $(_LINE_)
}
Try {
$WinComp.StopMaintenanceMode([DateTime]::Now.ToUniversalTime(),[Microsoft.EnterpriseManagement.Common.TraversalDepth]::Recursive);
LogIt -EventID 9992 -Type $info -Message "Maintenance mode ended on Microsoft.Windows.Computer object with name [$($AgentDisplayName)]." -Proceed $WriteToEventLog -Line $(_LINE_)
} Catch {
Try {
Add-PSSnapin "Microsoft.EnterpriseManagement.OperationsManager.Client"
$WinComp.StopMaintenanceMode([DateTime]::Now.ToUniversalTime(),[Microsoft.EnterpriseManagement.Common.TraversalDepth]::Recursive);
LogIt -EventID 9992 -Type $info -Message "Maintenance mode ended on Microsoft.Windows.Computer object with name [$($AgentDisplayName)]." -Proceed $WriteToEventLog -Line $(_LINE_)
} Catch {
LogIt -EventID 9996 -Type $critical -Message "FAILURE ending maintenance mode on Microsoft.Windows.Computer object with name [$($AgentDisplayName)]. Exiting." -Proceed $true -Line $(_LINE_)
}
}
}#end match
}#end Switch
#endregion ModifyMM
#region AgentTask
#----------------------------- Agent Task Region -----------------------------
If ( ($Verify -match 'None') -OR ( ($Verify -match 'Workflows') -AND ($MMWindowUpdated -match 'False') )) {
$explanation = @"
Verify: [$($Verify)], Force:[$($Force)], MMWindowUpdated:[$($MMWindowUpdated)]. No need to write status to agent log so exiting now.
Explanation:
If the user indicated to 'Verify Workflows' in combination with Force, this gets tricky. If the object was already in MM,
the duration would simply be updated but no workflow activity events would result on the agent so the powershell command would never verify/detect 1215/1216 events.
Check if the object was already in MM and duration was updated successfully. If so, ignore 'Verify Workflows' settings, proceed with triggering task to write to event log
because the posh module on the agent will depend on this data.
"@
LogIt -EventID 9991 -Type $info -Message $explanation -Proceed $WriteToEventLog -Line $(_LINE_)
$MG.Dispose()
Exit
}
# At this point it would be appropriate to trigger task to write status event
########## Launch agent task to write current status to event log ##########
# If Action is provided it is assumed that MM change was recently triggered, the script will try to wait for the status to be updated so that the resulting status message will be completely accurate.
# Otherwise it is assumed that the intention is to simply trigger a status message with no other action taken.
If ($Action -match 'Enable|Disable') {
switch ($Action){
'Enable' {
$ExpectedStatus = $true
}
'Disable' {
$ExpectedStatus = $false
}
}
If (-NOT (($WinComp.InMaintenanceMode) -eq $ExpectedStatus)) {
LogIt -EventID 9992 -Type $info -Message "WinComp.DisplayName: $($WinComp.DisplayName), 'InMaintenanceMode' status [$($WinComp.InMaintenanceMode.ToString())] is not yet changed to expected status [$($ExpectedStatus)]. Will keep checking every [$($pauseForStatusCheckSeconds)] seconds until timeout [$($WaitForStatusToUpdateSeconds) seconds] is expired. " -Proceed $WriteToEventLog -Line $(_LINE_)
}
$timer = [System.Diagnostics.Stopwatch]::StartNew()
# Loop for a reasonable amount of time to get expected MM status, based on "Action" parameter.
While ( (-NOT (($WinComp.InMaintenanceMode) -eq $ExpectedStatus)) -AND ($timer.Elapsed.TotalSeconds -le ($WaitForStatusToUpdateSeconds + $RandSleep) ) ) {
Start-Sleep -Seconds $pauseForStatusCheckSeconds
$WinComp = $MG.GetMonitoringObjects($objCriteria)
}
}
# Set up override for task: status in param[1]. Include data indicating if an existing MM window was updated (param[2]).
$hashOverrides = @{
Message = "$($mmStatus)^PreviousMMSettingsUpdated:$($MMWindowUpdated)^$($ThisScriptInstanceGUID)"
}
#endregion AgentTask
$MG.Dispose()
LogIt -EventID 9991 -Type $info -Message "End script." -Proceed $WriteToEventLog -Line $(_LINE_) </Script></ScriptBody>
<Parameters>
<Parameter>
<Name>Action</Name>
<Value>$Config/Action$</Value>
</Parameter>
<Parameter>
<Name>AgentDisplayName</Name>
<Value>$Config/AgentDisplayName$</Value>
</Parameter>
<Parameter>
<Name>Comment</Name>
<Value>$Config/Comment$</Value>
</Parameter>
<Parameter>
<Name>DurationMinutes</Name>
<Value>$Config/DurationMinutes$</Value>
</Parameter>
<Parameter>
<Name>DurationMinutesMaxAllowed</Name>
<Value>$Config/DurationMinutesMaxAllowed$</Value>
</Parameter>
<Parameter>
<Name>Force</Name>
<Value>$Config/Force$</Value>
</Parameter>
<Parameter>
<Name>Reason</Name>
<Value>$Config/Reason$</Value>
</Parameter>
<Parameter>
<Name>SpreadInitializationOverIntervalSeconds</Name>
<Value>$Config/SpreadInitializationOverIntervalSeconds$</Value>
</Parameter>
<Parameter>
<Name>UserName</Name>
<Value>$Config/UserName$</Value>
</Parameter>
<Parameter>
<Name>Verify</Name>
<Value>$Config/Verify$</Value>
</Parameter>
<Parameter>
<Name>WaitForStatusToUpdateSeconds</Name>
<Value>$Config/WaitForStatusToUpdateSeconds$</Value>
</Parameter>
<Parameter>
<Name>WorkflowName</Name>
<Value>$Config/WorkflowName$</Value>
</Parameter>
<Parameter>
<Name>WriteToEventLog</Name>
<Value>$Config/WriteToEventLog$</Value>
</Parameter>
</Parameters>
<TimeoutSeconds>$Config/WriteActionTimeoutSeconds$</TimeoutSeconds>
</WriteAction>
<!--<WriteAction ID="POSH_WriteNewStatusEvent" TypeID="Windows!Microsoft.Windows.PowerShellWriteAction">
<ScriptName>TriggerAgentTask-WriteMMStatus.ps1</ScriptName>
<ScriptBody><Script><![CDATA[<#
Script: TriggerAgentTask-WriteMMStatus.ps1
Description: This is for the WriteAction on the mgmt server to trigger the agent task which will write the MM status to the event log
Author: Tyson Paul
Version History:
2020.08.04.1531 - Added 'Verify' param to enable immediate exit if $Verify -match 'Workflows|None'. No point running this task in this case.
Added logic so task would trigger if an existing MM got updated and the Verify Workflows param was used. The posh module on agent will depend on the event log as no workflow activity events will occur from an 'update' to a MM window.
2020.07.31.1401 - Added logic to account for no Action param value; will immediately trigger status message.
2020.07.22.1935 - Added SpreadInitializationOverIntervalSeconds, WaitForStatusToUpdateSeconds
2020.07.20 - Original
Note: Why use 'Verify=MgmtPerspectiveOnly'? This would be useful if user is rebooting/poweroff a Computer. There's no need to wait for workflows to unload. The only concern is that the
mgmt server knows the object is in MM, and thus it won't alert about heartbeats. This relatively fast response to the agent log will indicate that it's 'safe' to power off the Computer
without waiting for workflows to unload.
#>
Param (
[string]$Action,
[string]$AgentDisplayName,
# True|False. This indicates if the object was already in MM but was simply updated.
[string]$PreviousMMSettingsUpdated = 'False',
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false,
ValueFromRemainingArguments=$false,
ParameterSetName='Parameter Set 1')]
[ValidateSet("Workflows","MgmtPerspectiveOnly","None")]
[string]$Verify='MgmtPerspectiveOnly',
Function Get-CurrentLineNumber {
$MyInvocation.ScriptLineNumber
}
########################################################################################################
New-Alias -Name _LINE_ -Value Get-CurrentLineNumber -Description 'Returns the current line number in a PowerShell script file.' -ErrorAction SilentlyContinue
########################################################################################################
[int]$info = 0
[int]$Critical = 1
[int]$warn = 2
[string]$whoami = whoami.exe
$ScriptName ='TriggerAgentTask-WriteMMStatus.ps1'
$ThisScriptInstanceGUID = (New-Guid).Guid.Substring(((New-Guid).Guid.Length ) -6).ToUpper()
[int]$maxLogLength = 31000 #max chars to allow for event log messages
$pauseForStatusCheckSeconds = 15
<#
If the user indicated to Verify Workflows in combination with Force, this gets tricky. If the object was already in MM,
the duration would simply be updated but no workflow activity events would result on the agent so the powershell command would never verify/detect 1215/1216 events.
Check if the object was already in MM and duration was updated. If so, ignore 'Verify' settings, proceed with triggering task to write to event log.
The posh module on the agent will depend on this data because no workflow activity events will be written after a MM settings update.
#>
If ( ($Verify -match 'None') -OR ( ($Verify -match 'Workflows') -AND ($PreviousMMSettingsUpdated -match 'False') )) {
LogIt -EventID 9991 -Type $info -Message "Verify: [$($Verify)]. No point writing status to agent log. Abandoning this script. Exiting." -Proceed $WriteToEventLog -Line $(_LINE_)
Exit
}
# If Action is provided it is assumed that MM change was recently triggered, the script will try to wait for the status to be updated so that the resulting status message will be completely accurate.
# Otherwise it is assumed that the intention is to simply trigger a status message with no other action taken.
If ($Action -match 'Enable|Disable') {
switch ($Action){
'Enable' {
$ExpectedStatus = $true
}
'Disable' {
$ExpectedStatus = $false
}
}
If (-NOT (($WinComp.InMaintenanceMode) -eq $ExpectedStatus)) {
LogIt -EventID 9992 -Type $info -Message "WinComp.DisplayName: $($WinComp.DisplayName), 'InMaintenanceMode' status [$($WinComp.InMaintenanceMode.ToString())] is not yet changed to expected status [$($ExpectedStatus)]. Will keep checking every [$($pauseForStatusCheckSeconds)] seconds until timeout [$($WaitForStatusToUpdateSeconds) seconds] is expired. " -Proceed $WriteToEventLog -Line $(_LINE_)
}
$timer = [System.Diagnostics.Stopwatch]::StartNew()
# Loop for a reasonable amount of time to get expected MM status, based on "Action" parameter.
While ( (-NOT (($WinComp.InMaintenanceMode) -eq $ExpectedStatus)) -AND ($timer.Elapsed.TotalSeconds -le ($WaitForStatusToUpdateSeconds + $RandSleep) ) ) {
Start-Sleep -Seconds $pauseForStatusCheckSeconds
$WinComp = $MG.GetMonitoringObjects($objCriteria)
}
$timer.Stop()
}
# Set up override for task: status in param[1]. Include data indicating if an existing MM window was updated (param[2]).
$hashOverrides = @{
Message = "$($mmStatus)^PreviousMMSettingsUpdated:$($PreviousMMSettingsUpdated)"
}