| #include "health.hpp" | |
| #include <array> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <filesystem> // NOLINT | |
| #include <memory> | |
| #include <optional> | |
| #include <set> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/log/log.h" | |
| #include "logging.hpp" | |
| #include "async_resp.hpp" | |
| #include "dbus_utility.hpp" | |
| #include "dbus_utils.hpp" | |
| #include "time_utils.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, | |
| [](const std::string& dbusPath) { | |
| HealthStatusCache::getInstance().processHealthStatusChange( | |
| dbusPath); | |
| }); | |
| #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 |