diff --git a/doc/19-technical-concepts.md b/doc/19-technical-concepts.md
index 74a2756ca..b3a056170 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::ClearLastNotifiedStatePerUser
+
+> Location: `clusterevents.cpp`
+
+##### Message Body
+
+Key | Value
+----------|---------
+jsonrpc | 2.0
+method | event::ClearLastNotifiedStatePerUser
+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::OnLastNotifiedStatePerUserCleared`
+Event Receiver: `LastNotifiedStatePerUserClearedAPIHandler`
+
+##### 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 95616d2e3..fe5167b78 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(UpdateLastNotifiedStatePerUser, event, &ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler);
+REGISTER_APIFUNCTION(ClearLastNotifiedStatePerUser, event, &ClusterEvents::LastNotifiedStatePerUserClearedAPIHandler);
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::OnLastNotifiedStatePerUserUpdated.connect(&ClusterEvents::LastNotifiedStatePerUserUpdatedHandler);
+ Notification::OnLastNotifiedStatePerUserCleared.connect(&ClusterEvents::LastNotifiedStatePerUserClearedHandler);
Checkable::OnForceNextCheckChanged.connect(&ClusterEvents::ForceNextCheckChangedHandler);
Checkable::OnForceNextNotificationChanged.connect(&ClusterEvents::ForceNextNotificationChangedHandler);
Checkable::OnNotificationsRequested.connect(&ClusterEvents::SendNotificationsHandler);
@@ -589,6 +591,57 @@ Value ClusterEvents::LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrig
return Empty;
}
+void ClusterEvents::LastNotifiedStatePerUserClearedHandler(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::ClearLastNotifiedStatePerUser");
+ message->Set("params", params);
+
+ listener->RelayMessage(origin, notification, message, true);
+}
+
+Value ClusterEvents::LastNotifiedStatePerUserClearedAPIHandler(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::OnLastNotifiedStatePerUserCleared(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 2e6e092f2..8daf86ab3 100644
--- a/lib/icinga/clusterevents.hpp
+++ b/lib/icinga/clusterevents.hpp
@@ -44,6 +44,9 @@ public:
static void LastNotifiedStatePerUserUpdatedHandler(const Notification::Ptr& notification, const String& user, uint_fast8_t state, const MessageOrigin::Ptr& origin);
static Value LastNotifiedStatePerUserUpdatedAPIHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
+ static void LastNotifiedStatePerUserClearedHandler(const Notification::Ptr& notification, const MessageOrigin::Ptr& origin);
+ static Value LastNotifiedStatePerUserClearedAPIHandler(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 9533d229b..ab8d42b8c 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::OnLastNotifiedStatePerUserUpdated;
+boost::signals2::signal Notification::OnLastNotifiedStatePerUserCleared;
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();
+ OnLastNotifiedStatePerUserCleared(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 != (uint_fast8_t)GetLastNotifiedStatePerUser()->Get(userName)) {
- GetLastNotifiedStatePerUser()->Set(userName, state);
- OnLastNotifiedStatePerUserUpdated(this, userName, state, nullptr);
- }
+ if (state != (uint_fast8_t)GetLastNotifiedStatePerUser()->Get(userName)) {
+ GetLastNotifiedStatePerUser()->Set(userName, state);
+ OnLastNotifiedStatePerUserUpdated(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 2c41a13e7..1b6cbedb1 100644
--- a/lib/icinga/notification.hpp
+++ b/lib/icinga/notification.hpp
@@ -94,6 +94,7 @@ public:
static boost::signals2::signal OnNextNotificationChanged;
static boost::signals2::signal OnLastNotifiedStatePerUserUpdated;
+ static boost::signals2::signal OnLastNotifiedStatePerUserCleared;
void Validate(int types, const ValidationUtils& utils) override;
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 5e054c48e..24eb2198c 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -141,6 +141,8 @@ add_boost_test(base
icinga_notification/no_filter_problem_no_duplicate
icinga_notification/filter_problem_no_duplicate
icinga_notification/volatile_filter_problem_duplicate
+ icinga_notification/no_recovery_filter_no_duplicate
+ icinga_notification/recovery_filter_duplicate
icinga_macros/simple
icinga_legacytimeperiod/simple
icinga_legacytimeperiod/advanced
diff --git a/test/icinga-notification.cpp b/test/icinga-notification.cpp
index 4b3fe5f73..a0aeb7df8 100644
--- a/test/icinga-notification.cpp
+++ b/test/icinga-notification.cpp
@@ -194,4 +194,22 @@ BOOST_AUTO_TEST_CASE(volatile_filter_problem_duplicate)
helper.SendStateNotification(ServiceCritical, true);
}
+BOOST_AUTO_TEST_CASE(no_recovery_filter_no_duplicate)
+{
+ DuplicateDueToFilterHelper helper (~0, ~0);
+
+ helper.SendStateNotification(ServiceCritical, true);
+ helper.SendStateNotification(ServiceOK, true);
+ helper.SendStateNotification(ServiceCritical, true);
+}
+
+BOOST_AUTO_TEST_CASE(recovery_filter_duplicate)
+{
+ DuplicateDueToFilterHelper helper (~NotificationRecovery, ~0);
+
+ helper.SendStateNotification(ServiceCritical, true);
+ helper.SendStateNotification(ServiceOK, false);
+ helper.SendStateNotification(ServiceCritical, true);
+}
+
BOOST_AUTO_TEST_SUITE_END()