| #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 |