diff --git a/doc/19-technical-concepts.md b/doc/19-technical-concepts.md
index 470e820c456..c3666fae0c8 100644
--- a/doc/19-technical-concepts.md
+++ b/doc/19-technical-concepts.md
@@ -1550,6 +1550,40 @@ Message updates will be dropped when:
* Notification does not exist.
* Origin endpoint is not within the local zone.
+#### event::ClearLastNotifiedStates
+
+> Location: `clusterevents.cpp`
+
+##### Message Body
+
+Key | Value
+----------|---------
+jsonrpc | 2.0
+method | event::ClearLastNotifiedStates
+params | Dictionary
+
+##### Params
+
+Key | Type | Description
+-------------|--------|------------------
+notification | String | Notification name
+
+Used to sync the state of a notification object within the same HA zone.
+
+##### Functions
+
+Event Sender: `Notification::OnLastNotifiedStatesCleared`
+Event Receiver: `LastNotifiedStatesClearedAPIHandler`
+
+##### Permissions
+
+The receiver will not process messages from not configured endpoints.
+
+Message updates will be dropped when:
+
+* Notification does not exist.
+* Origin endpoint is not within the local zone.
+
#### event::SetForceNextCheck
> Location: `clusterevents.cpp`
diff --git a/lib/icinga/clusterevents.cpp b/lib/icinga/clusterevents.cpp
index 2eac7243414..d8829b6281d 100644
--- a/lib/icinga/clusterevents.cpp
+++ b/lib/icinga/clusterevents.cpp
@@ -30,6 +30,7 @@ REGISTER_APIFUNCTION(SetSuppressedNotifications, event, &ClusterEvents::Suppress
REGISTER_APIFUNCTION(SetSuppressedNotificationTypes, event, &ClusterEvents::SuppressedNotificationTypesChangedAPIHandler);
REGISTER_APIFUNCTION(SetNextNotification, event, &ClusterEvents::NextNotificationChangedAPIHandler);
REGISTER_APIFUNCTION(UpdateLastNotifiedStateOfUser, event, &ClusterEvents::LastNotifiedStateOfUserUpdatedAPIHandler);
+REGISTER_APIFUNCTION(ClearLastNotifiedStates, event, &ClusterEvents::LastNotifiedStatesClearedAPIHandler);
REGISTER_APIFUNCTION(SetForceNextCheck, event, &ClusterEvents::ForceNextCheckChangedAPIHandler);
REGISTER_APIFUNCTION(SetForceNextNotification, event, &ClusterEvents::ForceNextNotificationChangedAPIHandler);
REGISTER_APIFUNCTION(SetAcknowledgement, event, &ClusterEvents::AcknowledgementSetAPIHandler);
@@ -52,6 +53,7 @@ void ClusterEvents::StaticInitialize()
Notification::OnSuppressedNotificationsChanged.connect(&ClusterEvents::SuppressedNotificationTypesChangedHandler);
Notification::OnNextNotificationChanged.connect(&ClusterEvents::NextNotificationChangedHandler);
Notification::OnLastNotifiedStateOfUserUpdated.connect(&ClusterEvents::LastNotifiedStateOfUserUpdatedHandler);
+ Notification::OnLastNotifiedStatesCleared.connect(&ClusterEvents::LastNotifiedStatesClearedHandler);
Checkable::OnForceNextCheckChanged.connect(&ClusterEvents::ForceNextCheckChangedHandler);
Checkable::OnForceNextNotificationChanged.connect(&ClusterEvents::ForceNextNotificationChangedHandler);
Checkable::OnNotificationsRequested.connect(&ClusterEvents::SendNotificationsHandler);
@@ -589,6 +591,57 @@ Value ClusterEvents::LastNotifiedStateOfUserUpdatedAPIHandler(const MessageOrigi
return Empty;
}
+void ClusterEvents::LastNotifiedStatesClearedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin)
+{
+ auto listener (ApiListener::GetInstance());
+
+ if (!listener) {
+ return;
+ }
+
+ Dictionary::Ptr params = new Dictionary();
+ params->Set("notification", notification->GetName());
+
+ Dictionary::Ptr message = new Dictionary();
+ message->Set("jsonrpc", "2.0");
+ message->Set("method", "event::ClearLastNotifiedStates");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, notification, message, true);
+}
+
+Value ClusterEvents::LastNotifiedStatesClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
+{
+ auto endpoint (origin->FromClient->GetEndpoint());
+
+ if (!endpoint) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user cleared' message from '"
+ << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";
+
+ return Empty;
+ }
+
+ if (origin->FromZone && origin->FromZone != Zone::GetLocalZone()) {
+ Log(LogNotice, "ClusterEvents")
+ << "Discarding 'last notified state of user cleared' message from '"
+ << origin->FromClient->GetIdentity() << "': Unauthorized access.";
+
+ return Empty;
+ }
+
+ auto notification (Notification::GetByName(params->Get("notification")));
+
+ if (!notification) {
+ return Empty;
+ }
+
+ notification->GetLastNotifiedStatePerUser()->Clear();
+ Notification::OnLastNotifiedStatesCleared(notification, origin);
+
+ return Empty;
+}
+
void ClusterEvents::ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin)
{
ApiListener::Ptr listener = ApiListener::GetInstance();
diff --git a/lib/icinga/clusterevents.hpp b/lib/icinga/clusterevents.hpp
index 0d67681b24e..2cb3a3a277f 100644
--- a/lib/icinga/clusterevents.hpp
+++ b/lib/icinga/clusterevents.hpp
@@ -44,6 +44,9 @@ class ClusterEvents
static void LastNotifiedStateOfUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin);
static Value LastNotifiedStateOfUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static void LastNotifiedStatesClearedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
+ static Value LastNotifiedStatesClearedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+
static void ForceNextCheckChangedHandler(const Checkable::Ptr& checkable, const MessageOrigin::Ptr& origin);
static Value ForceNextCheckChangedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
diff --git a/lib/icinga/notification.cpp b/lib/icinga/notification.cpp
index 2338f280bfb..f0c8dc00ca1 100644
--- a/lib/icinga/notification.cpp
+++ b/lib/icinga/notification.cpp
@@ -24,6 +24,7 @@ std::map Notification::m_TypeFilterMap;
boost::signals2::signal Notification::OnNextNotificationChanged;
boost::signals2::signal Notification::OnLastNotifiedStateOfUserUpdated;
+boost::signals2::signal Notification::OnLastNotifiedStatesCleared;
String NotificationNameComposer::MakeName(const String& shortName, const Object::Ptr& context) const
{
@@ -232,6 +233,13 @@ void Notification::BeginExecuteNotification(NotificationType type, const CheckRe
<< "notifications of type '" << notificationTypeName
<< "' for notification object '" << notificationName << "'.";
+ if (type == NotificationRecovery) {
+ auto states (GetLastNotifiedStatePerUser());
+
+ states->Clear();
+ OnLastNotifiedStatesCleared(this, nullptr);
+ }
+
Checkable::Ptr checkable = GetCheckable();
if (!force) {
@@ -469,19 +477,14 @@ void Notification::BeginExecuteNotification(NotificationType type, const CheckRe
/* collect all notified users */
allNotifiedUsers.insert(user);
- switch (type) {
- case NotificationProblem:
- case NotificationRecovery: {
- auto [host, service] = GetHostService(checkable);
- uint_fast8_t state = service ? service->GetState() : host->GetState();
+ if (type == NotificationProblem) {
+ auto [host, service] = GetHostService(checkable);
+ uint_fast8_t state = service ? service->GetState() : host->GetState();
- if (state != GetLastNotifiedStatePerUser()->Get(userName)) {
- GetLastNotifiedStatePerUser()->Set(userName, state);
- OnLastNotifiedStateOfUserUpdated(this, userName, state, nullptr);
- }
+ if (state != GetLastNotifiedStatePerUser()->Get(userName)) {
+ GetLastNotifiedStatePerUser()->Set(userName, state);
+ OnLastNotifiedStateOfUserUpdated(this, userName, state, nullptr);
}
- default:
- ;
}
/* store all notified users for later recovery checks */
diff --git a/lib/icinga/notification.hpp b/lib/icinga/notification.hpp
index f3e89018bd1..01446801838 100644
--- a/lib/icinga/notification.hpp
+++ b/lib/icinga/notification.hpp
@@ -94,6 +94,7 @@ class Notification final : public ObjectImpl
static boost::signals2::signal OnNextNotificationChanged;
static boost::signals2::signal OnLastNotifiedStateOfUserUpdated;
+ static boost::signals2::signal OnLastNotifiedStatesCleared;
void Validate(int types, const ValidationUtils& utils) override;
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 1f780efb2c1..e683edda341 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -140,6 +140,7 @@ add_boost_test(base
icinga_notification/type_filter
icinga_notification/duplicate_dueto_filter
icinga_notification/duplicate_dueto_filter_volatile
+ icinga_notification/duplicate_dueto_filter_recovery
icinga_macros/simple
icinga_legacytimeperiod/simple
icinga_legacytimeperiod/advanced
diff --git a/test/icinga-notification.cpp b/test/icinga-notification.cpp
index 1c82443363f..33f74718248 100644
--- a/test/icinga-notification.cpp
+++ b/test/icinga-notification.cpp
@@ -197,4 +197,39 @@ BOOST_AUTO_TEST_CASE(duplicate_dueto_filter_volatile)
BOOST_CHECK(helper.called == 2u);
}
+BOOST_AUTO_TEST_CASE(duplicate_dueto_filter_recovery)
+{
+ DuplicateDueToFilterHelper helper;
+
+ helper.s->SetStateRaw(ServiceCritical, true);
+ Application::GetTP().Start();
+ helper.n->BeginExecuteNotification(NotificationProblem, nullptr, false, false, "", "");
+ Application::GetTP().Stop();
+ BOOST_CHECK(helper.called == 1u);
+
+ helper.s->SetStateRaw(ServiceWarning, true);
+ Application::GetTP().Start();
+ helper.n->BeginExecuteNotification(NotificationProblem, nullptr, false, false, "", "");
+ Application::GetTP().Stop();
+ BOOST_CHECK(helper.called == 1u);
+
+ helper.s->SetStateRaw(ServiceCritical, true);
+ Application::GetTP().Start();
+ helper.n->BeginExecuteNotification(NotificationProblem, nullptr, false, false, "", "");
+ Application::GetTP().Stop();
+ BOOST_CHECK(helper.called == 1u);
+
+ helper.s->SetStateRaw(ServiceOK, true);
+ Application::GetTP().Start();
+ helper.n->BeginExecuteNotification(NotificationRecovery, nullptr, false, false, "", "");
+ Application::GetTP().Stop();
+ BOOST_CHECK(helper.called == 1u);
+
+ helper.s->SetStateRaw(ServiceCritical, true);
+ Application::GetTP().Start();
+ helper.n->BeginExecuteNotification(NotificationProblem, nullptr, false, false, "", "");
+ Application::GetTP().Stop();
+ BOOST_CHECK(helper.called == 2u);
+}
+
BOOST_AUTO_TEST_SUITE_END()