IcingaDB: Dump checkables dependencies config to redis correctly

This commit is contained in:
Yonas Habteab 2024-12-04 11:00:26 +01:00
parent 4bfaefadfa
commit c6466ee0ea
3 changed files with 199 additions and 196 deletions

View File

@ -62,7 +62,6 @@ std::vector<Type::Ptr> IcingaDB::GetTypes()
// Then sync them for similar reasons.
Downtime::TypeInstance,
Comment::TypeInstance,
Dependency::TypeInstance,
HostGroup::TypeInstance,
ServiceGroup::TypeInstance,
@ -208,10 +207,17 @@ void IcingaDB::UpdateAllConfigObjects()
m_Rcon->FireAndForgetQuery({"XADD", "icinga:dump", "MAXLEN", "1", "*", "key", "*", "state", "wip"}, Prio::Config);
const std::vector<String> globalKeys = {
m_PrefixConfigObject + "customvar",
m_PrefixConfigObject + "action:url",
m_PrefixConfigObject + "notes:url",
m_PrefixConfigObject + "icon:image",
m_PrefixConfigObject + "customvar",
m_PrefixConfigObject + "action:url",
m_PrefixConfigObject + "notes:url",
m_PrefixConfigObject + "icon:image",
// These keys aren't tied to a specific Checkable type but apply to both "Host" and "Service" types,
// and as such we've to make sure to clear them before we actually start dumping the actual objects.
// This allows us to wait on both types to be dumped before we send a config dump done signal for those keys.
m_PrefixConfigObject + "dependency:node",
m_PrefixConfigObject + "dependency:edge",
m_PrefixConfigObject + "redundancygroup",
};
DeleteKeys(m_Rcon, globalKeys, Prio::Config);
DeleteKeys(m_Rcon, {"icinga:nextupdate:host", "icinga:nextupdate:service"}, Prio::Config);
@ -222,6 +228,7 @@ void IcingaDB::UpdateAllConfigObjects()
m_DumpedGlobals.ActionUrl.Reset();
m_DumpedGlobals.NotesUrl.Reset();
m_DumpedGlobals.IconImage.Reset();
m_DumpedGlobals.DependencyGroup.Reset();
});
upq.ParallelFor(types, false, [this](const Type::Ptr& type) {
@ -793,140 +800,11 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
}
}
InsertCheckableDependencies(checkable, hMSets, runtimeUpdate ? &runtimeUpdates : nullptr);
return;
}
if (type == Dependency::TypeInstance) {
auto& dependencyNodes (hMSets[m_PrefixConfigObject + "dependency:node"]);
auto& dependencyEdges (hMSets[m_PrefixConfigObject + "dependency:edge"]);
auto& redundancyGroups (hMSets[m_PrefixConfigObject + "redundancygroup"]);
Dependency::Ptr dependency = static_pointer_cast<Dependency>(object);
Host::Ptr parentHost, childHost;
Service::Ptr parentService, childService;
tie(parentHost, parentService) = GetHostService(dependency->GetParent());
tie(childHost, childService) = GetHostService(dependency->GetChild());
String redundancyGroup = dependency->GetRedundancyGroup();
String redundancyGroupId, dependencyNodeParentId, dependencyNodeChildId, dependencyNodeReduId;
Dictionary::Ptr parentNodeData, childNodeData;
if (parentService) {
dependencyNodeParentId = HashValue(new Array({
m_EnvironmentId,
GetObjectIdentifier(parentHost),
GetObjectIdentifier(parentService)}));
parentNodeData = new Dictionary({
{"environment_id", m_EnvironmentId},
{"host_id", GetObjectIdentifier(parentHost)},
{"service_id", GetObjectIdentifier(parentService)}});
m_CheckablesToDependencies->Set(GetObjectIdentifier(parentService), dependency);
} else {
dependencyNodeParentId = HashValue(new Array({
m_EnvironmentId,
GetObjectIdentifier(parentHost)}));
parentNodeData = new Dictionary({
{"environment_id", m_EnvironmentId},
{"host_id", GetObjectIdentifier(parentHost)}});
m_CheckablesToDependencies->Set(GetObjectIdentifier(parentHost), dependency);
}
if (childService) {
dependencyNodeChildId = HashValue(new Array({
m_EnvironmentId,
GetObjectIdentifier(childHost),
GetObjectIdentifier(childService)}));
childNodeData = new Dictionary({
{"environment_id", m_EnvironmentId},
{"host_id", GetObjectIdentifier(childHost)},
{"service_id", GetObjectIdentifier(childService)}});
m_CheckablesToDependencies->Set(GetObjectIdentifier(childService), dependency);
} else {
dependencyNodeChildId = HashValue(new Array({
m_EnvironmentId,
GetObjectIdentifier(childHost)}));
childNodeData = new Dictionary({
{"environment_id", m_EnvironmentId},
{"host_id", GetObjectIdentifier(childHost)}});
m_CheckablesToDependencies->Set(GetObjectIdentifier(childHost), dependency);
}
dependencyNodes.emplace_back(dependencyNodeParentId);
dependencyNodes.emplace_back(JsonEncode(parentNodeData));
dependencyNodes.emplace_back(dependencyNodeChildId);
dependencyNodes.emplace_back(JsonEncode(childNodeData));
if (runtimeUpdate) {
AddObjectDataToRuntimeUpdates(runtimeUpdates, dependencyNodeParentId, m_PrefixConfigObject + "dependency:node", parentNodeData);
AddObjectDataToRuntimeUpdates(runtimeUpdates, dependencyNodeChildId, m_PrefixConfigObject + "dependency:node", childNodeData);
}
if (!redundancyGroup.IsEmpty()) {
/* TODO: name should be suffixed with names of all children.
* however, at this point I don't have this information,
* only the direct neighbors.
*/
redundancyGroupId = HashValue(new Array({m_EnvironmentId, redundancyGroup, dependencyNodeChildId}));
dependencyNodeReduId = redundancyGroupId;
redundancyGroups.emplace_back(redundancyGroupId);
Dictionary::Ptr groupData = new Dictionary({
{"environment_id", m_EnvironmentId},
{"name", redundancyGroupId},
{"display_name", redundancyGroup}});
redundancyGroups.emplace_back(JsonEncode(groupData));
dependencyNodes.emplace_back(dependencyNodeReduId);
Dictionary::Ptr reduNodeData = new Dictionary({
{"environment_id", m_EnvironmentId},
{"redundancy_group_id", redundancyGroupId}});
dependencyNodes.emplace_back(JsonEncode(reduNodeData));
String edgeInId = HashValue(new Array({m_EnvironmentId, dependencyNodeChildId, dependencyNodeReduId}));
dependencyEdges.emplace_back(edgeInId);
Dictionary::Ptr edgeInData = new Dictionary({
{"environment_id", m_EnvironmentId},
{"from_node_id", dependencyNodeChildId},
{"to_node_id", dependencyNodeReduId}});
dependencyEdges.emplace_back(JsonEncode(edgeInData));
String edgeOutId = HashValue(new Array({m_EnvironmentId, dependencyNodeReduId, dependencyNodeParentId}));
dependencyEdges.emplace_back(edgeOutId);
Dictionary::Ptr edgeOutData = new Dictionary({
{"environment_id", m_EnvironmentId},
{"from_node_id", dependencyNodeReduId},
{"to_node_id", dependencyNodeParentId},
{"dependency_id", GetObjectIdentifier(dependency)}});
dependencyEdges.emplace_back(JsonEncode(edgeOutData));
if (runtimeUpdate) {
AddObjectDataToRuntimeUpdates(runtimeUpdates, redundancyGroupId, m_PrefixConfigObject + "redundancygroup", groupData);
AddObjectDataToRuntimeUpdates(runtimeUpdates, dependencyNodeReduId, m_PrefixConfigObject + "dependency:node", reduNodeData);
AddObjectDataToRuntimeUpdates(runtimeUpdates, edgeInId, m_PrefixConfigObject + "dependency:edge", edgeInData);
AddObjectDataToRuntimeUpdates(runtimeUpdates, edgeOutId, m_PrefixConfigObject + "dependency:edge", edgeOutData);
}
} else {
String edgeId = HashValue(new Array({m_EnvironmentId, dependencyNodeChildId, dependencyNodeParentId}));
dependencyEdges.emplace_back(edgeId);
Dictionary::Ptr edgeData = new Dictionary({
{"environment_id", m_EnvironmentId},
{"from_node_id", dependencyNodeChildId},
{"to_node_id", dependencyNodeParentId},
{"dependency_id", GetObjectIdentifier(dependency)}});
dependencyEdges.emplace_back(JsonEncode(edgeData));
if (runtimeUpdate) {
AddObjectDataToRuntimeUpdates(runtimeUpdates, edgeId, m_PrefixConfigObject + "dependency:edge", edgeData);
}
}
}
if (type == TimePeriod::TypeInstance) {
TimePeriod::Ptr timeperiod = static_pointer_cast<TimePeriod>(object);
@ -1257,44 +1135,156 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
}
}
void IcingaDB::UpdateDependencyState(const Dependency::Ptr& dependency)
/**
* Inserts the dependency data for a Checkable object into the given Redis HMSETs and runtime updates.
*
* This function is responsible for serializing the in memory representation Checkable dependencies into
* Redis HMSETs and runtime updates (if any) according to the Icinga DB schema. The serialized data consists
* of the following Redis HMSETs:
* - RedisKey::DependencyNode: Contains dependency node data representing each host, service, and redundancy group
* in the dependency graph.
* - RedisKey::DependencyEdge: Dependency edge information representing all connections between the nodes.
* - RedisKey::RedundancyGroup: Redundancy group data representing all redundancy groups in the graph.
*
* For initial dumps, it shouldn't be necessary to set the `runtimeUpdates` parameter.
*
* @param checkable The checkable object to extract dependencies from.
* @param hMSets The map of Redis HMSETs to insert the dependency data into.
* @param runtimeUpdates If set, runtime updates are additionally added to this vector.
*/
void IcingaDB::InsertCheckableDependencies(
const Checkable::Ptr& checkable,
std::map<String, RedisConnection::Query>& hMSets,
std::vector<Dictionary::Ptr>* runtimeUpdates
)
{
if (!m_Rcon || !m_Rcon->IsConnected()) {
// Only generate a dependency node event if the Checkable is actually part of some dependency graph.
// That's, it either depends on other Checkables or others depend on it, and in both cases, we have
// to at least generate a dependency node entry for it.
if (!checkable->HasAnyDependencies()) {
return;
}
auto& redundancyGroupStates (hMSets[m_PrefixConfigObject + "redundancygroup:state"]);
// First and foremost, generate a dependency node entry for the provided Checkable object and insert it into
// the HMSETs map and if set, the `runtimeUpdates` vector.
auto [host, service] = GetHostService(checkable);
auto checkableId(GetObjectIdentifier(checkable));
{
Dictionary::Ptr data(new Dictionary{{"environment_id", m_EnvironmentId}, {"host_id", GetObjectIdentifier(host)}});
if (service) {
data->Set("service_id", checkableId);
}
String redundancyGroup = dependency->GetRedundancyGroup();
AddDataToHmSets(hMSets, RedisKey::DependencyNode, checkableId, data);
if (runtimeUpdates) {
AddObjectDataToRuntimeUpdates(*runtimeUpdates, checkableId, m_PrefixConfigObject + "dependency:node", data);
}
}
if (!redundancyGroup.IsEmpty()) {
Host::Ptr childHost;
Service::Ptr childService;
tie(childHost, childService) = GetHostService(dependency->GetChild());
for (auto& dependencyGroup : checkable->GetDependencyGroups()) {
String edgeFromNodeId(checkableId);
String dependencyNodeChildId = HashValue(
(childService)
? new Array({ m_EnvironmentId, GetObjectIdentifier(childHost), GetObjectIdentifier(childService) })
: new Array({ m_EnvironmentId, GetObjectIdentifier(childHost) }));
String redundancyGroupId = HashValue(new Array({
m_EnvironmentId,
redundancyGroup,
dependencyNodeChildId}));
if (dependencyGroup->IsRedundancyGroup()) {
auto redundancyGroupId(HashValue(new Array{m_EnvironmentId, dependencyGroup->GetCompositeKey()}));
dependencyGroup->SetIcingaDBIdentifier(redundancyGroupId);
redundancyGroupStates.emplace_back(redundancyGroupId);
Dictionary::Ptr groupStateData = new Dictionary({
edgeFromNodeId = redundancyGroupId;
// During the initial config sync, multiple children can depend on the same redundancy group, sync it only
// the first time it is encountered. Though, if this is a runtime update, we have to re-serialize and sync
// the redundancy group unconditionally, as we don't know whether it was already synced or the context that
// triggered this update.
if (runtimeUpdates || m_DumpedGlobals.DependencyGroup.IsNew(redundancyGroupId)) {
Dictionary::Ptr groupData(new Dictionary{
{"environment_id", m_EnvironmentId},
{"display_name", dependencyGroup->GetRedundancyGroupName()},
});
// Set/refresh the redundancy group data in the Redis HMSETs (redundancy_group database table).
AddDataToHmSets(hMSets, RedisKey::RedundancyGroup, redundancyGroupId, groupData);
Dictionary::Ptr nodeData(new Dictionary{
{"environment_id", m_EnvironmentId},
{"redundancy_group_id", redundancyGroupId},
});
// Obviously, the redundancy group is part of some dependency chain, thus we have to generate
// dependency node entry for it as well.
AddDataToHmSets(hMSets, RedisKey::DependencyNode, redundancyGroupId, nodeData);
if (runtimeUpdates) {
// Send the same data sent to the Redis HMSETs to the runtime updates stream as well.
AddObjectDataToRuntimeUpdates(*runtimeUpdates, redundancyGroupId, m_PrefixConfigObject + "redundancygroup", groupData);
AddObjectDataToRuntimeUpdates(*runtimeUpdates, redundancyGroupId, m_PrefixConfigObject + "dependency:node", nodeData);
}
}
Dictionary::Ptr data(new Dictionary{
{"environment_id", m_EnvironmentId},
{"redundancy_group_id", redundancyGroupId},
{"failed", !((childService) ? childService->IsReachable() : childHost->IsReachable())},
{"last_state_change", TimestampToMilliseconds(Utility::GetTime())}});
redundancyGroupStates.emplace_back(JsonEncode(groupStateData));
{"from_node_id", checkableId},
{"to_node_id", redundancyGroupId},
// All redundancy group members share the same state, thus use the group ID as a reference.
{"dependency_edge_state_id", redundancyGroupId},
{"display_name", dependencyGroup->GetRedundancyGroupName()},
});
// TODO
// AddObjectDataToRuntimeUpdates(runtimeUpdates, redundancyGroupId, m_PrefixConfigObject + "redundancygroup:state", groupStateData);
// dataClone->Set("id", objectKey); // redundancyGroupId
// dataClone->Set("redis_key", redisKey); // m_PrefixConfigObject + "redundancygroup:state"
// dataClone->Set("runtime_type", "upsert");
// runtimeUpdates.emplace_back(dataClone);
// Generate a dependency edge entry representing the connection between the Checkable and the redundancy
// group. This Checkable dependes on the redundancy group (is a child of it), thus the "dependency_edge_state_id"
// is set to the redundancy group ID. Note that if this group has multiple children, they all will have the
// same "dependency_edge_state_id" value.
auto edgeId(HashValue(new Array{checkableId, redundancyGroupId}));
AddDataToHmSets(hMSets, RedisKey::DependencyEdge, edgeId, data);
if (runtimeUpdates) {
AddObjectDataToRuntimeUpdates(*runtimeUpdates, edgeId, m_PrefixConfigObject + "dependency:edge", data);
}
}
auto dependencies(dependencyGroup->GetDependenciesForChild(checkable.get()));
// Sort the dependencies by their parent Checkable object to ensure that all dependency objects that share the
// same parent Checkable are placed next to each other in the container. See the while loop below for more info!
std::sort(dependencies.begin(), dependencies.end(), [](const Dependency::Ptr& lhs, const Dependency::Ptr& rhs) {
return lhs->GetParent() < rhs->GetParent();
});
// Traverse through each dependency objects within the current dependency group the provided Checkable depend
// on and generate a dependency edge entry. The generated dependency edge "from_node_id" may vary depending on
// whether the dependency group is a redundancy group or not. If it's a redundancy group, the "from_node_id"
// will be the redundancy group ID; otherwise, it will be the current Checkable ID. However, the "to_node_id"
// value will always be the parent Checkable ID of the dependency object.
for (auto it(dependencies.begin()); it != dependencies.end(); /* no increment */) {
auto dependency(*it);
auto parent(dependency->GetParent());
auto displayName(dependency->GetShortName());
// In case there are multiple Dependency objects with the same parent, these are merged into a single edge
// to prevent duplicate edges in the resulting graph. All objects with the same parent were placed next to
// each other by the sort function above.
//
// Additionally, the iterator for the surrounding loop is incremented by this loop: after it has finished,
// "it" will either point to the next dependency with a different parent or to the end of the container.
while (++it != dependencies.end() && (*it)->GetParent() == parent) {
displayName += ", " + (*it)->GetShortName();
}
Dictionary::Ptr data(new Dictionary{
{"environment_id", m_EnvironmentId},
{"from_node_id", edgeFromNodeId},
{"to_node_id", GetObjectIdentifier(parent)},
{"dependency_edge_state_id", HashValue(new Array{
dependencyGroup->IsRedundancyGroup()
? dependencyGroup->GetIcingaDBIdentifier()
: dependencyGroup->GetCompositeKey(),
GetObjectIdentifier(dependency->GetParent()),
})},
{"display_name", std::move(displayName)},
});
auto edgeId(HashValue(new Array{data->Get("from_node_id"), data->Get("to_node_id")}));
AddDataToHmSets(hMSets, RedisKey::DependencyEdge, edgeId, data);
if (runtimeUpdates) {
AddObjectDataToRuntimeUpdates(*runtimeUpdates, edgeId, m_PrefixConfigObject + "dependency:edge", data);
}
}
}
}
@ -1632,32 +1622,6 @@ bool IcingaDB::PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& a
return true;
}
if (type == Dependency::TypeInstance) {
Dependency::Ptr dependency = static_pointer_cast<Dependency>(object);
String redundancyGroup = dependency->GetRedundancyGroup();
attributes->Set("name", GetObjectIdentifier(dependency));
if (!redundancyGroup.IsEmpty()) {
Host::Ptr childHost;
Service::Ptr childService;
tie(childHost, childService) = GetHostService(dependency->GetChild());
String dependencyNodeChildId = HashValue(
(childService)
? new Array({ m_EnvironmentId, GetObjectIdentifier(childHost), GetObjectIdentifier(childService) })
: new Array({ m_EnvironmentId, GetObjectIdentifier(childHost) }));
String redundancyGroupId = HashValue(new Array({
m_EnvironmentId,
redundancyGroup,
dependencyNodeChildId}));
attributes->Set("redundancy_group_id", redundancyGroupId);
}
return true;
}
if (type == Downtime::TypeInstance) {
Downtime::Ptr downtime = static_pointer_cast<Downtime>(object);
@ -3182,3 +3146,35 @@ void IcingaDB::DeleteRelationship(const String& id, const String& redisKeyWithou
m_Rcon->FireAndForgetQueries(queries, Prio::Config);
}
/**
* Add the provided data to the Redis HMSETs map.
*
* Adds the provided data to the Redis HMSETs map for the provided Redis key. The actual Redis key is determined by
* the provided RedisKey enum. The data will be json encoded before being added to the Redis HMSETs map.
*
* @param hMSets The map of RedisConnection::Query you want to add the data to.
* @param redisKey The key of the Redis object you want to add the data to.
* @param id Unique Redis identifier for the provided data.
* @param data The actual data you want to add the Redis HMSETs map.
*/
void IcingaDB::AddDataToHmSets(std::map<String, RedisConnection::Query>& hMSets, RedisKey redisKey, const String& id, const Dictionary::Ptr& data) const
{
RedisConnection::Query* query;
switch (redisKey) {
case RedisKey::RedundancyGroup:
query = &hMSets[m_PrefixConfigObject + "redundancygroup"];
break;
case RedisKey::DependencyNode:
query = &hMSets[m_PrefixConfigObject + "dependency:node"];
break;
case RedisKey::DependencyEdge:
query = &hMSets[m_PrefixConfigObject + "dependency:edge"];
break;
default:
BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid RedisKey provided"));
}
query->emplace_back(id);
query->emplace_back(JsonEncode(data));
}

View File

@ -38,8 +38,6 @@ IcingaDB::IcingaDB()
m_PrefixConfigObject = "icinga:";
m_PrefixConfigCheckSum = "icinga:checksum:";
m_CheckablesToDependencies = new Dictionary();
}
void IcingaDB::Validate(int types, const ValidationUtils& utils)

View File

@ -90,6 +90,15 @@ private:
Full = Volatile | RuntimeOnly,
};
enum class RedisKey : uint8_t
{
RedundancyGroup,
DependencyNode,
DependencyEdge,
RedundancyGroupState,
DependencyEdgeState,
};
void OnConnectedHandler();
void PublishStatsTimerHandler();
@ -101,9 +110,10 @@ private:
void DeleteKeys(const RedisConnection::Ptr& conn, const std::vector<String>& keys, RedisConnection::QueryPriority priority);
std::vector<String> GetTypeOverwriteKeys(const String& type);
std::vector<String> GetTypeDumpSignalKeys(const Type::Ptr& type);
void InsertCheckableDependencies(const Checkable::Ptr& checkable, std::map<String, RedisConnection::Query>& hMSets,
std::vector<Dictionary::Ptr>* runtimeUpdates);
void InsertObjectDependencies(const ConfigObject::Ptr& object, const String typeName, std::map<String, std::vector<String>>& hMSets,
std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate);
void UpdateDependencyState(const Dependency::Ptr& dependency);
void UpdateState(const Checkable::Ptr& checkable, StateUpdate mode);
void SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpdate);
void CreateConfigUpdate(const ConfigObject::Ptr& object, const String type, std::map<String, std::vector<String>>& hMSets,
@ -113,6 +123,7 @@ private:
void AddObjectDataToRuntimeUpdates(std::vector<Dictionary::Ptr>& runtimeUpdates, const String& objectKey,
const String& redisKey, const Dictionary::Ptr& data);
void DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum = false);
void AddDataToHmSets(std::map<String, RedisConnection::Query>& hMSets, RedisKey redisKey, const String& id, const Dictionary::Ptr& data) const;
void SendSentNotification(
const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
@ -225,10 +236,8 @@ private:
std::unordered_map<ConfigType*, RedisConnection::Ptr> m_Rcons;
std::atomic_size_t m_PendingRcons;
Dictionary::Ptr m_CheckablesToDependencies;
struct {
DumpedGlobals CustomVar, ActionUrl, NotesUrl, IconImage;
DumpedGlobals CustomVar, ActionUrl, NotesUrl, IconImage, DependencyGroup;
} m_DumpedGlobals;
// m_EnvironmentId is shared across all IcingaDB objects (typically there is at most one, but it is perfectly fine