diff --git a/lib/base/dictionary.cpp b/lib/base/dictionary.cpp index 4fd9d24b9..435df3159 100644 --- a/lib/base/dictionary.cpp +++ b/lib/base/dictionary.cpp @@ -235,10 +235,10 @@ Object::Ptr Dictionary::Clone() const } /** - * Returns an array containing all keys + * Returns an ordered vector containing all keys * which are currently set in this directory. * - * @returns an array of key names + * @returns an ordered vector of key names */ std::vector Dictionary::GetKeys() const { diff --git a/lib/icinga/command.ti b/lib/icinga/command.ti index 5055ee3d5..a49d6e869 100644 --- a/lib/icinga/command.ti +++ b/lib/icinga/command.ti @@ -11,11 +11,11 @@ namespace icinga abstract class Command : CustomVarObject { [config] Value command (CommandLine); - [config] Value arguments; + [config, signal_with_old_value] Value arguments; [config] int timeout { default {{{ return 60; }}} }; - [config] Dictionary::Ptr env; + [config, signal_with_old_value] Dictionary::Ptr env; [config, required] Function::Ptr execute; }; diff --git a/lib/icinga/customvarobject.ti b/lib/icinga/customvarobject.ti index 18da48b00..3e40f6680 100644 --- a/lib/icinga/customvarobject.ti +++ b/lib/icinga/customvarobject.ti @@ -9,7 +9,7 @@ namespace icinga abstract class CustomVarObject : ConfigObject { - [config] Dictionary::Ptr vars; + [config, signal_with_old_value] Dictionary::Ptr vars; }; } diff --git a/lib/icinga/host.ti b/lib/icinga/host.ti index 1c8a6c9fe..d69d92102 100644 --- a/lib/icinga/host.ti +++ b/lib/icinga/host.ti @@ -15,7 +15,7 @@ class Host : Checkable load_after Endpoint; load_after Zone; - [config, no_user_modify, required] array(name(HostGroup)) groups { + [config, no_user_modify, required, signal_with_old_value] array(name(HostGroup)) groups { default {{{ return new Array(); }}} }; diff --git a/lib/icinga/notification.cpp b/lib/icinga/notification.cpp index f1c2c9862..f3826ca81 100644 --- a/lib/icinga/notification.cpp +++ b/lib/icinga/notification.cpp @@ -133,6 +133,9 @@ void Notification::Start(bool runtimeCreated) if (ApiListener::IsHACluster() && GetNextNotification() < Utility::GetTime() + 60) SetNextNotification(Utility::GetTime() + 60, true); + for (const UserGroup::Ptr& group : GetUserGroups()) + group->AddNotification(this); + ObjectImpl::Start(runtimeCreated); } @@ -144,6 +147,9 @@ void Notification::Stop(bool runtimeRemoved) if (obj) obj->UnregisterNotification(this); + + for (const UserGroup::Ptr& group : GetUserGroups()) + group->RemoveNotification(this); } Checkable::Ptr Notification::GetCheckable() const diff --git a/lib/icinga/notification.ti b/lib/icinga/notification.ti index e76a4f775..ab633c5dc 100644 --- a/lib/icinga/notification.ti +++ b/lib/icinga/notification.ti @@ -36,8 +36,8 @@ class Notification : CustomVarObject < NotificationNameComposer return TimePeriod::GetByName(GetPeriodRaw()); }}} }; - [config, protected] array(name(User)) users (UsersRaw); - [config, protected] array(name(UserGroup)) user_groups (UserGroupsRaw); + [config, signal_with_old_value] array(name(User)) users (UsersRaw); + [config, signal_with_old_value] array(name(UserGroup)) user_groups (UserGroupsRaw); [config] Dictionary::Ptr times; [config] array(Value) types; [no_user_view, no_user_modify] int type_filter_real (TypeFilter); diff --git a/lib/icinga/service.ti b/lib/icinga/service.ti index f76750ef4..4448c0aa8 100644 --- a/lib/icinga/service.ti +++ b/lib/icinga/service.ti @@ -27,7 +27,7 @@ class Service : Checkable < ServiceNameComposer load_after Host; load_after Zone; - [config, no_user_modify, required] array(name(ServiceGroup)) groups { + [config, no_user_modify, required, signal_with_old_value] array(name(ServiceGroup)) groups { default {{{ return new Array(); }}} }; diff --git a/lib/icinga/timeperiod.ti b/lib/icinga/timeperiod.ti index ccfbcc3d4..58a5e8c4d 100644 --- a/lib/icinga/timeperiod.ti +++ b/lib/icinga/timeperiod.ti @@ -18,15 +18,15 @@ class TimePeriod : CustomVarObject return m_DisplayName.m_Value; }}} }; - [config] Dictionary::Ptr ranges; + [config, signal_with_old_value] Dictionary::Ptr ranges; [config, required] Function::Ptr update; [config] bool prefer_includes { default {{{ return true; }}} }; - [config, required] array(name(TimePeriod)) excludes { + [config, required, signal_with_old_value] array(name(TimePeriod)) excludes { default {{{ return new Array(); }}} }; - [config, required] array(name(TimePeriod)) includes { + [config, required, signal_with_old_value] array(name(TimePeriod)) includes { default {{{ return new Array(); }}} }; [state, no_user_modify] Value valid_begin; diff --git a/lib/icinga/user.ti b/lib/icinga/user.ti index 5de35b523..c69bac69c 100644 --- a/lib/icinga/user.ti +++ b/lib/icinga/user.ti @@ -19,7 +19,7 @@ class User : CustomVarObject return m_DisplayName.m_Value; }}} }; - [config, no_user_modify, required] array(name(UserGroup)) groups { + [config, no_user_modify, required, signal_with_old_value] array(name(UserGroup)) groups { default {{{ return new Array(); }}} }; [config, navigation] name(TimePeriod) period (PeriodRaw) { diff --git a/lib/icinga/usergroup.cpp b/lib/icinga/usergroup.cpp index 7702e337c..6ff4b89d4 100644 --- a/lib/icinga/usergroup.cpp +++ b/lib/icinga/usergroup.cpp @@ -76,6 +76,24 @@ void UserGroup::RemoveMember(const User::Ptr& user) m_Members.erase(user); } +std::set UserGroup::GetNotifications() const +{ + std::unique_lock lock(m_UserGroupMutex); + return m_Notifications; +} + +void UserGroup::AddNotification(const Notification::Ptr& notification) +{ + std::unique_lock lock(m_UserGroupMutex); + m_Notifications.insert(notification); +} + +void UserGroup::RemoveNotification(const Notification::Ptr& notification) +{ + std::unique_lock lock(m_UserGroupMutex); + m_Notifications.erase(notification); +} + bool UserGroup::ResolveGroupMembership(const User::Ptr& user, bool add, int rstack) { if (add && rstack > 20) { diff --git a/lib/icinga/usergroup.hpp b/lib/icinga/usergroup.hpp index e0325b4fc..c6f82a131 100644 --- a/lib/icinga/usergroup.hpp +++ b/lib/icinga/usergroup.hpp @@ -11,6 +11,7 @@ namespace icinga { class ConfigItem; +class Notification; /** * An Icinga user group. @@ -27,6 +28,10 @@ public: void AddMember(const User::Ptr& user); void RemoveMember(const User::Ptr& user); + std::set> GetNotifications() const; + void AddNotification(const intrusive_ptr& notification); + void RemoveNotification(const intrusive_ptr& notification); + bool ResolveGroupMembership(const User::Ptr& user, bool add = true, int rstack = 0); static void EvaluateObjectRules(const User::Ptr& user); @@ -34,6 +39,7 @@ public: private: mutable std::mutex m_UserGroupMutex; std::set m_Members; + std::set> m_Notifications; static bool EvaluateObjectRule(const User::Ptr& user, const intrusive_ptr& group); }; diff --git a/lib/icingadb/icingadb-objects.cpp b/lib/icingadb/icingadb-objects.cpp index ffcd46ac2..768095e2c 100644 --- a/lib/icingadb/icingadb-objects.cpp +++ b/lib/icingadb/icingadb-objects.cpp @@ -15,6 +15,7 @@ #include "base/array.hpp" #include "base/exception.hpp" #include "base/utility.hpp" +#include "base/object-packer.hpp" #include "icinga/command.hpp" #include "icinga/compatutility.hpp" #include "icinga/customvarobject.hpp" @@ -35,6 +36,7 @@ #include #include #include +#include using namespace icinga; @@ -127,6 +129,40 @@ void IcingaDB::ConfigStaticInitialize() Service::OnHostProblemChanged.connect([](const Service::Ptr& service, const CheckResult::Ptr&, const MessageOrigin::Ptr&) { IcingaDB::StateChangeHandler(service); }); + + Notification::OnUsersRawChangedWithOldValue.connect([](const Notification::Ptr& notification, const Value& oldValues, const Value& newValues) { + IcingaDB::NotificationUsersChangedHandler(notification, oldValues, newValues); + }); + Notification::OnUserGroupsRawChangedWithOldValue.connect([](const Notification::Ptr& notification, const Value& oldValues, const Value& newValues) { + IcingaDB::NotificationUserGroupsChangedHandler(notification, oldValues, newValues); + }); + TimePeriod::OnRangesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) { + IcingaDB::TimePeriodRangesChangedHandler(timeperiod, oldValues, newValues); + }); + TimePeriod::OnIncludesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) { + IcingaDB::TimePeriodIncludesChangedHandler(timeperiod, oldValues, newValues); + }); + TimePeriod::OnExcludesChangedWithOldValue.connect([](const TimePeriod::Ptr& timeperiod, const Value& oldValues, const Value& newValues) { + IcingaDB::TimePeriodExcludesChangedHandler(timeperiod, oldValues, newValues); + }); + User::OnGroupsChangedWithOldValue.connect([](const User::Ptr& user, const Value& oldValues, const Value& newValues) { + IcingaDB::UserGroupsChangedHandler(user, oldValues, newValues); + }); + Host::OnGroupsChangedWithOldValue.connect([](const Host::Ptr& host, const Value& oldValues, const Value& newValues) { + IcingaDB::HostGroupsChangedHandler(host, oldValues, newValues); + }); + Service::OnGroupsChangedWithOldValue.connect([](const Service::Ptr& service, const Value& oldValues, const Value& newValues) { + IcingaDB::ServiceGroupsChangedHandler(service, oldValues, newValues); + }); + Command::OnEnvChangedWithOldValue.connect([](const ConfigObject::Ptr& command, const Value& oldValues, const Value& newValues) { + IcingaDB::CommandEnvChangedHandler(command, oldValues, newValues); + }); + Command::OnArgumentsChangedWithOldValue.connect([](const ConfigObject::Ptr& command, const Value& oldValues, const Value& newValues) { + IcingaDB::CommandArgumentsChangedHandler(command, oldValues, newValues); + }); + CustomVarObject::OnVarsChangedWithOldValue.connect([](const ConfigObject::Ptr& object, const Value& oldValues, const Value& newValues) { + IcingaDB::CustomVarsChangedHandler(object, oldValues, newValues); + }); } void IcingaDB::UpdateAllConfigObjects() @@ -609,7 +645,7 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S CustomVarObject::Ptr customVarObject = dynamic_pointer_cast(object); if (customVarObject) { - auto vars(SerializeVars(customVarObject)); + auto vars(SerializeVars(customVarObject->GetVars())); if (vars) { auto& typeCvs (hMSets[m_PrefixConfigObject + typeName + ":customvar"]); auto& allCvs (hMSets[m_PrefixConfigObject + "customvar"]); @@ -825,12 +861,7 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S if (type == User::TypeInstance) { User::Ptr user = static_pointer_cast(object); - - Array::Ptr groups; - ConfigObject::Ptr (*getGroup)(const String& name); - - groups = user->GetGroups(); - getGroup = &::GetObjectByName; + Array::Ptr groups = user->GetGroups(); if (groups) { ObjectLock groupsLock(groups); @@ -839,9 +870,10 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S groupIds->Reserve(groups->GetLength()); auto& members (hMSets[m_PrefixConfigObject + typeName + "group:member"]); + auto& notificationRecipients (hMSets[m_PrefixConfigObject + "notification:recipient"]); for (auto& group : groups) { - auto groupObj ((*getGroup)(group)); + UserGroup::Ptr groupObj = UserGroup::GetByName(group); String groupId = GetObjectIdentifier(groupObj); String id = HashValue(new Array({m_EnvironmentId, groupObj->GetName(), object->GetName()})); members.emplace_back(id); @@ -850,6 +882,16 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S if (runtimeUpdate) { AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + "group:member", data); + + // Recipients are handled by notifications during initial dumps and only need to be handled here during runtime (e.g. User creation). + for (auto& notification : groupObj->GetNotifications()) { + String recipientId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), groupObj->GetName(), notification->GetName()})); + notificationRecipients.emplace_back(recipientId); + Dictionary::Ptr recipientData = new Dictionary({{"notification_id", GetObjectIdentifier(notification)}, {"environment_id", m_EnvironmentId}, {"user_id", objectKey}, {"usergroup_id", groupId}}); + notificationRecipients.emplace_back(JsonEncode(recipientData)); + + AddObjectDataToRuntimeUpdates(runtimeUpdates, recipientId, m_PrefixConfigObject + "notification:recipient", recipientData); + } } groupIds->Add(groupId); @@ -863,10 +905,6 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S Notification::Ptr notification = static_pointer_cast(object); std::set users = notification->GetUsers(); - - std::set allUsers; - std::copy(users.begin(), users.end(), std::inserter(allUsers, allUsers.begin())); - Array::Ptr userIds = new Array(); auto usergroups(notification->GetUserGroups()); @@ -875,16 +913,22 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S userIds->Reserve(users.size()); auto& usrs (hMSets[m_PrefixConfigObject + typeName + ":user"]); + auto& notificationRecipients (hMSets[m_PrefixConfigObject + typeName + ":recipient"]); for (auto& user : users) { String userId = GetObjectIdentifier(user); - String id = HashValue(new Array({m_EnvironmentId, user->GetName(), object->GetName()})); + String id = HashValue(new Array({m_EnvironmentId, "user", user->GetName(), object->GetName()})); usrs.emplace_back(id); + notificationRecipients.emplace_back(id); + Dictionary::Ptr data = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}}); - usrs.emplace_back(JsonEncode(data)); + String dataJson = JsonEncode(data); + usrs.emplace_back(dataJson); + notificationRecipients.emplace_back(dataJson); if (runtimeUpdate) { AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":user", data); + AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", data); } userIds->Add(userId); @@ -893,43 +937,38 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S usergroupIds->Reserve(usergroups.size()); auto& groups (hMSets[m_PrefixConfigObject + typeName + ":usergroup"]); - auto& notificationRecipients (hMSets[m_PrefixConfigObject + typeName + ":recipient"]); for (auto& usergroup : usergroups) { String usergroupId = GetObjectIdentifier(usergroup); - - auto groupMembers = usergroup->GetMembers(); - std::copy(groupMembers.begin(), groupMembers.end(), std::inserter(allUsers, allUsers.begin())); - String id = HashValue(new Array({m_EnvironmentId, "usergroup", usergroup->GetName(), object->GetName()})); groups.emplace_back(id); - Dictionary::Ptr groupData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", usergroupId}}); - groups.emplace_back(JsonEncode(groupData)); - notificationRecipients.emplace_back(id); - Dictionary::Ptr notificationRecipientData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", usergroupId}}); - notificationRecipients.emplace_back(JsonEncode(notificationRecipientData)); + + Dictionary::Ptr groupData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"usergroup_id", usergroupId}}); + String groupDataJson = JsonEncode(groupData); + groups.emplace_back(groupDataJson); + notificationRecipients.emplace_back(groupDataJson); if (runtimeUpdate) { AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":usergroup", groupData); - AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", notificationRecipientData); + AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", groupData); + } + + for (const User::Ptr& user : usergroup->GetMembers()) { + String userId = GetObjectIdentifier(user); + String recipientId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), usergroup->GetName(), notification->GetName()})); + notificationRecipients.emplace_back(recipientId); + Dictionary::Ptr userData = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}, {"usergroup_id", usergroupId}}); + notificationRecipients.emplace_back(JsonEncode(userData)); + + if (runtimeUpdate) { + AddObjectDataToRuntimeUpdates(runtimeUpdates, recipientId, m_PrefixConfigObject + typeName + ":recipient", userData); + } } usergroupIds->Add(usergroupId); } - for (auto& user : allUsers) { - String userId = GetObjectIdentifier(user); - String id = HashValue(new Array({m_EnvironmentId, "user", user->GetName(), object->GetName()})); - notificationRecipients.emplace_back(id); - Dictionary::Ptr data = new Dictionary({{"notification_id", objectKey}, {"environment_id", m_EnvironmentId}, {"user_id", userId}}); - notificationRecipients.emplace_back(JsonEncode(data)); - - if (runtimeUpdate) { - AddObjectDataToRuntimeUpdates(runtimeUpdates, id, m_PrefixConfigObject + typeName + ":recipient", data); - } - } - return; } @@ -1121,10 +1160,11 @@ void IcingaDB::SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpd void IcingaDB::AddObjectDataToRuntimeUpdates(std::vector& runtimeUpdates, const String& objectKey, const String& redisKey, const Dictionary::Ptr& data) { - data->Set("id", objectKey); - data->Set("redis_key", redisKey); - data->Set("runtime_type", "upsert"); - runtimeUpdates.emplace_back(data); + Dictionary::Ptr dataClone = data->ShallowClone(); + dataClone->Set("id", objectKey); + dataClone->Set("redis_key", redisKey); + dataClone->Set("runtime_type", "upsert"); + runtimeUpdates.emplace_back(dataClone); } // Takes object and collects IcingaDB relevant attributes and computes checksums. Returns whether the object is relevant @@ -1456,7 +1496,8 @@ IcingaDB::CreateConfigUpdate(const ConfigObject::Ptr& object, const String typeN void IcingaDB::SendConfigDelete(const ConfigObject::Ptr& object) { - String typeName = object->GetReflectionType()->GetName().ToLower(); + Type::Ptr type = object->GetReflectionType(); + String typeName = type->GetName().ToLower(); String objectKey = GetObjectIdentifier(object); m_Rcon->FireAndForgetQueries({ @@ -1468,12 +1509,23 @@ void IcingaDB::SendConfigDelete(const ConfigObject::Ptr& object) } }, Prio::Config); - auto checkable (dynamic_pointer_cast(object)); + CustomVarObject::Ptr customVarObject = dynamic_pointer_cast(object); + + if (customVarObject) { + Dictionary::Ptr vars = customVarObject->GetVars(); + SendCustomVarsChanged(object, vars, nullptr); + } + + if (type == Host::TypeInstance || type == Service::TypeInstance) { + Checkable::Ptr checkable = static_pointer_cast(object); + + Host::Ptr host; + Service::Ptr service; + tie(host, service) = GetHostService(checkable); - if (checkable) { m_Rcon->FireAndForgetQuery({ "ZREM", - dynamic_pointer_cast(checkable) ? "icinga:nextupdate:service" : "icinga:nextupdate:host", + service ? "icinga:nextupdate:service" : "icinga:nextupdate:host", GetObjectIdentifier(checkable) }, Prio::CheckResult); @@ -1481,6 +1533,42 @@ void IcingaDB::SendConfigDelete(const ConfigObject::Ptr& object) {"HDEL", m_PrefixConfigObject + typeName + ":state", objectKey}, {"HDEL", m_PrefixConfigCheckSum + typeName + ":state", objectKey} }, Prio::RuntimeStateSync); + + if (service) { + SendGroupsChanged(checkable, service->GetGroups(), nullptr); + } else { + SendGroupsChanged(checkable, host->GetGroups(), nullptr); + } + + return; + } + + if (type == TimePeriod::TypeInstance) { + TimePeriod::Ptr timeperiod = static_pointer_cast(object); + SendTimePeriodRangesChanged(timeperiod, timeperiod->GetRanges(), nullptr); + SendTimePeriodIncludesChanged(timeperiod, timeperiod->GetIncludes(), nullptr); + SendTimePeriodExcludesChanged(timeperiod, timeperiod->GetExcludes(), nullptr); + return; + } + + if (type == User::TypeInstance) { + User::Ptr user = static_pointer_cast(object); + SendGroupsChanged(user, user->GetGroups(), nullptr); + return; + } + + if (type == Notification::TypeInstance) { + Notification::Ptr notification = static_pointer_cast(object); + SendNotificationUsersChanged(notification, notification->GetUsersRaw(), nullptr); + SendNotificationUserGroupsChanged(notification, notification->GetUserGroupsRaw(), nullptr); + return; + } + + if (type == CheckCommand::TypeInstance || type == NotificationCommand::TypeInstance || type == EventCommand::TypeInstance) { + Command::Ptr command = static_pointer_cast(object); + SendCommandArgumentsChanged(command, command->GetArguments(), nullptr); + SendCommandEnvChanged(command, command->GetEnv(), nullptr); + return; } } @@ -2174,6 +2262,150 @@ void IcingaDB::SendAcknowledgementCleared(const Checkable::Ptr& checkable, const m_Rcon->FireAndForgetQuery(std::move(xAdd), Prio::History); } +void IcingaDB::SendNotificationUsersChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) { + return; + } + + std::vector deletedUsers = GetArrayDeletedValues(oldValues, newValues); + + for (const auto& userName : deletedUsers) { + String id = HashValue(new Array({m_EnvironmentId, "user", userName, notification->GetName()})); + DeleteRelationship(id, "notification:user"); + DeleteRelationship(id, "notification:recipient"); + } +} + +void IcingaDB::SendNotificationUserGroupsChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) { + return; + } + + std::vector deletedUserGroups = GetArrayDeletedValues(oldValues, newValues); + + for (const auto& userGroupName : deletedUserGroups) { + UserGroup::Ptr userGroup = UserGroup::GetByName(userGroupName); + String id = HashValue(new Array({m_EnvironmentId, "usergroup", userGroupName, notification->GetName()})); + DeleteRelationship(id, "notification:usergroup"); + DeleteRelationship(id, "notification:recipient"); + + for (const User::Ptr& user : userGroup->GetMembers()) { + String userId = HashValue(new Array({m_EnvironmentId, "usergroupuser", user->GetName(), userGroupName, notification->GetName()})); + DeleteRelationship(userId, "notification:recipient"); + } + } +} + +void IcingaDB::SendTimePeriodRangesChanged(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) { + if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) { + return; + } + + std::vector deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues); + String typeName = GetLowerCaseTypeNameDB(timeperiod); + + for (const auto& rangeKey : deletedKeys) { + String id = HashValue(new Array({m_EnvironmentId, rangeKey, oldValues->Get(rangeKey), timeperiod->GetName()})); + DeleteRelationship(id, "timeperiod:range"); + } +} + +void IcingaDB::SendTimePeriodIncludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) { + return; + } + + std::vector deletedIncludes = GetArrayDeletedValues(oldValues, newValues); + + for (const auto& includeName : deletedIncludes) { + String id = HashValue(new Array({m_EnvironmentId, includeName, timeperiod->GetName()})); + DeleteRelationship(id, "timeperiod:override:include"); + } +} + +void IcingaDB::SendTimePeriodExcludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) { + return; + } + + std::vector deletedExcludes = GetArrayDeletedValues(oldValues, newValues); + + for (const auto& excludeName : deletedExcludes) { + String id = HashValue(new Array({m_EnvironmentId, excludeName, timeperiod->GetName()})); + DeleteRelationship(id, "timeperiod:override:exclude"); + } +} + +template +void IcingaDB::SendGroupsChanged(const ConfigObject::Ptr& object, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) { + return; + } + + std::vector deletedGroups = GetArrayDeletedValues(oldValues, newValues); + String typeName = GetLowerCaseTypeNameDB(object); + + for (const auto& groupName : deletedGroups) { + typename T::Ptr group = ConfigObject::GetObject(groupName); + String id = HashValue(new Array({m_EnvironmentId, group->GetName(), object->GetName()})); + DeleteRelationship(id, typeName + "group:member"); + + if (std::is_same::value) { + UserGroup::Ptr userGroup = dynamic_pointer_cast(group); + + for (const auto& notification : userGroup->GetNotifications()) { + String userId = HashValue(new Array({m_EnvironmentId, "usergroupuser", object->GetName(), groupName, notification->GetName()})); + DeleteRelationship(userId, "notification:recipient"); + } + } + } +} + +void IcingaDB::SendCommandEnvChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) { + if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) { + return; + } + + std::vector deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues); + String typeName = GetLowerCaseTypeNameDB(command); + + for (const auto& envvarKey : deletedKeys) { + String id = HashValue(new Array({m_EnvironmentId, envvarKey, command->GetName()})); + DeleteRelationship(id, typeName + ":envvar", true); + } +} + +void IcingaDB::SendCommandArgumentsChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) { + if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) { + return; + } + + std::vector deletedKeys = GetDictionaryDeletedKeys(oldValues, newValues); + String typeName = GetLowerCaseTypeNameDB(command); + + for (const auto& argumentKey : deletedKeys) { + String id = HashValue(new Array({m_EnvironmentId, argumentKey, command->GetName()})); + DeleteRelationship(id, typeName + ":argument", true); + } +} + +void IcingaDB::SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) { + if (!m_Rcon || !m_Rcon->IsConnected() || oldValues == newValues) { + return; + } + + Dictionary::Ptr oldVars = SerializeVars(oldValues); + Dictionary::Ptr newVars = SerializeVars(newValues); + + std::vector deletedVars = GetDictionaryDeletedKeys(oldVars, newVars); + String typeName = GetLowerCaseTypeNameDB(object); + + for (const auto& varId : deletedVars) { + String id = HashValue(new Array({m_EnvironmentId, varId, object->GetName()})); + DeleteRelationship(id, typeName + ":customvar"); + } +} + Dictionary::Ptr IcingaDB::SerializeState(const Checkable::Ptr& checkable) { Dictionary::Ptr attrs = new Dictionary(); @@ -2473,3 +2705,89 @@ void IcingaDB::AcknowledgementClearedHandler(const Checkable::Ptr& checkable, co } } } + +void IcingaDB::NotificationUsersChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendNotificationUsersChanged(notification, oldValues, newValues); + } +} + +void IcingaDB::NotificationUserGroupsChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendNotificationUserGroupsChanged(notification, oldValues, newValues); + } +} + +void IcingaDB::TimePeriodRangesChangedHandler(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendTimePeriodRangesChanged(timeperiod, oldValues, newValues); + } +} + +void IcingaDB::TimePeriodIncludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendTimePeriodIncludesChanged(timeperiod, oldValues, newValues); + } +} + +void IcingaDB::TimePeriodExcludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendTimePeriodExcludesChanged(timeperiod, oldValues, newValues); + } +} + +void IcingaDB::UserGroupsChangedHandler(const User::Ptr& user, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendGroupsChanged(user, oldValues, newValues); + } +} + +void IcingaDB::HostGroupsChangedHandler(const Host::Ptr& host, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendGroupsChanged(host, oldValues, newValues); + } +} + +void IcingaDB::ServiceGroupsChangedHandler(const Service::Ptr& service, const Array::Ptr& oldValues, const Array::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendGroupsChanged(service, oldValues, newValues); + } +} + +void IcingaDB::CommandEnvChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendCommandEnvChanged(command, oldValues, newValues); + } +} + +void IcingaDB::CommandArgumentsChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendCommandArgumentsChanged(command, oldValues, newValues); + } +} + +void IcingaDB::CustomVarsChangedHandler(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues) { + for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType()) { + rw->SendCustomVarsChanged(object, oldValues, newValues); + } +} + +void IcingaDB::DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum) { + Log(LogNotice, "IcingaDB") << "Deleting relationship '" << redisKeyWithoutPrefix << " -> '" << id << "'"; + + String redisKey = m_PrefixConfigObject + redisKeyWithoutPrefix; + + std::vector> queries; + + if (hasChecksum) { + queries.push_back({"HDEL", m_PrefixConfigCheckSum + redisKeyWithoutPrefix, id}); + } + + queries.push_back({"HDEL", redisKey, id}); + queries.push_back({ + "XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*", + "redis_key", redisKey, "id", id, "runtime_type", "delete" + }); + + m_Rcon->FireAndForgetQueries(queries, Prio::Config); +} diff --git a/lib/icingadb/icingadb-utility.cpp b/lib/icingadb/icingadb-utility.cpp index dbb7fa818..f82625019 100644 --- a/lib/icingadb/icingadb-utility.cpp +++ b/lib/icingadb/icingadb-utility.cpp @@ -90,7 +90,7 @@ String IcingaDB::CalcEventID(const char* eventType, const ConfigObject::Ptr& obj static const std::set metadataWhitelist ({"package", "source_location", "templates"}); /** - * Prepare object's custom vars for being written to Redis + * Prepare custom vars for being written to Redis * * object.vars = { * "disks": { @@ -124,14 +124,12 @@ static const std::set metadataWhitelist ({"package", "source_location", * } * } * - * @param object Config object with custom vars + * @param Dictionary Config object with custom vars * - * @return JSON-like data structure for Redis + * @return JSON-like data structure for Redis */ -Dictionary::Ptr IcingaDB::SerializeVars(const CustomVarObject::Ptr& object) +Dictionary::Ptr IcingaDB::SerializeVars(const Dictionary::Ptr& vars) { - Dictionary::Ptr vars = object->GetVars(); - if (!vars) return nullptr; @@ -258,3 +256,49 @@ String IcingaDB::IcingaToStreamValue(const Value& value) return JsonEncode(value); } } + +// Returns the items that exist in "arrayOld" but not in "arrayNew" +std::vector IcingaDB::GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew) { + std::vector deletedValues; + + if (!arrayOld) { + return deletedValues; + } + + if (!arrayNew) { + return std::vector(arrayOld->Begin(), arrayOld->End()); + } + + std::vector vectorOld(arrayOld->Begin(), arrayOld->End()); + std::sort(vectorOld.begin(), vectorOld.end()); + vectorOld.erase(std::unique(vectorOld.begin(), vectorOld.end()), vectorOld.end()); + + std::vector vectorNew(arrayNew->Begin(), arrayNew->End()); + std::sort(vectorNew.begin(), vectorNew.end()); + vectorNew.erase(std::unique(vectorNew.begin(), vectorNew.end()), vectorNew.end()); + + std::set_difference(vectorOld.begin(), vectorOld.end(), vectorNew.begin(), vectorNew.end(), std::back_inserter(deletedValues)); + + return deletedValues; +} + +// Returns the keys that exist in "dictOld" but not in "dictNew" +std::vector IcingaDB::GetDictionaryDeletedKeys(const Dictionary::Ptr& dictOld, const Dictionary::Ptr& dictNew) { + std::vector deletedKeys; + + if (!dictOld) { + return deletedKeys; + } + + std::vector oldKeys = dictOld->GetKeys(); + + if (!dictNew) { + return oldKeys; + } + + std::vector newKeys = dictNew->GetKeys(); + + std::set_difference(oldKeys.begin(), oldKeys.end(), newKeys.begin(), newKeys.end(), std::back_inserter(deletedKeys)); + + return deletedKeys; +} diff --git a/lib/icingadb/icingadb.hpp b/lib/icingadb/icingadb.hpp index 168bd2f00..7a392a2ad 100644 --- a/lib/icingadb/icingadb.hpp +++ b/lib/icingadb/icingadb.hpp @@ -79,6 +79,7 @@ private: void SendStateChange(const ConfigObject::Ptr& object, const CheckResult::Ptr& cr, StateType type); void AddObjectDataToRuntimeUpdates(std::vector& runtimeUpdates, const String& objectKey, const String& redisKey, const Dictionary::Ptr& data); + void DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum = false); void SendSentNotification( const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set& users, @@ -93,6 +94,16 @@ private: void SendNextUpdate(const Checkable::Ptr& checkable); void SendAcknowledgementSet(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry); void SendAcknowledgementCleared(const Checkable::Ptr& checkable, const String& removedBy, double changeTime, double ackLastChange); + void SendNotificationUsersChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues); + void SendNotificationUserGroupsChanged(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues); + void SendTimePeriodRangesChanged(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues); + void SendTimePeriodIncludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues); + void SendTimePeriodExcludesChanged(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues); + template + void SendGroupsChanged(const ConfigObject::Ptr& command, const Array::Ptr& oldValues, const Array::Ptr& newValues); + void SendCommandEnvChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues); + void SendCommandArgumentsChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues); + void SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues); std::vector UpdateObjectAttrs(const ConfigObject::Ptr& object, int fieldType, const String& typeNameOverride); Dictionary::Ptr SerializeState(const Checkable::Ptr& checkable); @@ -105,11 +116,13 @@ private: static String FormatCommandLine(const Value& commandLine); static long long TimestampToMilliseconds(double timestamp); static String IcingaToStreamValue(const Value& value); + static std::vector GetArrayDeletedValues(const Array::Ptr& arrayOld, const Array::Ptr& arrayNew); + static std::vector GetDictionaryDeletedKeys(const Dictionary::Ptr& dictOld, const Dictionary::Ptr& dictNew); static String GetObjectIdentifier(const ConfigObject::Ptr& object); static String CalcEventID(const char* eventType, const ConfigObject::Ptr& object, double eventTime = 0, NotificationType nt = NotificationType(0)); - static Dictionary::Ptr SerializeVars(const CustomVarObject::Ptr& object); static const char* GetNotificationTypeByEnum(NotificationType type); + static Dictionary::Ptr SerializeVars(const Dictionary::Ptr& vars); static String HashValue(const Value& value); static String HashValue(const Value& value, const std::set& propertiesBlacklist, bool propertiesWhitelist = false); @@ -135,6 +148,17 @@ private: static void NextCheckChangedHandler(const Checkable::Ptr& checkable); static void AcknowledgementSetHandler(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry); static void AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime); + static void NotificationUsersChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues); + static void NotificationUserGroupsChangedHandler(const Notification::Ptr& notification, const Array::Ptr& oldValues, const Array::Ptr& newValues); + static void TimePeriodRangesChangedHandler(const TimePeriod::Ptr& timeperiod, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues); + static void TimePeriodIncludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues); + static void TimePeriodExcludesChangedHandler(const TimePeriod::Ptr& timeperiod, const Array::Ptr& oldValues, const Array::Ptr& newValues); + static void UserGroupsChangedHandler(const User::Ptr& user, const Array::Ptr&, const Array::Ptr& newValues); + static void HostGroupsChangedHandler(const Host::Ptr& host, const Array::Ptr& oldValues, const Array::Ptr& newValues); + static void ServiceGroupsChangedHandler(const Service::Ptr& service, const Array::Ptr& oldValues, const Array::Ptr& newValues); + static void CommandEnvChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues); + static void CommandArgumentsChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues); + static void CustomVarsChangedHandler(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues); void AssertOnWorkQueue(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 25557944a..ba08fa431 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -70,6 +70,7 @@ add_boost_test(base base_dictionary/remove base_dictionary/clone base_dictionary/json + base_dictionary/keys_ordered base_fifo/construct base_fifo/io base_json/encode diff --git a/test/base-dictionary.cpp b/test/base-dictionary.cpp index cd074e1a9..3469be7cd 100644 --- a/test/base-dictionary.cpp +++ b/test/base-dictionary.cpp @@ -3,6 +3,8 @@ #include "base/dictionary.hpp" #include "base/objectlock.hpp" #include "base/json.hpp" +#include "base/string.hpp" +#include "base/utility.hpp" #include using namespace icinga; @@ -183,4 +185,16 @@ BOOST_AUTO_TEST_CASE(json) BOOST_CHECK(deserialized->Get("test2") == "hello world"); } +BOOST_AUTO_TEST_CASE(keys_ordered) +{ + Dictionary::Ptr dictionary = new Dictionary(); + + for (int i = 0; i < 100; i++) { + dictionary->Set(std::to_string(Utility::Random()), Utility::Random()); + } + + std::vector keys = dictionary->GetKeys(); + BOOST_CHECK(std::is_sorted(keys.begin(), keys.end())); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/tools/mkclass/class_lexer.ll b/tools/mkclass/class_lexer.ll index 56f219a76..217ca492c 100644 --- a/tools/mkclass/class_lexer.ll +++ b/tools/mkclass/class_lexer.ll @@ -134,6 +134,7 @@ no_user_view { yylval->num = FANoUserView; return T_FIELD_ATTRIBUTE; } deprecated { yylval->num = FADeprecated; return T_FIELD_ATTRIBUTE; } get_virtual { yylval->num = FAGetVirtual; return T_FIELD_ATTRIBUTE; } set_virtual { yylval->num = FASetVirtual; return T_FIELD_ATTRIBUTE; } +signal_with_old_value { yylval->num = FASignalWithOldValue; return T_FIELD_ATTRIBUTE; } virtual { yylval->num = FAGetVirtual | FASetVirtual; return T_FIELD_ATTRIBUTE; } navigation { return T_NAVIGATION; } validator { return T_VALIDATOR; } diff --git a/tools/mkclass/classcompiler.cpp b/tools/mkclass/classcompiler.cpp index f6793da50..09110bdee 100644 --- a/tools/mkclass/classcompiler.cpp +++ b/tools/mkclass/classcompiler.cpp @@ -830,10 +830,10 @@ void ClassCompiler::HandleClass(const Klass& klass, const ClassDebugInfo&) m_Impl << "void ObjectImpl<" << klass.Name << ">::Set" << field.GetFriendlyName() << "(" << field.Type.GetArgumentType() << " value, bool suppress_events, const Value& cookie)" << std::endl << "{" << std::endl; - if (field.Type.IsName || !field.TrackAccessor.empty()) - m_Impl << "\t" << "Value oldValue = Get" << field.GetFriendlyName() << "();" << std::endl; + if (field.Type.IsName || !field.TrackAccessor.empty() || field.Attributes & FASignalWithOldValue) + m_Impl << "\t" << "Value oldValue = Get" << field.GetFriendlyName() << "();" << std::endl + << "\t" << "auto *dobj = dynamic_cast(this);" << std::endl; - if (field.SetAccessor.empty() && !(field.Attributes & FANoStorage)) m_Impl << "\t" << "m_" << field.GetFriendlyName() << ".store(value);" << std::endl; else @@ -841,16 +841,22 @@ void ClassCompiler::HandleClass(const Klass& klass, const ClassDebugInfo&) if (field.Type.IsName || !field.TrackAccessor.empty()) { if (field.Name != "active") { - m_Impl << "\t" << "auto *dobj = dynamic_cast(this);" << std::endl - << "\t" << "if (!dobj || dobj->IsActive())" << std::endl + m_Impl << "\t" << "if (!dobj || dobj->IsActive())" << std::endl << "\t"; } m_Impl << "\t" << "Track" << field.GetFriendlyName() << "(oldValue, value);" << std::endl; } - m_Impl << "\t" << "if (!suppress_events)" << std::endl - << "\t\t" << "Notify" << field.GetFriendlyName() << "(cookie);" << std::endl + m_Impl << "\t" << "if (!suppress_events) {" << std::endl + << "\t\t" << "Notify" << field.GetFriendlyName() << "(cookie);" << std::endl; + + if (field.Attributes & FASignalWithOldValue) { + m_Impl << "\t\t" << "if (!dobj || dobj->IsActive())" << std::endl + << "\t\t\t" << "On" << field.GetFriendlyName() << "ChangedWithOldValue(static_cast<" << klass.Name << " *>(this), oldValue, value);" << std::endl; + } + + m_Impl << "\t" "}" << std::endl << std::endl << "}" << std::endl << std::endl; } } @@ -1053,6 +1059,15 @@ void ClassCompiler::HandleClass(const Klass& klass, const ClassDebugInfo&) for (const Field& field : klass.Fields) { m_Header << "\t" << "static boost::signals2::signal&, const Value&)> On" << field.GetFriendlyName() << "Changed;" << std::endl; m_Impl << std::endl << "boost::signals2::signal&, const Value&)> ObjectImpl<" << klass.Name << ">::On" << field.GetFriendlyName() << "Changed;" << std::endl << std::endl; + + if (field.Attributes & FASignalWithOldValue) { + m_Header << "\t" << "static boost::signals2::signal&, const Value&, const Value&)> On" << field.GetFriendlyName() << "ChangedWithOldValue;" + << std::endl; + m_Impl << std::endl << "boost::signals2::signal&, const Value&, const Value&)> ObjectImpl<" << klass.Name << ">::On" + << field.GetFriendlyName() << "ChangedWithOldValue;" << std::endl << std::endl; + } } } diff --git a/tools/mkclass/classcompiler.hpp b/tools/mkclass/classcompiler.hpp index c05de11a7..0bd789dd3 100644 --- a/tools/mkclass/classcompiler.hpp +++ b/tools/mkclass/classcompiler.hpp @@ -60,7 +60,8 @@ enum FieldAttribute FADeprecated = 4096, FAGetVirtual = 8192, FASetVirtual = 16384, - FAActivationPriority = 32768 + FAActivationPriority = 32768, + FASignalWithOldValue = 65536, }; struct FieldType