blob: dd1c6c59e76f8279961ef159e55a839924d4827d [file] [log] [blame]
#include "absl/log/log.h"
#include "dbus_utils.hpp"
#include "time_utils.hpp"
#include "health.hpp"
#include <array>
#include <cstdint>
#include <cstddef>
#include <filesystem>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "logging.hpp"
#include "async_resp.hpp"
#include "dbus_utility.hpp"
#include <nlohmann/json.hpp>
#include "managed_store.hpp"
#include "managed_store_types.hpp"
#include "sdbusplus/unpack_properties.hpp"
namespace redfish
{
/**
* Invoked each time Health status for a resource changes.
*/
void HealthStatusCache::processHealthStatusChange(const std::string& dbusPath)
{
if (healthStatusMap.find(dbusPath) == healthStatusMap.end())
{
LOG(ERROR) << "healthStatusMap missing " << dbusPath;
return;
}
healthStatusMap[dbusPath].health = !healthStatusMap[dbusPath].health;
healthStatusMap[dbusPath].toggleCount += 1;
std::pair<std::string, std::string> redfishDateTimeOffset =
redfish::time_utils::getDateTimeOffsetNow();
if (healthStatusMap[dbusPath].health == false)
{
logEntries.push_back(LogEntry(dbusPath, redfishDateTimeOffset.first,
healthStatusMap[dbusPath].severity));
} else {
logEntries.push_back(
LogEntry(dbusPath, redfishDateTimeOffset.first, "OK"));
}
healthStatusMap[dbusPath].logId = std::to_string(logEntries.size() - 1);
}
/**
* Source of Health attributes is Entity manager configuration.
* objPath points to Dbus object like below:
* /xyz/openbmc_project/inventory/system/board/<BOARD_NAME>/<CRITERIA_ID>
*/
void HealthStatusCache::getHealthAttributes(const std::string& objPath)
{
const std::string configService = "xyz.openbmc_project.EntityManager";
const std::string configInterface =
"xyz.openbmc_project.Configuration.HealthMonitor";
managedStore::GetManagedObjectStore()->getAllProperties(
configService, objPath, configInterface,
managedStore::ManagedObjectStoreContext(nullptr),
[this,
objPath](const boost::system::error_code& ec,
const dbus::utility::DBusPropertiesMap& configProperties) {
if (ec)
{
BMCWEB_LOG_ERROR << "Failed to get config properties for "
<< objPath << ": " << ec.message();
return;
}
if (configProperties.empty())
{
return; // No properties found.
}
const std::string* criteriaId = nullptr;
const std::vector<std::string>* messageArgs = nullptr;
const std::string* failureMessageId = nullptr;
const std::string* okMessageId = nullptr;
const std::string* resolution = nullptr;
const std::string* severity = nullptr;
const std::string* relatedEntity = nullptr;
const std::string* relatedEntityType = nullptr;
if (!sdbusplus::unpackPropertiesNoThrow(
dbus_utils::UnpackErrorPrinter(), configProperties, "Name",
criteriaId, "Severity", severity, "FailureMessageId",
failureMessageId, "OkMessageId", okMessageId, "MessageArgs",
messageArgs, "Resolution", resolution, "RelatedEntityName",
relatedEntity, "RelatedEntityType", relatedEntityType))
{
return;
}
if (criteriaId == nullptr || messageArgs == nullptr ||
failureMessageId == nullptr || okMessageId == nullptr ||
resolution == nullptr || severity == nullptr ||
relatedEntity == nullptr || relatedEntityType == nullptr ||
messageArgs->size() < 2)
{
BMCWEB_LOG_INFO << "Some health attributes not found for "
<< objPath;
return;
}
HealthAttributes attrs;
attrs.criteriaId = *criteriaId;
attrs.messageArgs = *messageArgs;
attrs.severity = *severity;
attrs.failureMessageId = *failureMessageId;
attrs.okMessageId = *okMessageId;
attrs.resolution = *resolution;
attrs.fruName = *relatedEntity;
attrs.fruType = *relatedEntityType;
std::filesystem::path path(objPath);
std::string boardPath = path.parent_path().string();
attrs.boardName = boardPath.substr(
path.parent_path().parent_path().string().size() + 1);
const std::string statusService = "xyz.openbmc_project.HealthMonitor";
std::string statusPath =
"/xyz/openbmc_project/HealthMonitor/" + attrs.criteriaId;
const std::string statusInterface =
"xyz.openbmc_project.HealthMonitor.GetStatus";
managedStore::GetManagedObjectStore()->getAllProperties(
statusService, statusPath, statusInterface,
managedStore::ManagedObjectStoreContext(nullptr),
[this, objPath, boardPath,
attrs = std::move(attrs)](const boost::system::error_code& ec2,
const dbus::utility::DBusPropertiesMap&
statusProperties) mutable {
if (ec2)
{
BMCWEB_LOG_ERROR << "Failed to get status for " << objPath
<< ": " << ec2.message();
return;
}
const bool* healthy = nullptr;
const uint64_t* toggleCount = nullptr;
if (!sdbusplus::unpackPropertiesNoThrow(
dbus_utils::UnpackErrorPrinter(), statusProperties,
"healthy", healthy, "toggle_count", toggleCount))
{
return;
}
if (healthy == nullptr || toggleCount == nullptr)
{
BMCWEB_LOG_INFO << "Health status not found for " << objPath;
return;
}
attrs.health = *healthy;
attrs.toggleCount = *toggleCount;
entityCache[boardPath][attrs.fruType][attrs.fruName].insert(
objPath);
if (!attrs.health)
{
std::pair<std::string, std::string> redfishDateTimeOffset =
redfish::time_utils::getDateTimeOffsetNow();
logEntries.emplace_back(LogEntry(
objPath, redfishDateTimeOffset.first, attrs.severity));
attrs.logId = std::to_string(logEntries.size() - 1);
}
healthStatusMap[objPath] = std::move(attrs);
#ifndef UNIT_TEST_BUILD
/*
* This call subscribes to a live D-Bus signal from the HealthMonitor.
* This directive excludes the live subscription call during the unit test
* build, allowing the test to verify logic without attempting to create
* a real signal match which does not exist in the test environment.
*/
managedStore::GetManagedObjectStore()->subscribeToHealthMonitor(
"/xyz/openbmc_project/HealthMonitor/" +
healthStatusMap[objPath].criteriaId);
#endif /* UNIT_TEST_BUILD */
});
});
}
HealthStatusCache::HealthStatusCache()
{
constexpr std::array<std::string_view, 1> interfaces = {
"xyz.openbmc_project.Configuration.HealthMonitor"};
managedStore::GetManagedObjectStore()->getSubTreePaths(
"/xyz/openbmc_project/inventory", 0, interfaces,
managedStore::ManagedObjectStoreContext(nullptr),
[this](const boost::system::error_code& ec,
const dbus::utility::MapperGetSubTreePathsResponse& resp) {
if (ec)
{
BMCWEB_LOG_ERROR << "Health monitor GetSubTreePaths failed: "
<< ec.message();
return;
}
if (resp.empty())
{
BMCWEB_LOG_ERROR << "No Health evaluation criterias found.";
return;
}
for (const auto& objPath : resp)
{
getHealthAttributes(objPath);
}
});
}
/**
* Populates the Health status information in asyncResp.
*
* @return On success, returns std::nullopt.
* On failure, returns a string with an error message.
*/
std::optional<std::string> HealthStatusCache::healthStatusPopulate(
std::shared_ptr<bmcweb::AsyncResp>& asyncResp, const std::string& oDataId,
const std::string& boardPath, const std::string& entityType,
const std::string& entityName, nlohmann::json::json_pointer statusPtr)
{
auto boardIt = entityCache.find(boardPath);
if (boardIt == entityCache.end())
{
return "Board path '" + boardPath + "' not found in entity cache.";
}
auto typeIt = boardIt->second.find(entityType);
if (typeIt == boardIt->second.end())
{
return "Entity type '" + entityType + "' not found for board path '" +
boardPath + "'.";
}
auto nameIt = typeIt->second.find(entityName);
if (nameIt == typeIt->second.end())
{
return "Entity name '" + entityName + "' not found for entity type '" +
entityType + "'.";
}
// Now it's safe to get the set of entity paths
const std::set<std::string>& entityPaths = nameIt->second;
size_t i = 0;
nlohmann::json& jsonStatus = asyncResp->res.jsonValue[statusPtr];
nlohmann::json& health = jsonStatus["Health"];
nlohmann::json& rollup = jsonStatus["HealthRollup"];
// Create Condition redfish resource for unhealthy Object
for (const auto& entityPath : entityPaths)
{
if (healthStatusMap[entityPath].health)
continue;
std::string severity = healthStatusMap[entityPath].severity;
std::vector<std::string> messageArgs =
healthStatusMap[entityPath].messageArgs;
jsonStatus["Conditions"][i]["OriginOfCondition"]["@odata.id"] = oDataId;
jsonStatus["Conditions"][i]["MessageId"] =
healthStatusMap[entityPath].failureMessageId;
jsonStatus["Conditions"][i]["Severity"] =
healthStatusMap[entityPath].severity;
jsonStatus["Conditions"][i]["Messages"] =
"The resource property " + messageArgs[0] +
" has detected errors of type " + messageArgs[1];
jsonStatus["Conditions"][i]["MessageArgs"] = messageArgs;
jsonStatus["Conditions"][i]["Resolution"] =
healthStatusMap[entityPath].resolution;
jsonStatus["Conditions"][i]["Resolved"] =
healthStatusMap[entityPath].health;
jsonStatus["Conditions"][i]["ErrorCount"] =
(healthStatusMap[entityPath].toggleCount + 1) / 2;
jsonStatus["Conditions"][i]["DateTime"] =
healthStatusMap[entityPath].timestamp;
jsonStatus["Conditions"][i]["LogEntry"]["@odata.id"] =
"/redfish/v1/Systems/system/LogServices/HealthMonitor/Entries/" +
healthStatusMap[entityPath].logId;
if (severity == "Critical")
{
health = "Critical";
rollup = "Critical";
} else if (severity == "Warning" && health == "OK") {
health = "Warning";
rollup = "Warning";
}
i++;
}
return std::nullopt; // Success
}
/**
* Fills a single log entry field
*/
void HealthStatusCache::fillLogEntry(nlohmann::json::object_t& logEntry,
size_t i, const std::string& systemName)
{
std::string dbusPath = logEntries[i].dbus_path;
if (healthStatusMap.find(dbusPath) == healthStatusMap.end())
{
LOG(ERROR) << "healthStatusMap missing " << dbusPath;
return;
}
std::vector<std::string> messageArgs =
healthStatusMap[dbusPath].messageArgs;
std::string severity = logEntries[i].severity;
logEntry["@odata.id"] = "/redfish/v1/Systems/" + systemName +
"/LogServices/HealthMonitor/Entries/" +
std::to_string(i);
logEntry["@odata.type"] = "#LogEntry.v1_9_0.LogEntry";
if (!healthStatusMap[dbusPath].oDataId.empty())
{
logEntry["OriginOfCondition"]["@odata.id"] =
healthStatusMap[dbusPath].oDataId;
} else { // in case oDataId is not initialized
logEntry["OriginOfCondition"]["@odata.id"] =
"HealthEvaluationCriteria=" + healthStatusMap[dbusPath].criteriaId;
}
logEntry["Created"] = logEntries[i].timestamp;
logEntry["EntryType"] = "Oem";
logEntry["Id"] = std::to_string(i);
logEntry["Message"] = "The resource property " + messageArgs[0] +
" has detected errors of type " + messageArgs[1];
logEntry["MessageArgs"] = messageArgs;
logEntry["ResolvedEntryId"] = "";
if (logEntries[i].severity == "OK")
{
logEntry["MessageId"] = healthStatusMap[dbusPath].okMessageId;
logEntry["Resolved"] = true;
for (int j = static_cast<int>(i) - 1; j >= 0; --j)
{ // Find previous log entry that corresponds to this resolved entry.
if (logEntries[static_cast<size_t>(j)].dbus_path == dbusPath &&
logEntries[static_cast<size_t>(j)].severity != "OK")
{
logEntry["ResolvedEntryId"] = std::to_string(j);
break;
}
}
} else {
logEntry["MessageId"] = healthStatusMap[dbusPath].failureMessageId;
logEntry["ErrorCount"] =
(healthStatusMap[dbusPath].toggleCount + 1) / 2;
logEntry["Resolved"] = false;
}
logEntry["Resolution"] = healthStatusMap[dbusPath].resolution;
logEntry["Name"] = "Health Monitor Log Entry";
logEntry["OemRecordFormat"] = "Health Monitor Log Entry";
logEntry["Severity"] = severity;
}
size_t HealthStatusCache::fillLogEntries(nlohmann::json& logEntryArray,
const std::string& systemName)
{
logEntryArray = nlohmann::json::array();
size_t entryCount = logEntries.size();
for (size_t i = 0; i < entryCount; ++i)
{
nlohmann::json::object_t logEntry;
fillLogEntry(logEntry, i, systemName);
logEntryArray.push_back(std::move(logEntry));
}
return entryCount;
}
} // namespace redfish